# Ego‑Browser CDP Transport and Session Caching Architecture Explained

> Explore the ego-browser CDP transport architecture, featuring a lightweight request-response system and short-lived session caching. Discover how it optimizes CDP message handling and reduces redundant calls.

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

---

**The ego‑browser CDP transport layer uses a lightweight request‑response system built on `ego.sendCDPMessage` with monotonic message IDs, automatic timeout handling, and short‑lived session caching (2‑second TTL) to eliminate redundant `Target.attachToTarget` calls.**

The **ego‑browser** package in the [citrolabs/ego‑lite](https://github.com/citrolabs/ego-lite) repository provides a streamlined interface to the Chrome DevTools Protocol (CDP) inside embedded Chrome environments. This article breaks down how the CDP transport layer manages raw message exchange and how session caching optimizes performance for page‑level interactions.

---

## CDP Transport Layer Design

All CDP communication flows through **[`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts)**, which implements a complete request‑response lifecycle without external dependencies.

### Message ID Generation and Payload Construction

The transport uses a simple monotonic counter to correlate responses with requests.

```javascript
let nextMessageId = 1;

```

Each outgoing message receives a unique `id`, then gets stringified with `method`, optional `params`, and an optional `sessionId`:

```javascript
const payload = JSON.stringify({
  id,
  method,
  params,
  ...(sessionId ? { sessionId } : {}),
});

```

This pattern appears at lines 48‑53 of [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts).

### Sending and Callback Registration

The payload is handed to the native bridge:

```javascript
runtime.sendCDPMessage(payload);

```

Before any sends occur, the runtime registers two critical callbacks:
- `onCDPMessage` — handles incoming responses and events
- `onSendCDPMessageError` — handles transport‑level failures

The send operation itself is synchronous; responses arrive asynchronously through the registered handler.

### Timeout and Error Handling

Every request carries a **15‑second timeout** (`RESPONSE_TIMEOUT_MS = 15000`). If no response arrives, the pending promise rejects and the entry is cleaned from the internal map:

```javascript
const timer = setTimeout(() => {
  // cleanup and reject
  reject(new Error(`CDP request timed out: ${method}`));
}, timeoutMs);

```

Send‑side failures trigger a blanket rejection of all pending requests with `EGO_CDP_SEND_FAILED`, implemented in `handleSendError` (lines 24‑30).

### Response Processing

Incoming messages parse in `handleMessage` (lines 39‑50). When `data.id` matches a pending request, the promise resolves with the full CDP response envelope. CDP‑level errors (present in `data.error`) propagate as rejections.

---

## Session Caching Architecture

Ego‑browser operates on **page sessions** rather than the browser‑level target. Session caching eliminates the overhead of repeated `Target.attachToTarget` calls across sequential helper invocations.

### Cache TTL and Validity

Sessions remain valid for **2 seconds** (`SESSION_TTL_MS = 2000`):

```javascript
const SESSION_TTL_MS = 2000;

```

The `ensureSession()` function checks freshness before creating a new attachment:

```javascript
if (state.sessionId && Date.now() - state.sessionAt < SESSION_TTL_MS) {
  return state.sessionId;
}

```

### Concurrent Request Deduplication

While a session creation is in flight, subsequent callers await the same promise via `state.sessionInflight`:

```javascript
if (state.sessionInflight) {
  return state.sessionInflight;
}

```

This prevents thundering‑herd problems when multiple helpers trigger simultaneously.

### Session Creation Flow

When the cache misses or expires, `ensureSession()`:
1. Lists available tabs via `ego.listTabs()`
2. Selects the active or preferred target
3. Calls `Target.attachToTarget` with `flatten: true`

```javascript
const attached = await rawCdp("Target.attachToTarget", {
  targetId,
  flatten: true,
}, undefined);

```

After attachment, `Page.enable` buffers events for that session (tracked in `pageEnabledSessions`).

### Automatic Invalidation and Retry

The transport detects lost sessions through a `SESSION_LOST` regex match. On detection:
- `invalidateSession()` clears `state.sessionId`, `state.sessionAt`, and removes from `pageEnabledSessions`
- The caller retries once with a fresh session

Explicit invalidation also discards pending dialogs and resets all state fields (lines 46‑53).

---

## Complete Interaction Flow

Here is how a typical helper call traverses the architecture:

1. **High‑level helper** calls `browserCdp(method, params)`
2. **Session resolution** — non‑browser‑level methods (not `Target.*` or `Browser.*`) trigger `ensureSession()` to fetch or reuse a cached session
3. **Raw send** — `rawCdp` constructs the JSON payload and invokes `ego.sendCDPMessage`
4. **Async response** — `handleMessage` resolves the pending promise
5. **Retry on failure** — lost sessions invalidate the cache and trigger one automatic retry

This design lets developers write concise code without manual session management:

```javascript
// Fetch page title through cached session
const result = await cdp('Runtime.evaluate', {
  expression: 'document.title',
});
console.log('Page title:', result.result.value);

// Force session refresh when switching contexts
await cdp('Network.enable');           // uses cached session
await ensureSession();                 // invalidates and recreates
await cdp('Network.enable');           // uses fresh session

// Access full response envelope when needed
const raw = await rawCdp('Page.navigate', {
  url: 'https://example.com',
});
console.log('Request ID:', raw.id);

```

All exported functions (`cdp`, `rawCdp`, `ensureSession`) are injected into agent scripts via `helperContext()` in **[`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts)**.

---

## Key Source Files

| File | Responsibility |
|------|--------------|
| [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) | Core CDP transport, message correlation, timeout handling, session caching, event buffering |
| [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) | Public API surface, context injection for agent scripts |
| [`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts) | Mutable runtime state (`sessionId`, `sessionAt`, `sessionInflight`) |
| [`src/ego-errors.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ego-errors.ts) | Custom error definitions including `EGO_CDP_SEND_FAILED` |

---

## Summary

- **Transport layer** — built on `ego.sendCDPMessage` with monotonic IDs, 15s timeouts, and centralized error handling in [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts)
- **Session caching** — 2‑second TTL eliminates redundant `Target.attachToTarget` calls while concurrent deduplication prevents duplicate work
- **Automatic recovery** — lost session detection triggers cache invalidation and transparent retry
- **Developer experience** — high‑level helpers hide complexity; low‑level `rawCdp` exposes full CDP envelopes when needed

---

## Frequently Asked Questions

### How does ego‑browser match CDP responses to requests?

The transport maintains a monotonically increasing `nextMessageId` counter starting at 1. Every outgoing request embeds its `id` in the JSON payload, and incoming responses are routed to the matching pending promise via this ID. This correlation happens in `handleMessage` within [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts).

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

A **15‑second timer** (`RESPONSE_TIMEOUT_MS`) rejects the promise with `CDP request timed out: ${method}` and cleans the pending entry from the internal map. The error propagates to the caller without automatic retry—timeouts indicate deeper runtime issues rather than transient failures.

### Why cache sessions for only 2 seconds?

The **2‑second TTL** (`SESSION_TTL_MS = 2000`) balances performance against correctness. Longer caches risk operating on detached or navigated-away targets; shorter caches would re‑attach too frequently. The chosen value amortizes attachment cost across typical sequential helper calls while remaining responsive to page lifecycle changes.

### Can I bypass session caching entirely?

Yes—use `rawCdp` with an explicit `sessionId` parameter or call `invalidateSession()` before your operation. `browserCdp` only invokes `ensureSession()` when no explicit session is provided and the method is not browser‑level (`Target.*` / `Browser.*`). Direct `rawCdp` calls give full control over the `sessionId` field in the CDP envelope.