How Ego-Lite Handles Data Flow Across Short-Lived Processes: A Deep Dive
Ego-Lite uses a process-per-command architecture where each agent script runs in an isolated short-lived process, with state persistence handled through CDP-connected snapshots, TTL-based ref maps, and task-space isolation.
Ego-Lite is a lightweight browser automation runtime designed for AI agents that execute JavaScript commands in discrete bursts. Unlike long-running automation frameworks, ego-lite spins up a fresh process for every command, making data flow across short-lived processes a core architectural challenge. This article examines how the codebase—specifically the files in src/run.ts, src/browser-runtime.ts, src/ref-map.ts, and src/state.ts—solves this problem without sacrificing browser state consistency.
The Process-Per-Command Entry Point
Every short-lived process begins in src/run.ts. The CLI receives JavaScript via STDIN and immediately hands it to runMain().
// src/run.ts
export async function runMain(code: string): Promise<void>
runMain() performs three critical operations:
- Wraps user code in an async function to enable top-level
await - Injects the helper context as function parameters so the script can call
nav(),click(),query(), etc. without imports - Executes the wrapped function and returns results to STDOUT
This design means no module initialization happens inside the user process. All dependencies arrive pre-bound through the helper context, eliminating import overhead and ensuring consistent API availability.
The Helper Context: Stateless API Injection
The helperContext() factory in src/helpers.ts assembles the complete public API:
// src/helpers.ts
export function helperContext(runtime: BrowserRuntime): HelperContext
The returned context includes:
nav(url)— navigation driverpointer,keyboard— input driversquery(selector),click(target)— element interactionobserve(selector)— DOM monitoringcdp(command, params)— raw Chrome DevTools Protocol accessjs(expression)— arbitrary JavaScript evaluation
Because these helpers are pure functions that delegate to the runtime, they carry no process-local state. The actual state lives in the BrowserRuntime instance passed from the parent process, which maintains the CDP connection across process boundaries.
BrowserRuntime: The Persistent CDP Bridge
src/browser-runtime.ts implements the BrowserRuntime class that survives individual process lifecycles. It manages:
- CDP session lifecycle with automatic re-attachment on disconnection
- Event buffering (10,000 event cap) to prevent data loss during brief network interruptions
- Message routing via
ego.sendCDPMessageto the embeddedegobinary
// src/browser-runtime.ts
class BrowserRuntime {
private sessionId: string;
private eventBuffer: CDPEvent[] = [];
async sendCDPMessage(method: string, params?: object): Promise<unknown>
async reattach(): Promise<void>
}
The runtime instance is not recreated for each short-lived process. Instead, the parent process maintains a singleton runtime that child processes receive via the helper context injection. This is how ego-lite achieves persistence without persistent processes.
Snapshots and the Ref Map: Surviving Process Death
The most ingenious mechanism for data flow across short-lived processes is the snapshot and ref map system in src/ref-map.ts.
After every navigation or DOM-mutating operation, ego-lite captures a page snapshot. This snapshot builds a mapping from DOM nodes to numeric references:
| Reference Format | Example | Meaning |
|---|---|---|
| Stable ref | @21 |
Element ID in current snapshot |
| Ephemeral ref | @e21 |
Temporary element (form inputs, etc.) |
The ref map has a TTL of approximately 2 seconds. When a new short-lived process starts and attempts to use a reference like @21, the runtime checks the map:
IF ref exists in current map:
→ Use cached element
ELSE:
→ Trigger fresh snapshot
→ Rebuild ref map
→ Resolve reference against new map
This lazy snapshot regeneration guarantees that short-lived processes always operate against current DOM state without requiring explicit synchronization. The src/element-resolver.ts module handles this transparently:
// src/element-resolver.ts
export function elementResolver(
selector: string,
currentSnapshot: Snapshot
): ResolutionResult {
// Parses: loc=css:, loc=role:, xpath=, raw CSS, @N refs
// Returns: { element, transient: boolean } or error classification
}
The resolver distinguishes transient failures (element not yet rendered, retryable) from permanent failures (invalid selector, navigation changed page structure). This classification drives the wait loops in src/driver/waits.ts.
Task Spaces: Isolated Sandboxes for Agent Workflows
While snapshots handle DOM consistency, task spaces handle logical isolation. The src/state.ts module implements a global singleton that tracks:
// src/state.ts
interface PersistentState {
activeTaskSpaces: Map<string, TaskSpace>;
currentTaskSpaceId: string | null;
pendingOverrides: Map<string, unknown>;
}
Key functions for task-space management:
useOrCreateTaskSpace(id)— retrieve existing or initialize new spaceswitchTaskSpace(id)— change active contextclaimTaskSpace(id)— lock space for exclusive usecompleteTaskSpace(id, options)— cleanup with optional retention
Each task space maintains its own:
- Browser context (cookies, localStorage, sessionStorage)
- Page state (URL, history)
- CDP session bindings
When a short-lived process finishes, its in-memory snapshot and ref map are discarded. Only the task space reference persists in the global state singleton. The next process picks up the same task space or creates fresh isolation as needed.
The Complete Data Flow: A Walkthrough
Consider an agent executing two commands in sequence:
Process A (navigation and capture):
runMain()receives:await nav('https://example.com'); const ref = await query('loc=css:#login');- Helpers injected with active
BrowserRuntimesingleton nav()triggers navigation → snapshot captured → ref@21assigned to#loginquery()returns@21to agent- Process A exits; ref map discarded; task space "task-1" remains active
Process B (reuse and interaction):
runMain()receives:await click('@21');- Same
BrowserRuntimeinjected; current task space "task-1" resumed - Ref map empty (new process) → stale ref detected
- Fresh snapshot triggered → new ref map built
@21resolved against current DOM → click executed- Process B exits
This flow demonstrates how ego-lite achieves stateful behavior from stateless processes through strategic persistence at the runtime and state layers.
Code Examples in Practice
Basic navigation and element interaction
// Sent to ego-lite via STDIN
await nav('https://news.ycombinator.com');
await waitForElement('loc=css:.titleline > a');
const firstStory = await query('loc=css:.titleline > a');
await click(firstStory);
Reference survival across process boundaries
// Process 1: Obtain reference
const loginRef = await query('loc=css:#login-form');
console.log(loginRef); // "@42"
// Process 2: Reuse reference (auto-snapshot if stale)
await fill('@42', { username: 'agent', password: 'secret' });
await click('@42 >> loc=css:button[type="submit"]');
Task-space isolation for multi-workflow agents
// Initialize dedicated workspace
const researchSpace = await useOrCreateTaskSpace('research-task-001');
await switchTaskSpace(researchSpace);
// Perform work
await nav('https://scholar.google.com');
const paperRef = await query('loc=aria:PDF link');
// Complete and cleanup
await completeTaskSpace(researchSpace, { keep: false });
// Or preserve for later: { keep: true }
Driver Architecture: Capabilities as Modules
Under src/driver/, individual capabilities implement the CDP transport layer:
| Driver | File | Responsibility |
|---|---|---|
| Navigation | driver/nav.ts |
Page loads, history, frame management |
| Pointer | driver/pointer.ts |
Mouse movements, clicks, scrolls |
| Keyboard | driver/keyboard.ts |
Key presses, combinations, text input |
| Uploads | driver/uploads.ts |
File selection and transfer |
| Screencast | driver/screencast.ts |
Frame capture and streaming |
| Waits | driver/waits.ts |
Retry loops with transient/permanent error handling |
Each driver follows the pattern: validate input → execute CDP command → update snapshot/ref map if DOM changed → return result or throw classified error.
Summary
- Process-per-command:
runMain()insrc/run.tswraps and executes agent JavaScript with injected helpers, eliminating import overhead - Runtime persistence:
BrowserRuntimeinsrc/browser-runtime.tsmaintains CDP connection and event buffering across process boundaries - Snapshot-driven consistency:
src/ref-map.tsgenerates TTL-cached numeric references with automatic regeneration on stale access - Intelligent resolution:
src/element-resolver.tsclassifies errors and enables appropriate retry strategies - Task-space isolation:
src/state.tsprovides persistent sandboxing without persistent processes, ensuring cross-task data leakage prevention
Frequently Asked Questions
How does ego-lite handle references when the DOM changes between commands?
The ref map has a ~2 second TTL and is discarded with each process. When a new process uses a reference, elementResolver() detects an empty or stale map and triggers a fresh snapshot transparently. The reference is then resolved against the rebuilt map, ensuring current DOM state without agent intervention.
Can multiple agents share the same browser state simultaneously?
No. Task spaces (implemented in src/state.ts) enforce isolation through claimTaskSpace() and switchTaskSpace(). While the underlying CDP connection is shared, each task space maintains separate cookies, storage, and page contexts. Concurrent access to the same task space requires explicit coordination.
What happens if the CDP connection drops during command execution?
BrowserRuntime in src/browser-runtime.ts buffers up to 10,000 CDP events and implements automatic re-attachment. Short disconnections are masked; the agent process continues unaware. Extended outages surface as transient errors that the wait loops in driver/waits.ts can retry.
Why does ego-lite use numeric references instead of selectors for elements?
Numeric references (@21) are compact, fast to resolve, and stable within a snapshot's lifetime. They reduce selector re-evaluation overhead and enable precise element targeting even when CSS classes or attributes change. The auto-snapshot mechanism ensures these references remain practical across process boundaries despite their ephemeral nature.
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 →