How Ego-Lite Manages CDP Transport and Session Caching
TLDR: Ego-Lite implements a lightweight, auto-reconnecting CDP transport in 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 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. 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.
// 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:
// 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():
// 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:
- Calls
invalidateSession()to clear the cached ID. - Obtains a fresh session via
ensureSession(). - Retries the original request.
// 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():
// 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:
- Call
browserCdp('Network.enable'). - Ensure a session (either cached or freshly attached).
- Send the JSON-RPC payload via
rawCdp. - Wait for a response (or a timeout).
- 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):
// Example used by a helper inside ego-lite
await browserCdp('Network.enable');
Evaluating JavaScript in the page context:
// 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):
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 overego.sendCDPMessagewith 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 avoidTarget.attachToTargetchurn.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. Supporting state definitions are in src/state.ts, and the public cdp() / js() helpers are in 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.
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 →