# How the Browser Runtime Module Manages Chrome DevTools Protocol (CDP) Communication in ego‑lite

> Explore how ego-lite's browser runtime module expertly manages CDP communication. Discover its session-aware transport, automatic retries, and clean async API for seamless DevTools interaction.

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

---

**The browser runtime module in ego‑lite provides a robust, session‑aware CDP transport layer with automatic retry logic, event buffering, and timeout protection—abstracting raw Chrome DevTools Protocol calls into a clean async API.**

The **ego‑lite** browser automation library (see [`citrolabs/ego-lite`](https://github.com/citrolabs/ego-lite)) implements its CDP integration through a dedicated runtime module. This article examines how [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts) orchestrates protocol communication, from low‑level message transport to higher‑level session management and event subscription.

---

## Runtime Detection and Environment Setup

Before attempting any CDP communication, ego‑lite verifies it's running inside a browser host environment.

**`isBrowserRuntime()`** checks for the existence of `globalThis.ego.sendCDPMessage` ([`browser-runtime.ts#L25-L29`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L25-L29)):

```typescript
export const isBrowserRuntime = (): boolean => {
  return (
    typeof globalThis.ego !== 'undefined' &&
    typeof globalThis.ego.sendCDPMessage === 'function'
  );
};

```

This guard prevents runtime errors when the code executes outside the expected browser context.

---

## Low‑Level CDP Transport with `rawCdp()`

The **raw CDP transport** function handles the complete request‑response lifecycle.

**`rawCdp()`** ([`browser-runtime.ts#L38-L76`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L38-L76)):

- Generates a unique `requestId` using a monotonic counter
- Structures the JSON payload with `id`, `method`, and optional `params`
- Registers callbacks for success (`onCDPMessage`) and failure (`onSendCDPMessageError`)
- Sends via `ego.sendCDPMessage`
- Sets a **15‑second timeout** (``RESPONSE_TIMEOUT_MS = 15000``) that rejects pending requests if no response arrives ([`browser-runtime.ts#L55-L58`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L55-L58))

```typescript
// Sending a raw CDP command
const result = await rawCdp(
  'Runtime.evaluate',
  { expression: 'document.title', returnByValue: true },
  'my-session-id'  // optional session ID
);

```

The pending‑request map stores promise resolvers/rejectors keyed by `requestId`, enabling asynchronous response correlation.

---

## Session Management with `ensureSession()`

CDP requires a **target session** for most operations. The runtime automates session lifecycle to hide this complexity.

**`ensureSession()`** ([`browser-runtime.ts#L7-L37`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L7-L37)) performs:

1. **Tab enumeration** via `ego.listTabs()`
2. **Target selection** (preferred tab or active fallback)
3. **Session attachment** via `Target.attachToTarget`
4. **Domain enablement** (e.g., `Page.enable` for page events)

Sessions are **cached for 2 seconds** (`SESSION_TTL_MS = 2000`) to avoid redundant attachment overhead ([`browser-runtime.ts#L8-L14`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L8-L14)). An "in‑flight" guard prevents duplicate concurrent session creation attempts.

```typescript
// Ensure a session exists (creates or reuses cached)
const sessionId = await ensureSession();

// Use the session for subsequent calls
await rawCdp('Page.reload', { ignoreCache: false }, sessionId);

```

---

## Session Recovery and Retry Logic

When the underlying browser target detaches (e.g., page navigation), CDP returns session‑lost errors. The runtime **auto‑detects and recovers**.

If `rawCdp()` encounters a `SESSION_LOST` error pattern ([`browser-runtime.ts#L96-L103`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L96-L103)):

1. The stale session is invalidated
2. The request is automatically retried with a fresh session

This transparent recovery eliminates manual session handling for most use cases.

---

## Public API: `browserCdp()`

**`browserCdp()`** serves as the primary entry point for CDP communication throughout ego‑lite ([`browser-runtime.ts#L79-L89`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L79-L89)).

Key behaviors:

- **Automatic session injection**: Adds the current session ID unless the method is `Browser.*` or `Target.*` (top‑level browser/target calls)
- **Test override support**: Respects `state.cdpOverride` for mocking in test environments
- **Delegates to `rawCdp`** for actual transport

```typescript
// Navigate to a URL (session injected automatically)
await browserCdp('Page.navigate', { url: 'https://example.com' });

// Evaluate JavaScript in the page context
const { result } = await browserCdp('Runtime.evaluate', {
  expression: '1 + 1',
  returnByValue: true
});
console.log(result.value); // 2

```

---

## Event Buffering and Subscription

CDP is **bidirectional**: the browser pushes events (`Page.loadEventFired`, `Runtime.consoleAPICalled`, etc.) without prior requests. The runtime provides mechanisms to consume these.

### Event Buffer

Incoming events are stored in a bounded array (`events`) with a **10,000‑entry maximum** (`MAX_BUFFERED_EVENTS`) to prevent memory exhaustion ([`browser-runtime.ts#L6-L8`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L6-L8), [`browser-runtime.ts#L86-L90`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L86-L90)). Old events are evicted when the limit is reached.

### `subscribeBrowserEvent()`

Create persistent subscriptions for specific CDP methods ([`browser-runtime.ts#L88-L95`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L88-L95)):

```typescript
// Subscribe to all console messages
const unsubscribe = subscribeBrowserEvent(
  'Runtime.consoleAPICalled',
  undefined,           // any session
  (event) => {
    const { type, args } = event.params;
    console.log(`[${type}]`, args.map(a => a.value).join(' '));
  }
);

// Later: clean up
unsubscribe();

```

### `waitForBrowserEvent()`

Wait for a **one‑off event** matching a predicate, with timeout protection ([`browser-runtime.ts#L69-L85`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L69-L85)):

```typescript
// Wait for page load completion
const loadEvent = await waitForBrowserEvent(
  (e) => e.method === 'Page.loadEventFired' && e.params.sessionId === sessionId,
  30000  // 30 second timeout
);
console.log('Loaded at:', loadEvent.params.timestamp);

```

---

## Dialog Tracking

JavaScript dialogs (`alert`, `confirm`, `prompt`) block page execution. The runtime tracks these via dedicated event handlers.

**Dialog state** is maintained in `pendingDialogs` per session, updated on `Page.javascriptDialogOpening` and `Page.javascriptDialogClosed` events ([`browser-runtime.ts#L66-L75`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L66-L75)).

```typescript
// Navigate to a page that triggers an alert
await browserCdp('Page.navigate', { url: 'https://example.com/dialog' });

// Check for pending dialog
const dialog = pendingDialog();
if (dialog) {
  console.log('Dialog type:', dialog.type); // 'alert' | 'confirm' | 'prompt'
  console.log('Message:', dialog.message);

  // Accept or dismiss
  await browserCdp('Page.handleJavaScriptDialog', {
    accept: true,
    promptText: 'user input here'  // for prompts
  });
}

```

---

## Error Handling and Cleanup

Transport failures (host process termination, network issues) are centralized through `handleSendError()` ([`browser-runtime.ts#L22-L30`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L22-L30)), which:

- Rejects all pending requests with a descriptive `EgoError`
- Clears the pending‑request map
- Triggers any registered error callbacks

This ensures no hanging promises when the underlying connection fails.

---

## Complete Working Example

```typescript
import {
  isBrowserRuntime,
  ensureSession,
  browserCdp,
  subscribeBrowserEvent,
  waitForBrowserEvent,
  pendingDialog
} from './browser-runtime';

async function automationExample() {
  // 1. Verify environment
  if (!isBrowserRuntime()) {
    throw new Error('Not running in ego browser host');
  }

  // 2. Establish session (cached automatically)
  const sessionId = await ensureSession();

  // 3. Subscribe to console events
  const unsubConsole = subscribeBrowserEvent(
    'Runtime.consoleAPICalled',
    sessionId,
    (e) => console.log('Console:', e.params.args)
  );

  // 4. Navigate and wait for load
  await browserCdp('Page.navigate', { url: 'https://example.com' });
  await waitForBrowserEvent(
    (e) => e.method === 'Page.loadEventFired',
    30000
  );

  // 5. Evaluate script
  const { result } = await browserCdp('Runtime.evaluate', {
    expression: 'document.title',
    returnByValue: true
  });
  console.log('Page title:', result.value);

  // 6. Check and handle any dialog
  const dialog = pendingDialog();
  if (dialog) {
    await browserCdp('Page.handleJavaScriptDialog', { accept: true });
  }

  // 7. Cleanup
  unsubConsole();
}

automationExample().catch(console.error);

```

---

## Key Source Files

| File | Responsibility | Link |
|------|---------------|------|
| [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts) | Core CDP transport, session management, event buffering, public API (`browserCdp`, `ensureSession`, etc.) | [View source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts) |
| [`state.ts`](https://github.com/citrolabs/ego-lite/blob/main/state.ts) | Mutable runtime state: current session ID, preferred target, test overrides | [View source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/state.ts) |
| [`cdp-eval.ts`](https://github.com/citrolabs/ego-lite/blob/main/cdp-eval.ts) | Higher‑level wrappers (`cdp()`, `js()`) that simplify common evaluation patterns | [View source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/cdp-eval.ts) |
| [`helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/helpers.ts) | Exposes runtime utilities to user scripts through `helperContext()` | [View source](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts) |

---

## Summary

- **Detection**: `isBrowserRuntime()` verifies the ego host environment before CDP operations
- **Transport**: `rawCdp()` implements request correlation, timeouts, and error handling at the protocol level
- **Sessions**: `ensureSession()` automates target attachment with caching and transparent retry on session loss
- **Public API**: `browserCdp()` provides the clean interface used throughout the codebase, with automatic session injection
- **Events**: Bounded buffering (`MAX_BUFFERED_EVENTS = 10000`) plus `subscribeBrowserEvent()` and `waitForBrowserEvent()` enable flexible event consumption
- **Dialogs**: Automatic tracking of JavaScript dialogs with `pendingDialog()` for synchronous‑style inspection

---

## Frequently Asked Questions

### What happens if a CDP request times out?

The `rawCdp()` function enforces a **15‑second timeout** via `RESPONSE_TIMEOUT_MS`. When triggered, the pending promise rejects with a timeout error and the request is removed from the internal pending map to prevent memory leaks.

### Can I mock CDP responses for testing?

Yes. The `browserCdp()` function checks `state.cdpOverride` before proceeding to the real transport. Setting this property to a mock function intercepts all CDP calls, enabling isolated unit tests without a live browser.

### How does event subscription handle high‑volume CDP traffic?

Events are buffered in a **fixed‑size array** capped at 10,000 entries. When the limit is reached, oldest events are discarded. Subscriptions filter this buffer by method name and optional session ID, so consumers receive only relevant events without processing overhead.

### Why does session creation have a 2‑second TTL?

The **2000ms cache** (`SESSION_TTL_MS`) balances performance and correctness. CDP session creation involves multiple round‑trips (list tabs → attach → enable domains). Caching avoids this overhead for rapid successive calls, while the short TTL ensures timely recovery if the underlying target changes.