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 eventsonSendCDPMessageError— 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():
- Lists available tabs via
ego.listTabs() - Selects the active or preferred target
- Calls
Target.attachToTargetwithflatten: 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()clearsstate.sessionId,state.sessionAt, and removes frompageEnabledSessions- 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:
- High‑level helper calls
browserCdp(method, params) - Session resolution — non‑browser‑level methods (not
Target.*orBrowser.*) triggerensureSession()to fetch or reuse a cached session - Raw send —
rawCdpconstructs the JSON payload and invokesego.sendCDPMessage - Async response —
handleMessageresolves the pending promise - 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.sendCDPMessagewith monotonic IDs, 15s timeouts, and centralized error handling inbrowser-runtime.ts - Session caching — 2‑second TTL eliminates redundant
Target.attachToTargetcalls 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
rawCdpexposes 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →