Your Browser Relay Says Connected. Your Agent Is Looking at a Dead Tab.
There’s a startup with Marc Benioff money behind it whose pitch, stripped of the deck language, is AI that fixes the AI deployment problem. I smirked at that for about a day before realizing it describes most of my time with browser agents. The model is not the bottleneck. The wiring between the model and the browser is where the hours go.
Browser Relay is the purest version of this. You start OpenClaw, point it at your local Chrome, the indicator turns green, and for a few minutes it’s genuinely great. Then you click a link. Or Chrome restarts to apply an update. And the agent keeps happily narrating a page that stopped existing several minutes ago.
The relay isn’t attached to Chrome. It’s attached to one tab.
This is the part that surprises people. CDP attachment is per-target, and a “target” is roughly a page, not the browser. So the relay holds a session against one specific page target, and that handle is only as durable as the page.
Same-origin navigation usually survives. Cross-origin navigation often does not, because Chrome can swap the renderer process out from under you and hand back a fresh target with a new ID. Even when the target ID survives, every execution context in that frame gets destroyed on navigation, so any bindings or injected helpers the relay set up are gone while the socket itself still looks perfectly healthy, which is exactly why the status light lies to you.
That gap between “socket open” and “socket useful” is the whole bug.
Restarting Chrome takes the front door with it
Say you launched Chrome once with --remote-debugging-port=9222 and a dedicated --user-data-dir. Fine. Now Chrome auto-updates overnight and relaunches, or you quit it and reopen from the taskbar. That relaunch has no flags. No debugging port, no port, no relay. The profile lock also moves, so if anything else grabbed that user-data-dir first you get a silent second profile instead of an error.
The relay reconnects to a port nobody is listening on and reports the connection as pending forever.
And the service worker naps
Manifest V3 workers get evicted after roughly 30 seconds idle. If your relay bridge lives in one, it dies quietly between tasks. The reconnect happens on the next event, which is often several seconds after you asked for something.
So what actually keeps it alive
Assume the connection is temporary and build for that. Concretely, the things that made this stop happening for me:
Stop caching session IDs. Use Target.setAutoAttach with flatten: true and handle Target.attachedToTarget and Target.detachedFromTarget as normal traffic rather than as errors. When a navigation swaps the target, you re-attach instead of noticing twenty seconds later that nothing responds.
Re-inject on every Page.frameNavigated. Anything you put into the page is disposable. Page.addScriptToEvaluateOnNewDocument handles most of it, but you still want the explicit navigation listener for redirects and history API pushes that don’t fire what you expect.
Make the debugging flag permanent. Edit the actual shortcut, or ship a launcher script, so a Chrome restart comes back with the port open. Pair it with a fixed --user-data-dir you control, otherwise you end up in the wrong profile entirely, which is its own separate headache — I wrote about that failure mode here.
Add a heartbeat with backoff. A Runtime.evaluate of 1 every few seconds tells you the session is real, not just that the TCP connection hasn’t been reaped yet. When it fails, reconnect with backoff rather than hammering.
Wake the worker on purpose. chrome.alarms at the minimum interval, or a long-lived port from a content script, keeps the bridge from evicting mid-task.
None of that is hard. It’s just infrastructure you’re maintaining forever for a browser sitting eighteen inches from your face. Claude Code’s extension has the same shape of problem, for the same reason.
The failure mode disappears when there’s no socket
A side panel extension doesn’t attach to a tab. It is part of the browser. Navigation is an event it observes rather than a rupture it recovers from, and when Chrome restarts the extension restarts with it, in whatever profile you were already using, still logged into everything.
That’s the design behind Dassi. It runs in the side panel, reads the page you’re actually looking at, and there’s no port to forward and no relaunch flag to remember. Bring your ChatGPT login or your own API key and the whole reconnection layer just isn’t in the product.
Relay has real uses. Driving a browser on a machine you’re not sitting at is one. But if the browser is right there, spending your evening writing a supervisor for a WebSocket is a hell of a way to avoid installing an extension.