How CDP Messages Are Handled and Tracked in ego-lite
ego-lite correlates CDP requests and responses using a monotonic ID registry with Promise-based tracking, implements automatic session recovery with a 2-second TTL cache, and buffers non-response events in a 10,000-entry ring buffer to prevent unbounded memory growth.
The citrolabs/ego-lite project embeds a Chrome-based browser and controls it via the Chrome DevTools Protocol (CDP). Understanding how CDP messages are handled and tracked in ego-lite requires examining the runtime transport layer, session lifecycle management, and event buffering system centralized in package/ego-browser/src/browser-runtime.ts.
The Core CDP Transport Layer
At the lowest level, ego-lite implements a request-response correlation system using a monotonically increasing message counter and a pending request registry.
Message ID Generation and Pending Registry
Every outgoing CDP request receives a unique, monotonically increasing ID stored in nextMessageId. A Map called pending stores a Promise-like entry for each request, mapping this ID to resolution handlers so incoming responses can be routed back to the original caller. This mechanism lives at browser-runtime.ts:L18-L20.
Raw Message Sending and Timeouts
The rawCdp function constructs the JSON payload ({id, method, params, sessionId?}) and forwards it to the host environment via globalThis.ego.sendCDPMessage. It installs global callbacks onCDPMessage and onSendCDPMessageError to handle responses or transport failures. A timeout guard (default 15 seconds) automatically removes the pending entry and rejects the Promise if no response arrives, preventing memory leaks from dangling requests. See the implementation at browser-runtime.ts:L38-L76.
Session Management and Auto-Recovery
ego-lite maintains a resilient session layer that transparently recovers from detached or expired CDP sessions.
Automatic Session Creation
The ensureSession() function obtains or recreates a CDP session by listing tabs, selecting the active one, attaching to it via Target.attachToTarget, and enabling page events with Page.enable. The resulting session ID is cached for 2 seconds (SESSION_TTL_MS) to avoid redundant attachment operations during rapid sequential calls. This logic is found at browser-runtime.ts:L107-L143.
Session Invalidation
When a target is detached or destroyed, invalidateSession() clears the cached state to force a fresh attachment on the next request. This ensures the system does not attempt to reuse stale session IDs. See browser-runtime.ts:L146-L154.
Event Buffering and Subscription Model
Not all CDP traffic follows a request-response pattern. ego-lite handles asynchronous events through a dedicated buffering and subscription system.
The Event Ring Buffer
Incoming CDP events that are not responses to pending requests are pushed into an in-memory ring buffer named events. This buffer is capped at 10,000 entries to prevent unbounded growth during high-volume event streams like console logging or network activity. The buffer definition resides at browser-runtime.ts:L21-L23.
Consuming Browser Events
Consumers interact with buffered events through three high-level primitives:
drainBrowserEvents()– Atomically retrieves and clears the current event buffer.waitForBrowserEvent(predicate, timeout)– Registers a one-time waiter that resolves when an event matching the predicate function arrives.subscribeBrowserEvent(method, sessionId?, listener)– Registers a permanent subscriber that receives every matching event until explicitly unsubscribed.
These utilities are implemented at browser-runtime.ts:L64-L71 and browser-runtime.ts:L88-L99.
High-Level Browser API
The browserCdp wrapper function adds session handling and retry logic atop the raw transport. If a request targets a tab-specific method (not Target.* or Browser.*) and lacks a session ID, it automatically invokes ensureSession(). Upon encountering a recoverable "session lost" error, the wrapper invalidates the cached session and retries the request with a fresh session, masking transient detachments from the caller. This orchestration is found at browser-runtime.ts:L79-L104.
Error Handling and Dialog Tracking
Global Error Routing
If sendCDPMessage fails at the host level (e.g., the task becomes inactive), handleSendError rejects all pending requests with a unified EgoError. This prevents promises from waiting indefinitely for responses that will never arrive. The error routing logic is at browser-runtime.ts:L24-L31.
Dialog State Tracking
Special handling for JavaScript dialogs (Page.javascriptDialogOpening and Page.javascriptDialogClosed) stores the latest dialog parameters per session in a pendingDialogs Map. This enables helpers like page.dialog() to synchronously surface the current dialog state to user scripts. See the tracking implementation at browser-runtime.ts:L66-L75.
Practical Examples
// Execute JavaScript in the browser context
import { browserCdp } from "./browser-runtime.js";
async function getCurrentUrl() {
const resp = await browserCdp("Runtime.evaluate", {
expression: "window.location.href",
returnByValue: true,
});
return resp.result?.result?.value;
}
// Wait for a specific navigation event
import { waitForBrowserEvent } from "./browser-runtime.js";
async function waitForNavigation() {
const navigation = await waitForBrowserEvent(
(e) => e.method === "Page.frameNavigated"
);
console.log("Navigated to:", navigation.params?.url);
}
// Subscribe to console messages
import { subscribeBrowserEvent } from "./browser-runtime.js";
const unsubscribe = subscribeBrowserEvent(
"Runtime.consoleAPICalled",
undefined,
(event) => {
console.log("Console:", event.params.args.map((a) => a.value).join(" "));
}
);
// Later: unsubscribe() to stop listening
Summary
- Request Tracking: ego-lite uses a monotonic ID counter and a
pendingMap to correlate every CDP request with its response via Promise resolution. - Timeout Protection: All requests carry a default 15-second timeout to prevent memory leaks from orphaned promises.
- Session Resilience: The
ensureSessionandinvalidateSessioncycle provides automatic recovery from session loss with a 2-second TTL cache for performance. - Event Management: Non-response events flow into a 10,000-entry ring buffer, accessible via draining, waiting, or subscription APIs.
- Error Safety: Global error handlers reject all pending requests uniformly when the transport fails, ensuring no dangling promises remain.
Frequently Asked Questions
How does ego-lite match CDP responses to their original requests?
ego-lite assigns a monotonically increasing integer ID to every outgoing request stored in nextMessageId. A global Map named pending maps these IDs to Promise resolution handlers. When onCDPMessage receives a response containing the same ID, it looks up and resolves the corresponding entry in the pending Map, routing the result back to the original caller.
What happens when a CDP session is lost during execution?
The browserCdp wrapper detects recoverable "session lost" errors. It calls invalidateSession() to clear the stale cache, then invokes ensureSession() to create a fresh session by re-attaching to the active tab. The original request is automatically retried with the new session ID, making session recovery transparent to user scripts.
How are unsolicited CDP events handled?
Events that are not responses to pending requests are pushed into an in-memory ring buffer called events, capped at 10,000 entries. Consumers retrieve these via drainBrowserEvents() for batch processing, waitForBrowserEvent() for one-time predicates, or subscribeBrowserEvent() for persistent listeners. This buffering prevents event loss during high-frequency browser activity.
What is the default timeout for CDP requests?
The rawCdp function implements a default timeout of 15 seconds. If no response arrives within this window, the pending entry is removed from the pending Map and the associated Promise is rejected, preventing memory leaks and hanging async operations in long-running scripts.
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 →