**Important Chrome caveat:** modern Google Chrome may refuse DevTools remote debugging on the default everyday profile and show an error like:
```text
DevTools remote debugging requires a non-default data directory.
Specify this using --user-data-dir.
```
When that happens:
- do **not** assume the browser is controllable just because the process command line contains `--remote-debugging-port=9222`
- verify the listener is actually up with `curl http://127.0.0.1:9222/json/version`
- if it refuses connection, Chrome never brought CDP up
**Recommended workaround for a "near-real session" without touching the live default profile:**
- make a **one-time clone** of the default Chrome profile
- launch Chrome with `--user-data-dir` pointed at the clone
- bind CDP to `127.0.0.1`, not `0.0.0.0`
- remove stale `SingletonLock`, `SingletonSocket`, and `SingletonCookie` files from the clone after the initial copy if present
**Important profile-drift lesson:**
- do **not** assume any existing `google-chrome-cdp-clone` directory is a clean baseline for later attach debugging
- if attach hangs or times out against a reused clone, create a **new distinct wrapper/profile pair** (for example `~/.config/google-chrome-cdp-hermes`) rather than silently reusing the old clone
- A/B test attach against the old clone and the new profile explicitly; if the new profile attaches immediately while the old clone times out after websocket connect, treat the issue as **profile/session-state specific**, not generic CDP failure
- once the new profile is proven good, keep it as the canonical Hermes CDP profile instead of inheriting unknown historical session state from the old clone
then the remaining bug is probably **inside the node browser controller's Playwright/CDP attach path**, not Chrome startup and not basic node/gateway routing.
### What this pattern means
That call log proves more than it looks like:
- the CDP HTTP endpoint is reachable
- Playwright successfully fetched the websocket URL
- the websocket connection itself opened
- the stall happens *after* transport connect, during attach completion inside Playwright or immediately after in controller-side attach handling
So stop blaming:
- Chrome not running
- wrong `cdp_url`
- firewall/network reachability
- missing browser-control dispatch, if simple browser commands already succeed
### High-value narrowing sequence
1.**Prove browser-control transport separately**
- Run a cheap command like `list_pages` first.
- If `list_pages` succeeds but attach times out, routing is alive and the fault is attach-specific.
2.**Check the attach implementation in `browser_controller.py`**
- Inspect the `launch(... attach=True ...)` path.
- Look for `connect_over_cdp(...)` followed by any attach-state hydration such as `_ingest_attached_browser_state()`.
- If logs never reach the "attached successfully" line, the timeout is occurring before or during that step.
3.**Add attach-specific observability**
- Log attach success with counts of discovered contexts/pages.
- Return discovered `contexts` / `page_ids` in the attach result.
- Make CDP connect timeout configurable (for example `connect_timeout_ms`) so retries are faster while debugging.
4.**Differentiate Playwright-browser launch failures from attach failures**
This does not guarantee a fix, but it sharply narrows where the stall lives.
### High-probability checks
1.**BrowserController entrypoint mismatch**
- Inspect the live `browser_controller.py` used by the node agent.
- If the node agent calls something like `self.browser.execute(command, params)` or `self.browser.run(command, params)`, verify the controller class actually implements that dispatcher.
- A controller with only method-per-action (`launch`, `navigate`, `click`, etc.) but no `execute()` / `run()` adapter will advertise capability successfully yet fail every real browser command.
2.**Gateway response-schema mismatch**
- Inspect the gateway/browser response handler.
- If the node sends payloads shaped like:
-`type: browser_control_result`
-`success: true/false`
- but the gateway decides success using something like `msg.get("result") == "ok"`, the waiter logic is stale.
- That mismatch can make the browser path look like a timeout even when the node replied.
3.**Verify the live runtime copy, not just repo source**
- Browser-control bugs often survive because the repo file is fixed but the installed node/gateway runtime still uses stale code.
- Inspect the exact installed file on the node and the live gateway plugin copy.
- On packaged Linux nodes, useful live paths are often:
-`/home/nextime/.local/bin/hermes-node-agent`
-`/home/nextime/.local/bin/browser_controller.py`
4.**Prove where the timeout lives with a split-brain check**
- If `node_status` shows `browser_control` available, *and* raw CDP works via:
-`curl http://127.0.0.1:9222/json/version`
-`curl http://127.0.0.1:9222/json/list`
-*and* the installed live node files already contain the expected fixes (for example the agent uses `await self.browser.execute(command, params)` and the controller has `if self.playwright is None: await self.initialize()`),
- but Hermes `browser_control(...)` still returns `Gateway timeout`,
- then the highest-probability remaining fault is the **live gateway/plugin browser-control path**, not Chrome and not the node runtime.
### Diagnostic lesson
Once raw CDP is healthy, stop blaming Chrome. Shift to:
- node-agent browser dispatcher wiring
- gateway waiter/response handling
- runtime copy drift between repo and installed plugin/node files
- and, after ruling those out on the live node, the live Hermes gateway/plugin browser-control path itself
## Important attach-mode pitfall: attach succeeds, but `list_pages` is empty
If all of these are true:
-`launch` with `attach: true` succeeds
-`curl http://127.0.0.1:9222/json/list` shows real Chrome targets/pages
- but Hermes `list_pages` returns `[]`
- and follow-up commands fail with `Page not found: page_X`
then the likely bug is **inside `browser_controller.py` attach-state hydration**, not Chrome startup and not necessarily the gateway.
### Root cause pattern
`connect_over_cdp(...)` can successfully attach to an existing browser session, but the controller's own internal registries may still be empty:
-`self.contexts`
-`self.default_context`
-`self.pages`
If the controller only stores pages it created itself via `create_context()` / `new_page()`, then attached real-session tabs never become addressable by Hermes high-level commands.
### What to inspect
1. Verify live CDP state first:
-`curl http://127.0.0.1:9222/json/version`
-`curl http://127.0.0.1:9222/json/list`
2. Attach via Hermes browser control.
3. Immediately test `list_pages`.
4. Inspect the live `browser_controller.py` used by the node and look for logic that ingests existing browser contexts/pages after `connect_over_cdp(...)`.
### Correct fix shape
After successful attach, hydrate the controller from the attached browser object:
- enumerate `self.browser.contexts`
- set a usable default context
- register existing pages into `self.pages` with stable synthetic IDs like `page_0`, `page_1`, ...
and call it immediately after `connect_over_cdp(...)` succeeds.
### Verification
After patching the live runtime copy:
1. attach again
2. run `list_pages` — it should return real existing tabs
3. run `get_title` / `get_url` against one returned `page_id`
4. only after that test richer navigation/evaluate flows
### Related request-shape footgun
If a command like `get_title` fails with a missing `page_id` positional-argument error even though you supplied a top-level tool `page_id`, inspect how the node protocol passes arguments into `browser_controller.execute(...)`.
In this stack, the controller filters accepted args from `params`, so high-level commands that require `page_id` may need it present inside the message params payload, not only at the outer tool wrapper layer.
That request-shape mismatch is secondary, but it can mask the main attach-state bug.
## Virtual desktops / workspaces on X11
On X11 desktops like `sissy` (XFCE), workspace switching can already be done without adding new protocol actions.
### Read current workspace
Use node exec on the remote node:
```bash
xdotool get_desktop
```
### Read workspace count
```bash
xdotool get_num_desktops
```
### Switch workspace with current computer_control
Use keyboard shortcuts through `computer_control`:
- next workspace:
-`key_press` with `ctrl+alt+Right`
- previous workspace:
-`key_press` with `ctrl+alt+Left`
Example:
```json
{
"action":"key_press",
"params":{"key":"ctrl+alt+Right"}
}
```
### Verify the switch
After switching, verify with:
```bash
xdotool get_desktop
```
### Caveats
- This is an operational pattern, not a first-class `desktop_observe` / `computer_control` workspace API.
- Works for X11/xdotool-friendly desktops.
- Do not assume the same method works unchanged on Wayland or Windows.