How ego-browser Uses Chrome DevTools Protocol (CDP) for Communication

ego-browser communicates with Chrome DevTools Protocol via a three-layer architecture wrapped around globalThis.ego.sendCDPMessage, providing raw transport handling, automatic session management, and high-level evaluation helpers.

The ego-browser package in the citrolabs/ego-lite repository implements a lightweight, resilient client for Chrome DevTools Protocol (CDP). It abstracts the complexity of direct message passing while maintaining full access to browser automation capabilities, allowing scripts to execute commands, evaluate JavaScript, and subscribe to browser events.

CDP Communication Architecture

The implementation separates concerns into three distinct layers, each handling specific aspects of the protocol lifecycle.

Raw Transport Layer

At the foundation, rawCdp() in package/ego-browser/src/browser-runtime.ts (lines 38-77) manages the direct protocol communication. This function constructs JSON payloads containing {id, method, params, sessionId?}, assigns unique message IDs, and maintains a pending Map to correlate asynchronous responses. It calls the runtime-injected globalThis.ego.sendCDPMessage(payload) to transmit requests and registers a 15-second timeout (RESPONSE_TIMEOUT_MS) to prevent hanging promises.

Session Management Layer

The middle layer handles CDP session lifecycle through browserCdp() and ensureSession() (lines 79-104 and 107-142 in browser-runtime.ts). When a command requires page-level context (non-browser methods), browserCdp() checks for an active session. If missing, ensureSession() discovers the active tab via Target.getTargets, attaches using Target.attachToTarget, and enables page events. This layer automatically injects the sessionId into subsequent commands and detects session loss via regex matching (SESSION_LOST), triggering invalidateSession() and retry logic when connections drop.

High-Level API Layer

The top layer exposes ergonomic methods in package/ego-browser/src/cdp-eval.ts. The cdp() function (lines 12-27) forwards requests through state.send (defaulting to defaultSend() in state.ts lines 10-21), while evaluate() (lines 36-65) constructs Runtime.evaluate commands, optionally attaches to specific targets, and extracts return values. These helpers shield callers from message ID generation, timeout handling, and session details.

Message Flow and Request Lifecycle

Understanding the exact path of a CDP request reveals how the layers coordinate:

  1. Runtime DetectionisBrowserRuntime() verifies the presence of globalThis.ego.sendCDPMessage when the harness loads inside the ego-lite host.

  2. Request Initiation – A script calls cdp('Page.navigate', {url}), which invokes state.send().

  3. Session ResolutionbrowserCdp() inspects the method name. Commands starting with Target. or Browser. bypass session injection; others trigger ensureSession() to obtain a valid sessionId.

  4. Payload ConstructionrawCdp() generates a unique ID, stores the resolver in the pending Map, and calls ego.sendCDPMessage().

  5. Response Handling – The runtime delivers CDP responses to onCDPMessage, where handleMessage (in browser-runtime.ts) parses JSON, matches IDs against the pending Map, and resolves promises. Errors are normalized via buildEgoError().

  6. Event Processing – Unsolicited CDP events (e.g., Page.screencastFrame) enter a bounded events buffer and dispatch to registered subscribers via subscribeBrowserEvent(), enabling waitForEvent() and drainBrowserEvents() helpers.

Handling Edge Cases and Resilience

The architecture includes specific safeguards for production reliability:

  • Automatic Session Recovery – When browserCdp() detects "Session not found" errors using the SESSION_LOST regex, it automatically invalidates the cached session and retries the command on a fresh connection.

  • Request Timeouts – Every request includes a 15-second timeout that cleans up the pending Map entry and rejects the promise if the runtime fails to respond.

  • Browser-Level Commands – Methods prefixed with Target. or Browser. skip session attachment, allowing direct control of the browser process (such as Target.getTargets) without page context requirements.

Practical Code Examples

Send arbitrary CDP commands or evaluate JavaScript using the high-level API:

// Enable network tracking
import { cdp } from 'ego-browser';
await cdp('Network.enable');

// Navigate to a URL
await cdp('Page.navigate', { url: 'https://example.com' });

// Evaluate JavaScript on the current page
import { evaluate } from 'ego-browser';
const title = await evaluate(() => document.title);
console.log('Page title →', title);

// Evaluate on a specific target (legacy string form)
const targetId = 'target-1234';
const href = await evaluate('document.location.href', targetId);

Summary

  • ego-browser wraps CDP communication through globalThis.ego.sendCDPMessage provided by the ego-lite runtime.
  • Three-layer architecture: Raw transport (rawCdp), session management (browserCdp/ensureSession), and high-level helpers (cdp/evaluate).
  • Automatic resilience: Handles session loss detection, automatic reconnection, and 15-second request timeouts.
  • Direct browser control: Browser-level commands bypass session injection while page-level commands automatically manage tab attachment.
  • Event subscription: Unsolicited CDP events are buffered and dispatched to registered listeners for reactive automation.

Frequently Asked Questions

What is the entry point for CDP communication in ego-browser?

The entry point is the globally injected ego object provided by the ego-lite host runtime. The function isBrowserRuntime() checks for globalThis.ego.sendCDPMessage, and all subsequent communication flows through this injected interface rather than a WebSocket or external debugger connection.

How does ego-browser handle lost CDP sessions?

When a command returns a "Session not found" error (matched via SESSION_LOST regex in browserCdp), the client automatically calls invalidateSession() to clear the cached session ID, then retries the original command. This triggers ensureSession() to create a fresh attachment to the active tab, ensuring resilience against page navigations or disconnections.

What timeout applies to CDP requests?

All CDP requests use a 15-second timeout defined by RESPONSE_TIMEOUT_MS. If the runtime does not return a response within this window, the promise is rejected and the pending request is removed from the internal Map to prevent memory leaks.

How can I evaluate JavaScript on a specific target?

Use the evaluate() function from cdp-eval.ts with a target ID string as the second argument: await evaluate('document.title', 'target-1234'). Alternatively, pass a function reference for execution in the current session context, which automatically handles Runtime.evaluate construction and result extraction.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →