# How `ego.sendCDPMessage` Works: The CDP Transport Mechanism in Ego‑Lite

> Understand how ego.sendCDPMessage functions as the CDP transport mechanism in Ego-Lite. Learn how JavaScript sends Chrome DevTools Protocol commands and receives promise-based responses.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: internals
- Published: 2026-08-15

---

**`ego.sendCDPMessage` is the low‑level bridge that Ego‑Lite injects into the page, allowing JavaScript code to send Chrome DevTools Protocol commands to the underlying browser and receive structured responses via promises.**

The **ego‑lite** repository provides the open‑source runtime wrapper around this closed‑source host function. Understanding this mechanism is essential for anyone building browser automation agents or extending Ego‑Lite's capabilities. The implementation lives in [`package/ego-browser/src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts) and handles everything from request serialization to automatic session recovery.

## Runtime Detection and Global Hook

Before any CDP communication begins, the runtime verifies the host environment. The `isBrowserRuntime()` function checks for the presence of `globalThis.ego.sendCDPMessage` at lines 25‑30 of [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts). This Boolean guard prevents the runtime from loading in non‑Ego contexts and is also used throughout driver modules like [`pointer.ts`](https://github.com/citrolabs/ego-lite/blob/main/pointer.ts) and [`keyboard.ts`](https://github.com/citrolabs/ego-lite/blob/main/keyboard.ts) to conditionally expose functionality.

## The Request Lifecycle: From Call to Promise

The core of the mechanism is the `rawCdp()` function (lines 38‑77). Here's the exact flow:

### Step 1: Message Construction

Each CDP call receives a **unique message ID** generated via incrementing counter. The runtime builds a JSON payload with this structure:

```json
{
  "id": 42,
  "method": "DOM.getDocument",
  "params": { "depth": 1 }
}

```

### Step 2: Pending Registration

The runtime stores a **pending entry** containing:
- The message ID
- A `Promise` resolver/rejector pair
- A timeout handle (configurable, typically 30 seconds)

### Step 3: Host Delivery

The payload passes to `runtime.sendCDPMessage(payload)` at lines 44‑46. This is the actual injection point where your JavaScript hands control to the closed‑source ego‑lite binary, which streams the request to Chrome's DevTools endpoint.

### Step 4: Response Routing

When Chrome replies, the host invokes `handleMessage(message)` (lines 32‑50). The runtime:
1. Parses the JSON response
2. Looks up the pending entry by matching `id`
3. Clears the timeout
4. **Resolves** the promise if `result` exists, or **rejects** if `error` is present

## Error Handling for Local Failures

Not all failures come from Chrome. If the host cannot deliver the request—due to inactive tasks, user‑controlled state, or browser process termination—`handleSendError(message, error_code)` triggers at lines 24‑30. This rejects **all pending requests** immediately with a formatted `EgoError` from [`src/ego-errors.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ego-errors.ts), preventing hanging promises.

## Session Management and Auto‑Recovery

Most high‑level helpers like `browserCdp()` first call `ensureSession()` at lines 85‑103. This attaches a CDP session to the target tab. If the session is lost—detected via `SESSION_LOST` regex matching on error messages—the runtime **automatically re‑attaches and retries** the original command transparently.

```javascript
import { browserCdp } from 'ego-browser';

// This handles session loss automatically
const doc = await browserCdp('DOM.getDocument');
console.log(doc.result.root.nodeId);

```

## Event Handling and Buffering

CDP events (messages without an `id` field) are dispatched to subscribers and buffered internally. The runtime exposes these through:

- `drainBrowserEvents()` – returns all buffered events
- `waitForBrowserEvent(predicate, timeout)` – promise‑based filtering

This buffering at lines 70‑92 ensures no events are lost during async gaps between agent instructions.

## High‑Level API Usage

While `rawCdp()` handles transport, most code uses the wrappers in [`src/cdp-eval.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/cdp-eval.ts):

```javascript
import { evaluate, browserCdp } from 'ego-browser';

// Execute arbitrary JavaScript in page context
const title = await evaluate(() => document.title);

// Direct CDP call with full response
const metrics = await browserCdp('Performance.getMetrics');

```

Both helpers ultimately resolve through the `ego.sendCDPMessage` flow described above.

## Key Source Files

| Path | Responsibility |
|------|---------------|
| [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) | Core transport, pending map, timeout handling, session recovery |
| [`src/cdp-eval.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/cdp-eval.ts) | Public API surface (`cdp`, `evaluate`, `browserCdp`) |
| [`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts) | Global mutable state: session IDs, pending queue, event buffers |
| [`src/ego-errors.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ego-errors.ts) | Error code mapping and user‑friendly message formatting |
| `src/driver/*.ts` | Runtime availability checks for input simulation |
| [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) | Agent context injection and sandbox setup |

## Summary

- **`ego.sendCDPMessage`** is the host‑injected primitive that enables CDP communication from within the page
- The runtime wraps it with **promise‑based request/response handling**, **automatic timeouts**, and **session recovery**
- All failures—whether from Chrome or local delivery—surface as **structured rejections** via `EgoError`
- Higher‑level APIs abstract the transport while preserving full access to raw CDP methods
- Event buffering ensures reliable delivery of CDP notifications to automation agents

## Frequently Asked Questions

### What happens if `ego.sendCDPMessage` is not available?

The runtime detects this via `isBrowserRuntime()` and refuses to initialize. Driver modules like [`pointer.ts`](https://github.com/citrolabs/ego-lite/blob/main/pointer.ts) also check this flag, returning false for feature availability checks when running outside the Ego‑Lite host.

### Can I use `ego.sendCDPMessage` directly without the wrapper?

Technically yes, but you lose **timeout protection**, **session auto‑recovery**, **structured error handling**, and **event buffering**. The `rawCdp()` wrapper in [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts) adds approximately 50 lines of safety logic that raw usage bypasses.

### How does session recovery work when a tab crashes?

When a command returns an error matching `SESSION_LOST`, `ensureSession()` (lines 85‑103) catches this, re‑attaches to the target via `Target.attachToTarget`, updates the stored session ID in [`state.ts`](https://github.com/citrolabs/ego-lite/blob/main/state.ts), and retries the original command with the same parameters. This is transparent to calling code.

### What's the difference between `browserCdp` and `evaluate`?

`**browserCdp**` sends raw CDP commands and returns the full protocol response. `**evaluate**` is a convenience wrapper that uses `Runtime.evaluate` under the hood, handles script execution context automatically, and returns the evaluated result directly rather than the CDP envelope.