Ego‑Browser CDP Transport and Session Caching Architecture Explained

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 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, 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.

let nextMessageId = 1;

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

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

This pattern appears at lines 48‑53 of browser-runtime.ts.

Sending and Callback Registration

The payload is handed to the native bridge:

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:

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):

const SESSION_TTL_MS = 2000;

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

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:

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
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 sendrawCdp constructs the JSON payload and invokes ego.sendCDPMessage
  4. Async responsehandleMessage 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:

// 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.


Key Source Files

File Responsibility
src/browser-runtime.ts Core CDP transport, message correlation, timeout handling, session caching, event buffering
src/helpers.ts Public API surface, context injection for agent scripts
src/state.ts Mutable runtime state (sessionId, sessionAt, sessionInflight)
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
  • 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.

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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →