# How Ego-Lite Manages CDP Transport and Session Caching

> Discover how ego-lite efficiently manages CDP transport and session caching. Learn about its auto-reconnecting CDP transport, JSON-RPC message bundling, and smart session caching for seamless retries and performance.

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

---

**TLDR:** Ego-Lite implements a lightweight, auto-reconnecting CDP transport in [`package/ego-browser/src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts) that bundles JSON-RPC messages via `globalThis.ego.sendCDPMessage`, caches session IDs for 2 seconds to avoid re-attaching, and transparently retries failed calls after invalidating the cached session.

The [citrolabs/ego-lite](https://github.com/citrolabs/ego-lite) project is a browser automation library built on the Chrome DevTools Protocol (CDP). Under the hood, it manages the CDP transport and session caching with a clean, minimal design that prioritizes reliability and low memory overhead. In this guide, you'll learn exactly how Ego-Lite's CDP transport works, how it caches sessions, and what happens when a session is lost.

## Where the CDP Transport Lives in Ego-Lite

The entire CDP layer is concentrated in a single file: [`package/ego-browser/src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts). This runtime communicates with the embedded browser solely through the global `ego` object, specifically calling `globalThis.ego.sendCDPMessage`.

The architecture intentionally separates concerns:

- **Transport** – sending raw CDP messages and handling responses.
- **Session management** – caching and re-validating target-attached sessions.
- **Event buffering** – capping page events to prevent memory leaks.

Let's walk through each component in detail.

## Raw CDP Transport: Building JSON-RPC Payloads

The most fundamental function is `rawCdp()`. It constructs a JSON-RPC payload, assigns a unique request ID, stores a resolver in a `pending` map, and sends the message via `ego.sendCDPMessage`.

```ts
// https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L38-L76
function rawCdp(method, params = {}, sessionId = undefined, timeoutMs = RESPONSE_TIMEOUT_MS) {
  // Build payload
  // Assign unique id
  // Register resolver in pending map
  // Send via ego.sendCDPMessage
}

```

The key detail: a **15-second timeout** (`RESPONSE_TIMEOUT_MS`) automatically cleans up the `pending` map when no response arrives. Responses are matched back to the correct resolver by `runtime.onCDPMessage` (the `handleMessage` implementation) later in the same file.

## Session-Aware Calls with `browserCdp`

Every high-level helper (like `cdp()` and `js()`) ultimately calls **`browserCdp()`**. This public entry point adds session-aware logic on top of `rawCdp`:

```ts
// https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L79-L105
export async function browserCdp(method, params = {}, sessionId = undefined, timeoutMs = RESPONSE_TIMEOUT_MS) {
  // 1. Check for test-only cdpOverride
  // 2. If sessionId is undefined and method is not a top-level Browser/Target command,
  //    call ensureSession() to reuse a cached session
  // 3. Forward to rawCdp()
}

```

The function checks for a test-only `cdpOverride` first. If you don't supply a session ID and the method isn't a top-level `Browser.*` or `Target.*` command, it calls `ensureSession()` to obtain a valid session. This avoids the overhead of manually managing session IDs for every call.

## Session Caching: The 2-Second TTL

Session caching is implemented in **`ensureSession()`**:

```ts
// https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L107-L144
export async function ensureSession() {
  // Check state.sessionId and state.sessionAt
  // If cached session is fresh (within SESSION_TTL_MS), return it
  // Otherwise, create one via Target.attachToTarget
}

```

The runtime caches the session in `state.sessionId` with a timestamp `state.sessionAt`. The **TTL is 2 seconds** (`SESSION_TTL_MS`). Any CDP call that happens within that window reuses the cached session, eliminating an extra `Target.attachToTarget` round-trip.

Why 2 seconds? Short enough to avoid stale sessions, but long enough to cover the typical burst of operations on the same focused target.

## Automatic Session Invalidation and Retry

Broken sessions are handled transparently. If a CDP request fails with an error matching the **`SESSION_LOST` regex** (e.g., “session not found” or “Target closed”), `browserCdp()` does the following:

1. Calls `invalidateSession()` to clear the cached ID.
2. Obtains a fresh session via `ensureSession()`.
3. Retries the original request.

```ts
// https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L96-L103

```

This auto-reconnect mechanism makes the transport robust to target detachment, page reloads, or devtools crashes, without requiring developer intervention.

## Page-Level Event Buffering and `enablePageEvents`

To support long-lived browser sessions without running into memory issues, Ego-Lite caps buffered events at **10,000** (`MAX_BUFFERED_EVENTS`). After a successful attach, the runtime enables page-level events via **`enablePageEvents()`**:

```ts
// https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L205-L215
async function enablePageEvents(sessionId) {
  // Enable Page.* and Runtime.* events
  // Cap buffered events at MAX_BUFFERED_EVENTS
}

```

This ensures events aren't stored indefinitely, preventing memory leaks during extended automation runs.

## How the Pieces Work Together

The full flow for a typical CDP command (e.g., `Network.enable`) is:

1. Call `browserCdp('Network.enable')`.
2. Ensure a session (either cached or freshly attached).
3. Send the JSON-RPC payload via `rawCdp`.
4. Wait for a response (or a timeout).
5. If the response indicates a lost session, invalidate, re-attach, and retry.

That's it — a minimal, resilient CDP transport that's easy to trust.

## Practical Code Examples

**Sending a CDP command (e.g., `Network.enable`):**

```ts
// Example used by a helper inside ego-lite
await browserCdp('Network.enable');

```

**Evaluating JavaScript in the page context:**

```ts
// src/cdp-eval.ts uses browserCdp under the hood
import { js } from './cdp-eval.js';

// Returns the page title
const title = await js('document.title');
console.log('Page title →', title);

```

**Manually forcing a new session (rarely needed):**

```ts
import { invalidateSession, ensureSession } from './browser-runtime.js';

// Invalidate the current session
invalidateSession();

// Get a fresh session id for the next CDP calls
const freshSession = await ensureSession();
console.log('New session id:', freshSession);

```

## Summary

The CDP transport and session caching architecture in Ego-Lite is compact yet robust:

- **`rawCdp()`** – low-level JSON-RPC over `ego.sendCDPMessage` with a 15-second pending-timeout.
- **`browserCdp()`** – the session-aware public entrypoint that handles retries and overrides.
- **`ensureSession()`** – caches the session ID with a 2-second TTL to avoid `Target.attachToTarget` churn.
- **`invalidateSession()` + retry** – automatically recovers when a session is lost.
- **`enablePageEvents()` + `MAX_BUFFERED_EVENTS`** – caps event buffering to prevent memory bloat.

The combination of a short-lived session cache and automatic re-attachment makes Ego-Lite ideal for fast, long-running browser automation scripts.

## Frequently Asked Questions

### How does Ego-Lite avoid extra CDP round-trips for sessions?

Ego-Lite keeps a session ID in `state.sessionId` alongside a timestamp (`state.sessionAt`). For 2 seconds (`SESSION_TTL_MS`) after the initial attach, it reuses the cached session instead of sending another `Target.attachToTarget` command.

### What happens if a CDP session dies mid-request?

If the error message matches the `SESSION_LOST` regex (e.g., “session not found”), `browserCdp()` calls `invalidateSession()` to clear the cache, then calls `ensureSession()` for a fresh session, and finally retries the original request once.

### Where is the CDP transport code located?

All logic lives in [`package/ego-browser/src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts). Supporting state definitions are in [`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts), and the public `cdp()` / `js()` helpers are in [`src/cdp-eval.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/cdp-eval.ts).

### Is there a limit to how many events Ego-Lite buffers?

Yes — the buffer is capped at `MAX_BUFFERED_EVENTS` (10,000). When this limit is reached, newer events are not buffered, preventing memory exhaustion during long sessions.