# Ego-Browser CDP Transport Layer Architecture: How It Works

> Understand the ego-browser CDP transport layer architecture. Learn how it manages framing, requests, session, and error recovery for efficient communication.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: architecture
- Published: 2026-07-30

---

**Ego-browser's CDP transport layer is a thin wrapper around the host-provided `globalThis.ego.sendCDPMessage` API that handles message framing, pending request tracking, automatic session management, and resilient error recovery.**

The **ego-browser** SDK from the `citrolabs/ego-lite` repository provides a streamlined interface for controlling headless Chrome instances via the **Chrome DevTools Protocol (CDP)**. At its foundation lies a specialized transport layer that abstracts raw protocol complexity behind a robust promise-based API, managing everything from message correlation to session lifecycle.

## Core Message Handling

### Message Framing and ID Management

Every outgoing CDP request is assigned a **monotonically increasing numeric ID** via `nextMessageId` and wrapped in a JSON payload. In [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) (lines 38-53), the transport constructs messages that optionally include a `sessionId` when targeting specific pages, ensuring proper routing through the host's `globalThis.ego.sendCDPMessage` API.

### Pending Request Tracking with Promise Resolution

The transport maintains a `Map` named `pending` that stores resolver functions for each in-flight request. When the host returns a response, the `handleMessage` function matches the response `id` to its pending entry, resolving or rejecting the associated Promise (lines 39-50, 59-65, 71-77). This correlation mechanism ensures that asynchronous CDP commands return the correct results to the caller.

### Configurable Timeout Protection

To prevent hanging calls, each request 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`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts). This hard limit ensures that network stalls or unresponsive targets do not block execution indefinitely.

## Session Lifecycle Management

### Automatic Session Attachment

For page-level commands (those that do not start with `Target.` or `Browser.`), the transport ensures a session is attached to the active tab. The `ensureSession` function inspects cached state from [`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts) and, if stale, enumerates available tabs via `ego.listTabs()`, selects the preferred or most recent target, and executes `Target.attachToTarget` to obtain a fresh `sessionId` (lines 7-23, 84-103).

### Session Caching and TTL

To optimize performance, the transport caches session identifiers with a **time-to-live (TTL)** of `SESSION_TTL_MS` (2 seconds). This caching strategy balances responsiveness with freshness, automatically refreshing the session when the TTL expires or when targeting a different context.

### Transparent Recovery from Session Loss

If a CDP request fails with a "session lost" error (matching the `SESSION_LOST` regex), the transport clears the cached session from state, creates a new one via `ensureSession`, and retries the original request transparently without throwing an error to the caller (lines 96-103). This resilience pattern handles transient disconnections gracefully.

## Event Buffering and Subscription

### Bounded Circular Buffer for Events

Incoming CDP events are parsed in `handleMessage` and stored in a **bounded circular buffer** limited to `MAX_BUFFERED_EVENTS` (10,000 entries). This architectural choice prevents unbounded memory growth during long-running browser automations (lines 84-92, 164-172). When the buffer reaches capacity, older events are overwritten to maintain stable memory usage.

### Event Subscription Interface

The transport exposes `subscribeBrowserEvent` to allow user scripts to listen for specific CDP events. Subscribers receive events from the buffer in real-time, and the function returns an unsubscribe handler for cleanup. This decouples event consumption from the core message handling loop.

## Error Handling and Propagation

### Normalized Error Objects

Local send failures—such as when the host task becomes inactive—are caught by `handleSendError`, which iterates through all pending promises and rejects them with a normalized `EgoError` object defined in [`src/ego-errors.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ego-errors.ts) (lines 24-31). This ensures consistent error shapes across the SDK, regardless of where the failure originates.

## Practical Usage Examples

```javascript
// 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);

```

```javascript
// 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();

```

```javascript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) wraps `globalThis.ego.sendCDPMessage` to provide a promise-based CDP interface.
- **Message correlation** relies on a monotonic ID system and a `pending` Map to match responses with requests.
- **Session management** includes automatic attachment, 2-second TTL caching, and transparent retry logic for lost sessions.
- **Memory safety** is enforced through a 10,000-event circular buffer that prevents unbounded growth during long executions.
- **Error handling** normalizes all failures into `EgoError` objects and includes 15-second timeouts to prevent hanging operations.

## Frequently Asked Questions

### How does ego-browser correlate CDP requests with responses?

The transport assigns each outgoing message a unique numeric ID using `nextMessageId` and stores the corresponding Promise resolver in a `pending` Map. When the host returns a response containing the same `id`, the transport looks up and executes the resolver, ensuring accurate correlation even with out-of-order responses.

### What happens when a CDP session expires during execution?

When the `SESSION_TTL_MS` (2 seconds) expires or a "session lost" error occurs, the transport automatically clears the cached `sessionId` from [`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts), calls `Target.attachToTarget` to create a new session, and retries the failed request. This recovery happens transparently without requiring manual intervention from the calling code.

### How does the transport prevent memory leaks from CDP events?

Incoming events are stored in a **circular buffer** capped at `MAX_BUFFERED_EVENTS` (10,000 entries). Once the buffer reaches capacity, new events overwrite the oldest entries. This design ensures that long-running browser sessions do not consume unlimited memory while still preserving recent event history for subscribers.

### What causes the EgoError objects and how are they structured?

`EgoError` objects are created in [`src/ego-errors.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ego-errors.ts) and propagated by `handleSendError` when the host environment fails to send a message (for example, if the underlying task becomes inactive). These normalized error objects wrap the underlying failure reason, allowing consistent error handling patterns across the entire ego-browser SDK.