Core Components of the ego-lite Browser Runtime: CDP Transport, Session Management, and Element Resolution
The ego-lite browser runtime is built around nine cohesive TypeScript modules—centered on browser-runtime.ts, state.ts, and element-resolver.ts—that provide Chrome DevTools Protocol (CDP) transport, mutable session state, stable DOM reference mapping, and high-level automation drivers.
The citrolabs/ego-lite repository delivers a compact browser automation layer designed to interact with CDP-compatible browsers. The ego-lite browser runtime abstracts the complexity of Chrome DevTools Protocol messaging into a deterministic API that handles session lifecycle, event buffering, and element resolution. Understanding these core components is essential for extending the runtime or debugging automation scripts.
Runtime State and Session Management
The foundation of the ego-lite browser runtime rests on two critical modules that manage mutable state and CDP session lifecycles.
src/state.ts exports a mutable singleton that tracks the current CDP session, configuration overrides, timing helpers, and workspace information. This global state object serves as the source of truth for the entire runtime, ensuring that drivers and resolvers share consistent configuration across async boundaries.
src/browser-runtime.ts implements the core session management logic through the ensureSession() function. This utility lazily attaches to the active browser tab, caches the session ID for SESSION_TTL_MS (2000 milliseconds), and automatically re-attaches when the session is lost. The runtime detects session loss via the SESSION_LOST regex pattern and triggers transparent reconnection without throwing errors to the caller. This mechanism ensures that long-running automation scripts survive tab crashes or navigation events that would otherwise terminate the CDP connection.
CDP Transport and Message Routing
All communication with the browser flows through the transport layer in browser-runtime.ts, which wraps globalThis.ego.sendCDPMessage and implements request-response correlation.
rawCdp() sends raw CDP commands and stores pending promises keyed by message ID. Each request times out after RESPONSE_TIMEOUT_MS (15000 milliseconds) if no response arrives. Incoming CDP messages route through handleMessage(), which resolves pending promises, buffers unrelated events for later consumption, and forwards notifications to subscribed listeners.
browserCdp() provides the primary interface for higher-level modules. It automatically injects the active session ID (obtained via ensureSession()) into every request, eliminating the need for drivers to manage session state manually. When the session expires or disconnects, browserCdp() triggers ensureSession() to re-establish the connection before retrying the command.
Event Handling and Dialog Tracking
The runtime implements a pub-sub event system atop the CDP transport to handle asynchronous browser events.
subscribeBrowserEvent() registers listeners for specific CDP methods such as Network.responseReceived or Page.javascriptDialogOpening. The system buffers the most recent 100 events in a circular buffer, allowing late subscribers to drain historical events via drainBrowserEvents().
waitForBrowserEvent() offers a promise-based interface that awaits a specific event matching a predicate function with a configurable timeout. This is particularly useful for waiting on network responses or DOM mutations.
JavaScript dialogs are tracked separately in a pendingDialogs Map. When Page.javascriptDialogOpening fires, the runtime stores the dialog metadata; it clears the entry on Page.javascriptDialogClosed. Helpers can query pendingDialog() to check for active modal dialogs before attempting interactions that would otherwise hang.
Element Resolution and Reference Mapping
Stable element identification is handled by the reference mapping and resolution modules, which decouple selectors from volatile DOM nodes.
src/ref-map.ts and src/ref-state.ts maintain the mapping between snapshot references (denoted as @N in selectors) and their underlying Accessibility (AX) backend node IDs. When a snapshot is captured, browserSnapshotRefsToRefMap() converts backend node IDs into a RefMap instance. This enables the runtime to reference specific elements using stable @N identifiers that survive DOM re-renders and attribute changes.
src/element-resolver.ts provides the high-level resolution API through resolveElementCenter() and resolveElementObjectId(). These functions accept multiple locator types:
- Role-based:
role:button[name="Submit"] - CSS selectors:
#id,.class - XPath:
//button[@id='submit'] - Text-based:
text=Click me - Href:
href=/path/to/page - Snapshot refs:
@0,@1
The resolver classifies errors as transient (retryable network or timing issues) or permanent (invalid selectors), allowing drivers to implement appropriate retry logic.
Driver Layer and Public API
The concrete automation primitives live in src/driver/, with thin wrappers implementing navigation (nav.ts), pointer actions (pointer.ts), keyboard input (keyboard.ts), and observation (observe.ts). Each driver calls browserCdp() or the low-level rawCdp() to execute CDP commands such as Input.dispatchMouseEvent, Page.navigate, or DOM.getBoxModel.
src/cdp-eval.ts provides the cdp() and js() helpers for evaluating JavaScript within the page context. These functions automatically wrap top-level return statements to ensure expression results are captured correctly.
src/helpers.ts curates the public API surface exposed to automation scripts. The helperContext() function aggregates all runtime services—browserRuntime, state, cdp-eval, and drivers—into a single context object. src/run.ts serves as the CLI entry point, reading scripts from stdin and executing them inside an async wrapper with the helper context pre-loaded.
Practical Usage Examples
Sending Raw CDP Commands
Use browserCdp() to send manual CDP commands for metrics or debugging:
import { browserCdp } from "./browser-runtime.js";
async function getViewportDimensions() {
const result = await browserCdp("Page.getLayoutMetrics");
console.log("Viewport:", result.result?.clientWidth, result.result?.clientHeight);
}
Ensuring Session Before Driver Calls
Explicitly ensure a session exists before invoking driver actions:
import { ensureSession } from "./browser-runtime.js";
import { click } from "./driver/pointer.js";
async function clickButton(selector: string) {
const sessionId = await ensureSession();
await click(sessionId, selector);
}
Resolving Elements by Role Locator
Combine element resolution with pointer actions for precise interactions:
import { resolveElementCenter } from "./element-resolver.js";
import { clickAt } from "./driver/pointer.js";
async function clickRoleButton(role: string, name?: string) {
const { x, y, sessionId } = await resolveElementCenter(
globalThis.ego,
undefined,
new Map(),
`role:${role}[name=${name}]`
);
await clickAt(sessionId, x, y);
}
Waiting for Network Events
Listen for specific CDP events to synchronize with page loads or API calls:
import { waitForBrowserEvent } from "./browser-runtime.js";
async function waitForJson(url: string) {
const event = await waitForBrowserEvent(
(e) => e.method === "Network.responseReceived" && e.params?.response?.url === url,
10_000
);
console.log("Got response:", event.params.response);
}
Summary
The ego-lite browser runtime architecture consists of nine interconnected modules that provide deterministic browser automation:
state.tsmanages global mutable state and workspace configuration.browser-runtime.tshandles CDP transport, session lifecycle (ensureSession), and event buffering with automatic reconnection onSESSION_LOST.ref-map.tsandref-state.tstranslate DOM snapshots into stable@Nreferences using AX backend node IDs.element-resolver.tsconverts role-based, CSS, XPath, and text locators into concrete screen coordinates or object IDs.driver/contains thin wrappers that translate high-level actions (click, type, navigate) into CDP commands.cdp-eval.tsfacilitates JavaScript execution in the page context.helpers.tsandrun.tsexpose the curated public API and CLI entry point for script execution.
Together, these components create a resilient automation layer that abstracts CDP complexity while maintaining full access to browser internals.
Frequently Asked Questions
How does the ego-lite browser runtime handle lost CDP sessions?
The runtime detects session loss via the SESSION_LOST regex pattern in browser-runtime.ts. When a command fails with a session error, ensureSession() automatically re-attaches to the active tab, caches the new session ID for SESSION_TTL_MS (2 seconds), and retries the original command transparently without throwing to the caller.
What selector strategies does the element resolver support?
According to src/element-resolver.ts, the resolver supports role-based locators (role:button[name=Submit]), CSS selectors (#id, .class), XPath expressions (//button), text-based matching (text=Click me), href matching (href=/path), and stable snapshot references (@0, @1). The parseLocator function determines the strategy based on the selector prefix.
How does the runtime maintain stable references to DOM elements?
The runtime uses RefMap instances created from AX backend node IDs in src/ref-map.ts. When a snapshot is taken via browserSnapshotRefsToRefMap(), each DOM node receives a numeric reference (e.g., @0) mapped to its backend ID. Because AX node IDs persist across DOM mutations better than JavaScript object references, these @N refs provide stable identifiers for element resolution even when attributes or classes change.
What is the difference between browserCdp() and rawCdp()?
rawCdp() in src/browser-runtime.ts sends low-level CDP messages via globalThis.ego.sendCDPMessage and manages the promise resolution map directly. browserCdp() is the higher-level wrapper that automatically calls ensureSession() to inject the current session ID into requests, making it the preferred interface for drivers that need session resilience.
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 →