How Ego-Lite Uses Chrome DevTools Protocol (CDP) for Headless Automation

Ego-Lite drives headless Chrome through a three-layer CDP stack—rawCdp for transport, browserCdp for session resilience, and high-level helpers like evaluate—wrapping raw protocol commands into ergonomic async APIs.

The Chrome DevTools Protocol (CDP) is the backbone of the citrolabs/ego-lite browser automation framework. Instead of relying on external drivers like Selenium or Playwright, ego-lite communicates directly with a headless Chrome instance via CDP, managing session lifecycle, error recovery, and event buffering internally.

Core CDP Architecture in browser-runtime.ts

All CDP communication originates in package/ego-browser/src/browser-runtime.ts, which exposes three critical functions that form a reliability pyramid.

rawCdp: Low-Level Transport

The rawCdp function (lines 38–77) dispatches raw CDP requests through globalThis.ego.sendCDPMessage. It serializes each command with a unique id, registers a pending promise, and resolves or rejects when the matching response arrives via onCDPMessage. This layer handles transport errors and request timeouts but assumes a valid session already exists.

browserCdp: Session-Aware Wrapper

Sitting above rawCdp, the browserCdp function (lines 79–105) provides the primary entry point for all CDP traffic. Before sending any command, it invokes ensureSession to verify an active CDP session is attached to the current tab. If a request fails with a "Session not found" error (matched by the SESSION_LOST regex), browserCdp automatically discards the stale session, re-attaches to the target, and retries the original command.

ensureSession: Tab Attachment and Caching

The ensureSession function (lines 107–144) implements session lifecycle management:

  • Discovery: Looks up the active Chrome tab.
  • Attachment: Calls Target.attachToTarget to create a CDP session.
  • Enablement: Sends Page.enable to start receiving page-level events.
  • Caching: Stores the session ID for 2 seconds (SESSION_TTL_MS) to avoid re-attaching on every call.

Session Resilience and Event Buffering

Ego-lite’s CDP implementation anticipates target-loss events common in long-running automation tasks.

Automatic Recovery

When browserCdp detects a session-loss error, it triggers a full re-attachment cycle without throwing to the caller. This makes CDP usage resilient to tab crashes, navigation-induced detachment, or timeout errors.

Event Buffering

Incoming CDP events are stored in an in-memory queue (events) capped at 10,000 entries (MAX_BUFFERED_EVENTS). This prevents unbounded memory growth when the consumer temporarily outpaces the event stream.

High-Level CDP Abstractions in cdp-eval.ts

The package/ego-browser/src/cdp-eval.ts module translates raw protocol verbs into JavaScript-friendly helpers used by the driver layer.

The cdp Helper

The cdp(method, params, sessionId?) function (lines 12–25) forwards commands to state.send, which delegates to browserCdp (unless a cdpOverride is set for testing). It also mirrors Network domain state, automatically tracking whether Network.enable or Network.disable has been called.

The evaluate Helper

The evaluate(pageFunction, arg?) function (lines 28–66) wraps Runtime.evaluate to execute JavaScript in the page context. It accepts either a function (stringified internally) or a plain expression, automatically wraps top-level return statements in an IIFE, and handles legacy target-id strings for backward compatibility.

End-to-End CDP Flow

Understanding the request path clarifies how ego-lite isolates transport details from business logic:

  1. Agent calls page.goto('https://example.com') in driver/nav.ts.
  2. Navigator invokes cdp('Page.navigate', {url}).
  3. CDP Eval forwards to state.send (lines 10–22 in state.ts).
  4. State delegates to browserCdp.
  5. Runtime ensures the session via ensureSession, then calls rawCdp.
  6. Transport serializes the request and sends via globalThis.ego.sendCDPMessage.
  7. Response propagates back through the promise chain to resolve the original goto call.

Practical Code Examples

Sending a Raw CDP Command

import { cdp } from "ego-browser/src/cdp-eval.js";

async function getViewportSize() {
  const result = await cdp("Page.getLayoutMetrics");
  return result.layoutViewport;
}

The cdp helper automatically manages the current session and retries if the session disappears mid-request.

Evaluating JavaScript in the Page

import { evaluate } from "ego-browser/src/cdp-eval.js";

async function getTitle() {
  // Pass a function; the return value is serialized back.
  return await evaluate(() => document.title);
}

evaluate converts the function to a string, sends it via Runtime.evaluate, and returns the deserialized result.

import { cdp } from "ego-browser/src/cdp-eval.js";

async function go(url: string) {
  await cdp("Page.navigate", { url });
  // Wait for the load event
  await cdp("Page.loadEventFired");
}

The internal nav.ts driver uses this pattern; the example demonstrates the low-level CDP steps explicitly.

Summary

  • Three-Layer Architecture: rawCdp handles transport, browserCdp manages sessions, and ensureSession caches attachments for 2 seconds.
  • Resilient Sessions: Automatic re-attachment on "Session not found" errors makes CDP calls robust against target loss.
  • Event Safety: CDP events are buffered up to 10,000 entries to prevent memory leaks.
  • High-Level API: cdp-eval.ts provides evaluate and cdp helpers that driver modules (nav.ts, pointer.ts, keyboard.ts) use for actions like clicking, typing, and screenshotting.
  • State Isolation: The state.ts singleton decouples the driver layer from transport details via state.send and cdpOverride hooks.

Frequently Asked Questions

How does ego-lite handle lost CDP sessions during long-running tasks?

Ego-lite detects session-loss errors using the SESSION_LOST regex in browserCdp. When triggered, it automatically discards the stale session ID, re-invokes ensureSession to attach to the current tab via Target.attachToTarget, and retries the failed command without propagating the error to the user script.

What is the maximum number of CDP events ego-lite can buffer?

The framework stores incoming CDP events in an in-memory queue limited to 10,000 entries, defined by MAX_BUFFERED_EVENTS in browser-runtime.ts. Once the buffer hits this ceiling, old events are discarded to prevent unbounded memory growth.

Can I override the default CDP transport for testing?

Yes. The state.ts module exports a cdpOverride hook. When set, the cdp helper in cdp-eval.ts routes all CDP requests through your override function instead of the default state.sendbrowserCdp chain, enabling easy mocking or proxying of protocol messages.

How does evaluate differ from a raw Runtime.evaluate call?

The evaluate helper in cdp-eval.ts provides automatic serialization: it converts JavaScript functions to strings, wraps expressions containing top-level return in an IIFE, and handles deserialization of the return value. It also manages legacy target-id attachments, offering a more ergonomic interface than raw Runtime.evaluate parameters.

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 →