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

> Discover how Ego-Lite leverages Chrome DevTools Protocol CDP for robust headless automation. Explore its three-layer CDP stack for efficient browser control.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: how-to-guide
- Published: 2026-08-24

---

**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](https://github.com/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`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts)

All CDP communication originates in [`package/ego-browser/src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/cdp-eval.ts)

The [`package/ego-browser/src/cdp-eval.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/driver/nav.ts).
2. **Navigator** invokes `cdp('Page.navigate', {url})`.
3. **CDP Eval** forwards to `state.send` (lines 10–22 in [`state.ts`](https://github.com/citrolabs/ego-lite/blob/main/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

```typescript
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

```typescript
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.

### Navigating to a URL

```typescript
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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/cdp-eval.ts) provides `evaluate` and `cdp` helpers that driver modules ([`nav.ts`](https://github.com/citrolabs/ego-lite/blob/main/nav.ts), [`pointer.ts`](https://github.com/citrolabs/ego-lite/blob/main/pointer.ts), [`keyboard.ts`](https://github.com/citrolabs/ego-lite/blob/main/keyboard.ts)) use for actions like clicking, typing, and screenshotting.
- **State Isolation**: The [`state.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/state.ts) module exports a `cdpOverride` hook. When set, the `cdp` helper in [`cdp-eval.ts`](https://github.com/citrolabs/ego-lite/blob/main/cdp-eval.ts) routes all CDP requests through your override function instead of the default `state.send` → `browserCdp` 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`](https://github.com/citrolabs/ego-lite/blob/main/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.