CDP Message Protocol and Session Attachment Flow in Ego-Lite: A Deep Dive

Ego-Lite drives the Chromium DevTools Protocol (CDP) through a robust message bridge that automatically manages request/response pairing, timeouts, and page-level session attachment with automatic retry logic.

The CDP message protocol in Ego-Lite abstracts the low-level Chromium DevTools Protocol into a reliable JavaScript API. According to the citrolabs/ego-lite source code, this implementation centers on the ego.sendCDPMessage bridge exposed by the host runtime (globalThis.ego), with core coordination handled in browser-runtime.ts and mutable state managed by the singleton in state.ts.

CDP Request/Response Handling

Every interaction with the browser follows a strict request/response lifecycle. The system assigns unique message IDs, enforces timeouts, and routes errors through a centralized handler.

Message ID Management

Each outgoing CDP request receives a monotonically increasing id generated by nextMessageId. The promise is stored in the pending map, keyed by this ID, allowing handleMessage to match incoming replies to their originating requests.

In browser-runtime.ts, the ID generation and pending registration occur before the bridge transfer:

// Conceptual flow from browser-runtime.ts
const id = ++nextMessageId;
pending.set(id, { resolve, reject, timer });
const payload = JSON.stringify({ id, method, params, sessionId });

Timeout and Error Handling

Requests automatically abort if the host runtime fails to respond within 15 seconds (RESPONSE_TIMEOUT_MS = 15000 ms). Error routing distinguishes between synchronous bridge failures and runtime send-errors:

  • Synchronous throws: If runtime.sendCDPMessage throws immediately, the promise rejects with the raw error.
  • Send-errors: If the bridge reports a transport failure, handleSendError rejects all pending promises with a unified EgoError, ensuring no dangling promises remain in the pending map.

Raw Transport Layer

The rawCdp function constructs the JSON payload and forwards it to the host. It accepts an optional sessionId parameter, enabling both root browser commands and targeted page sessions:

// From browser-runtime.ts implementation
function rawCdp(method: string, params?: object, sessionId?: string) {
  const id = ++nextMessageId;
  // ... pending registration ...
  const message = JSON.stringify({ id, method, params, ...(sessionId && { sessionId }) });
  runtime.sendCDPMessage(message);
  return promise;
}

High-Level API

browserCdp serves as the public entry point. It automatically injects the current session ID for all calls except top-level "Browser." or "Target." domains. If a request fails with a session-lost pattern, the function triggers invalidateSession and performs an automatic retry after re-attaching.

Session Attachment Flow

Ego-Lite operates on a page-level CDP session rather than the root Browser domain. The ensureSession function encapsulates the attachment logic with caching, concurrency control, and automatic recovery.

Cached Session Validation

Before creating a new session, the system checks if a valid sessionId exists in state.sessionId and whether it is younger than SESSION_TTL_MS (2 seconds). If cached, the existing session ID returns immediately without additional CDP traffic.

Concurrent Session Creation

To prevent duplicate attachment requests when multiple async operations initiate simultaneously, ensureSession guards against concurrent execution. If another call is already creating a session, the same promise is returned to all awaiters.

Target Discovery and Attachment

When no valid session exists, the flow executes:

  1. Tab discovery: browserEgo().listTabs() retrieves Chrome targets, selecting either the "preferred" target or the active tab.
  2. Attach command: Target.attachToTarget invokes with flatten: true, returning a sessionId bound to the specific targetId.
  3. State update: The sessionId and targetId are stored in the mutable state object, and state.sessionAt records the creation timestamp for TTL checks.

Page Event Initialization

Immediately after attachment, the system sends Page.enable to start buffering page-level events such as JavaScript dialogs and network activity. This ensures event subscribers receive historical context even if they connect after the page loads.

Session Invalidation and Retry

If a CDP request returns a session-lost error (matched against the SESSION_LOST regex), invalidateSession clears state.sessionId and state.targetId. Subsequent calls to browserCdp trigger ensureSession to re-attach automatically. The host also triggers invalidation upon receiving Target.detachedFromTarget or Target.targetDestroyed events.

Event Handling and Buffering

Event Routing

Incoming CDP events parse through handleMessage. If a subscriber exists for the event type (via subscribeBrowserEvent), the listener receives the payload immediately. Otherwise, the event enters a bounded buffer (MAX_BUFFERED_EVENTS = 10,000) for later consumption by helpers like drainBrowserEvents.

Dialog State Tracking

The runtime maintains pendingDialogs per session by listening to Page.javascriptDialogOpening and Page.javascriptDialogClosed. This enables synchronous-style queries about the current dialog state from async helper functions.

Automatic Session Teardown

When the host reports session destruction through Target.detachedFromTarget or Target.targetDestroyed for the current target, invalidateSession runs automatically. This ensures the next CDP call initiates a fresh attachment sequence rather than failing on stale session IDs.

Mutable State Management

The State Singleton

All session-related data lives in the exported state object from state.ts. This singleton stores:

  • sessionId: The active CDP session identifier
  • sessionAt: Timestamp of session creation for TTL validation
  • send: The default request function, defaulting to defaultSend which forwards to browserCdp

Test overrides can inject custom send implementations or session IDs through this state object without modifying the runtime logic.

Summary

  • Message Protocol: Ego-Lite uses monotonic IDs and a pending map to correlate asynchronous CDP responses, with a 15-second timeout and unified error handling via EgoError.
  • Session Management: The ensureSession function implements a 2-second TTL cache, concurrency deduplication, and automatic re-attachment on session loss.
  • Transport Layers: rawCdp handles the JSON-RPC wire format, while browserCdp provides automatic session injection and retry logic.
  • Event System: Unconsumed events buffer up to 10,000 entries, with dedicated tracking for dialog states and automatic cleanup on target destruction.
  • State Architecture: A mutable singleton in state.ts centralizes session data and allows for testable dependency injection.

Frequently Asked Questions

What is the CDP message protocol in ego-lite?

The CDP message protocol in ego-lite is a JavaScript abstraction over the Chromium DevTools Protocol that runs inside a host runtime environment. It exposes ego.sendCDPMessage through globalThis.ego and wraps raw JSON-RPC calls with Promise-based request/response matching, automatic ID generation, and timeout handling.

How does ego-lite handle session attachment to browser targets?

Ego-lite attaches to specific page targets rather than the root browser using Target.attachToTarget with flatten: true. The ensureSession function manages this flow by checking a 2-second TTL cache, preventing concurrent attachment attempts, selecting the active tab via listTabs(), and enabling page events automatically.

What happens when a CDP session is lost in ego-lite?

When a session is lost, either through a session-lost error pattern or Target.detachedFromTarget events, invalidateSession clears the cached sessionId and targetId. The next call to browserCdp automatically triggers re-attachment via ensureSession, making session recovery transparent to higher-level code.

How are CDP events buffered and consumed in ego-lite?

Events arriving without an active subscriber enter a bounded buffer with a maximum capacity of 10,000 events (MAX_BUFFERED_EVENTS). Consumers can retrieve these later using drainBrowserEvents, while active subscribers receive events immediately through subscribeBrowserEvent callbacks.

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 →