# How CDP Messages Are Handled and Tracked in ego-lite

> Discover how ego-lite handles CDP messages with monotonic ID tracking, automatic session recovery, and a ring buffer. Learn about its efficient memory management.

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

---

**ego-lite correlates CDP requests and responses using a monotonic ID registry with Promise-based tracking, implements automatic session recovery with a 2-second TTL cache, and buffers non-response events in a 10,000-entry ring buffer to prevent unbounded memory growth.**

The citrolabs/ego-lite project embeds a Chrome-based browser and controls it via the Chrome DevTools Protocol (CDP). Understanding how CDP messages are handled and tracked in ego-lite requires examining the runtime transport layer, session lifecycle management, and event buffering system centralized in [`package/ego-browser/src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts).

## The Core CDP Transport Layer

At the lowest level, ego-lite implements a request-response correlation system using a monotonically increasing message counter and a pending request registry.

### Message ID Generation and Pending Registry

Every outgoing CDP request receives a unique, monotonically increasing ID stored in `nextMessageId`. A **`Map` called `pending`** stores a Promise-like entry for each request, mapping this ID to resolution handlers so incoming responses can be routed back to the original caller. This mechanism lives at [`browser-runtime.ts:L18-L20`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L18-L20).

### Raw Message Sending and Timeouts

The `rawCdp` function constructs the JSON payload (`{id, method, params, sessionId?}`) and forwards it to the host environment via `globalThis.ego.sendCDPMessage`. It installs global callbacks `onCDPMessage` and `onSendCDPMessageError` to handle responses or transport failures. A timeout guard (default **15 seconds**) automatically removes the pending entry and rejects the Promise if no response arrives, preventing memory leaks from dangling requests. See the implementation at [`browser-runtime.ts:L38-L76`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L38-L76).

## Session Management and Auto-Recovery

ego-lite maintains a resilient session layer that transparently recovers from detached or expired CDP sessions.

### Automatic Session Creation

The `ensureSession()` function obtains or recreates a CDP session by listing tabs, selecting the active one, attaching to it via `Target.attachToTarget`, and enabling page events with `Page.enable`. The resulting session ID is cached for **2 seconds** (`SESSION_TTL_MS`) to avoid redundant attachment operations during rapid sequential calls. This logic is found at [`browser-runtime.ts:L107-L143`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L107-L143).

### Session Invalidation

When a target is detached or destroyed, `invalidateSession()` clears the cached state to force a fresh attachment on the next request. This ensures the system does not attempt to reuse stale session IDs. See [`browser-runtime.ts:L146-L154`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L146-L154).

## Event Buffering and Subscription Model

Not all CDP traffic follows a request-response pattern. ego-lite handles asynchronous events through a dedicated buffering and subscription system.

### The Event Ring Buffer

Incoming CDP events that are **not** responses to pending requests are pushed into an in-memory ring buffer named `events`. This buffer is capped at **10,000 entries** to prevent unbounded growth during high-volume event streams like console logging or network activity. The buffer definition resides at [`browser-runtime.ts:L21-L23`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L21-L23).

### Consuming Browser Events

Consumers interact with buffered events through three high-level primitives:

- **`drainBrowserEvents()`** – Atomically retrieves and clears the current event buffer.
- **`waitForBrowserEvent(predicate, timeout)`** – Registers a one-time waiter that resolves when an event matching the predicate function arrives.
- **`subscribeBrowserEvent(method, sessionId?, listener)`** – Registers a permanent subscriber that receives every matching event until explicitly unsubscribed.

These utilities are implemented at [`browser-runtime.ts:L64-L71`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L64-L71) and [`browser-runtime.ts:L88-L99`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L88-L99).

## High-Level Browser API

The `browserCdp` wrapper function adds session handling and retry logic atop the raw transport. If a request targets a tab-specific method (not `Target.*` or `Browser.*`) and lacks a session ID, it automatically invokes `ensureSession()`. Upon encountering a recoverable "session lost" error, the wrapper invalidates the cached session and retries the request with a fresh session, masking transient detachments from the caller. This orchestration is found at [`browser-runtime.ts:L79-L104`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L79-L104).

## Error Handling and Dialog Tracking

### Global Error Routing

If `sendCDPMessage` fails at the host level (e.g., the task becomes inactive), `handleSendError` rejects **all** pending requests with a unified `EgoError`. This prevents promises from waiting indefinitely for responses that will never arrive. The error routing logic is at [`browser-runtime.ts:L24-L31`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L24-L31).

### Dialog State Tracking

Special handling for JavaScript dialogs (`Page.javascriptDialogOpening` and `Page.javascriptDialogClosed`) stores the latest dialog parameters per session in a `pendingDialogs` Map. This enables helpers like `page.dialog()` to synchronously surface the current dialog state to user scripts. See the tracking implementation at [`browser-runtime.ts:L66-L75`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L66-L75).

## Practical Examples

```typescript
// Execute JavaScript in the browser context
import { browserCdp } from "./browser-runtime.js";

async function getCurrentUrl() {
  const resp = await browserCdp("Runtime.evaluate", {
    expression: "window.location.href",
    returnByValue: true,
  });
  return resp.result?.result?.value;
}

```

```typescript
// Wait for a specific navigation event
import { waitForBrowserEvent } from "./browser-runtime.js";

async function waitForNavigation() {
  const navigation = await waitForBrowserEvent(
    (e) => e.method === "Page.frameNavigated"
  );
  console.log("Navigated to:", navigation.params?.url);
}

```

```typescript
// Subscribe to console messages
import { subscribeBrowserEvent } from "./browser-runtime.js";

const unsubscribe = subscribeBrowserEvent(
  "Runtime.consoleAPICalled",
  undefined,
  (event) => {
    console.log("Console:", event.params.args.map((a) => a.value).join(" "));
  }
);

// Later: unsubscribe() to stop listening

```

## Summary

- **Request Tracking**: ego-lite uses a monotonic ID counter and a `pending` Map to correlate every CDP request with its response via Promise resolution.
- **Timeout Protection**: All requests carry a default 15-second timeout to prevent memory leaks from orphaned promises.
- **Session Resilience**: The `ensureSession` and `invalidateSession` cycle provides automatic recovery from session loss with a 2-second TTL cache for performance.
- **Event Management**: Non-response events flow into a 10,000-entry ring buffer, accessible via draining, waiting, or subscription APIs.
- **Error Safety**: Global error handlers reject all pending requests uniformly when the transport fails, ensuring no dangling promises remain.

## Frequently Asked Questions

### How does ego-lite match CDP responses to their original requests?

ego-lite assigns a monotonically increasing integer ID to every outgoing request stored in `nextMessageId`. A global `Map` named `pending` maps these IDs to Promise resolution handlers. When `onCDPMessage` receives a response containing the same ID, it looks up and resolves the corresponding entry in the `pending` Map, routing the result back to the original caller.

### What happens when a CDP session is lost during execution?

The `browserCdp` wrapper detects recoverable "session lost" errors. It calls `invalidateSession()` to clear the stale cache, then invokes `ensureSession()` to create a fresh session by re-attaching to the active tab. The original request is automatically retried with the new session ID, making session recovery transparent to user scripts.

### How are unsolicited CDP events handled?

Events that are not responses to pending requests are pushed into an in-memory ring buffer called `events`, capped at 10,000 entries. Consumers retrieve these via `drainBrowserEvents()` for batch processing, `waitForBrowserEvent()` for one-time predicates, or `subscribeBrowserEvent()` for persistent listeners. This buffering prevents event loss during high-frequency browser activity.

### What is the default timeout for CDP requests?

The `rawCdp` function implements a default timeout of **15 seconds**. If no response arrives within this window, the pending entry is removed from the `pending` Map and the associated Promise is rejected, preventing memory leaks and hanging async operations in long-running scripts.