Architecture of Ego-Browser's CDP Transport Layer: Inside the Chrome DevTools Protocol Implementation

Ego-Browser's CDP transport layer is a resilient, session-aware wrapper around the host-provided globalThis.ego.sendCDPMessage API that manages message framing, pending request tracking, and automatic session recovery to provide a Promise-based interface for Chrome DevTools Protocol commands.

The citrolabs/ego-lite repository implements a lightweight browser automation SDK that abstracts raw CDP complexity through a specialized transport architecture. This system handles the complete lifecycle of protocol messages—from ID assignment and timeout management to session attachment and error propagation—enabling reliable communication between automation scripts and the browser host.

Message Framing and ID Management

In src/browser-runtime.ts, every outgoing CDP request is assigned a monotonically increasing numeric identifier via nextMessageId (lines 38‑53). The transport wraps each command in a JSON payload structure that includes the method name, parameters, and optionally a sessionId when targeting specific browser tabs. This framing ensures that responses can be correctly correlated with their originating requests across the asynchronous boundary.

Pending Request Tracking

The transport maintains an in-flight request registry using a Map named pending (lines 39‑50, 59‑65, 71‑77). When sendCDPMessage dispatches a command, it stores a promise resolver in this map, keyed by the message ID. Upon receiving a response, the handleMessage function extracts the matching id, retrieves the corresponding resolver from pending, and resolves or rejects the promise based on the response payload. This mechanism guarantees one-to-one request-response correlation even under high concurrency.

Timeout Handling

To prevent indefinite blocking, each CDP call is wrapped in a Promise that automatically rejects after RESPONSE_TIMEOUT_MS (default 15 seconds) as implemented in lines 55‑58 of src/browser-runtime.ts. If the host fails to respond within this window, the pending resolver is removed from the map and rejected with a timeout error, ensuring that automation scripts fail fast rather than hanging indefinitely.

Session Management Architecture

The transport layer implements intelligent session handling for page-level commands—those not prefixed with Target. or Browser.—ensuring that automation targets the correct execution context.

Session Attachment and Caching

The ensureSession function inspects cached state in state.sessionId and state.sessionAt (lines 7‑23, 84‑103). If the cached session exceeds SESSION_TTL_MS (2 seconds), the transport enumerates available tabs via ego.listTabs(), selects the preferred or most recent tab, and executes Target.attachToTarget to obtain a fresh sessionId. This TTL-based caching minimizes redundant attachment operations while ensuring that commands execute against the current active tab.

Automatic Session Recovery

When a CDP request fails with a "session lost" error (detected via the SESSION_LOST regex), the transport automatically clears state.sessionId and invokes ensureSession to establish a new session before transparently retrying the original request (lines 96‑103). This recovery mechanism handles scenarios where the target page navigates or crashes, providing fault tolerance without requiring manual session management in user scripts.

Event Buffering and Subscription

Incoming CDP events are parsed in handleMessage and stored in a bounded circular buffer configured with MAX_BUFFERED_EVENTS (10,000 entries) to prevent unbounded memory growth during long-running automation sessions (lines 84‑92, 164‑172). Users can subscribe to specific protocol events via subscribeBrowserEvent(eventName, sessionId?, callback), which routes matching events from the buffer to registered listeners. The function returns an unsubscribe handler for cleanup.

Error Propagation

Local send failures—such as when the host task becomes inactive—are caught by handleSendError in src/browser-runtime.ts (lines 24‑31). This function iterates through the pending map and rejects all unresolved promises with a normalized EgoError object defined in src/ego-errors.ts. This ensures that network interruptions or host crashes propagate as structured exceptions rather than silent failures.

Practical Usage Examples

The following patterns demonstrate how the transport layer functions are used in practice:

// Example: sending a CDP command that automatically gets a page session.
import { browserCdp } from 'ego-browser';

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

// Evaluate an expression in the page context
const result = await browserCdp('Runtime.evaluate', {
  expression: 'document.title',
});
console.log('Page title:', result.result.value);
// Example: subscribing to a CDP event (e.g., console messages)
import { subscribeBrowserEvent } from 'ego-browser';

const unsubscribe = subscribeBrowserEvent(
  'Runtime.consoleAPICalled',
  undefined,               // listen on any session
  (event) => console.log('Console:', event.params.args)
);

// Later – stop listening
unsubscribe();
// Example: manually creating a session for a specific tab (advanced)
import { ensureSession, browserCdp } from 'ego-browser';

// Force a new session (useful for a non‑active tab)
const sessionId = await ensureSession();
await browserCdp('Runtime.enable', {}, sessionId);

Summary

  • The transport layer in src/browser-runtime.ts wraps globalThis.ego.sendCDPMessage to provide a Promise-based CDP interface with automatic ID management via nextMessageId.
  • In-flight requests are tracked in a pending Map with a 15-second timeout (RESPONSE_TIMEOUT_MS) to prevent hanging operations.
  • Session management caches sessionId for 2 seconds (SESSION_TTL_MS) and automatically recovers from "session lost" errors by re-attaching to targets.
  • Incoming events are buffered in a circular queue limited to 10,000 entries (MAX_BUFFERED_EVENTS) and exposed through subscribeBrowserEvent.
  • All transport errors are normalized to EgoError objects via handleSendError for consistent error handling across the SDK.

Frequently Asked Questions

How does ego-browser handle CDP session timeouts?

The transport implements a time-to-live (TTL) mechanism where state.sessionId remains valid for SESSION_TTL_MS (2 seconds). When invoking page-level commands, ensureSession checks this cache and only re-executes Target.attachToTarget if the session has expired, minimizing protocol overhead while ensuring fresh execution contexts.

What happens when a CDP request fails with a session error?

If a response matches the SESSION_LOST regex, the transport clears the cached session ID and invokes ensureSession to create a new session, then automatically retries the original request without throwing to the caller. This transparent recovery handles page navigations and target detaches seamlessly (lines 96‑103 in src/browser-runtime.ts).

How does the transport prevent memory leaks during long-running scripts?

The handleMessage function stores CDP events in a bounded circular buffer with a capacity of MAX_BUFFERED_EVENTS (10,000). Once the buffer reaches capacity, oldest events are overwritten, ensuring that memory usage remains constant regardless of how long the automation runtime executes.

Can I subscribe to specific CDP events like console logs?

Yes. Import subscribeBrowserEvent from ego-browser to register callbacks for specific protocol events such as Runtime.consoleAPICalled. The function accepts an optional sessionId filter (pass undefined to listen globally) and returns an unsubscribe function to stop listening and free resources.

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 →