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:

  1. Wraps user code in an async function to enable top-level await
  2. Injects the helper context as function parameters so the script can call nav(), click(), query(), etc. without imports
  3. 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 driver
  • pointer, keyboard — input drivers
  • query(selector), click(target) — element interaction
  • observe(selector) — DOM monitoring
  • cdp(command, params) — raw Chrome DevTools Protocol access
  • js(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.sendCDPMessage to the embedded ego binary
// 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 space
  • switchTaskSpace(id) — change active context
  • claimTaskSpace(id) — lock space for exclusive use
  • completeTaskSpace(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):

  1. runMain() receives: await nav('https://example.com'); const ref = await query('loc=css:#login');
  2. Helpers injected with active BrowserRuntime singleton
  3. nav() triggers navigation → snapshot captured → ref @21 assigned to #login
  4. query() returns @21 to agent
  5. Process A exits; ref map discarded; task space "task-1" remains active

Process B (reuse and interaction):

  1. runMain() receives: await click('@21');
  2. Same BrowserRuntime injected; current task space "task-1" resumed
  3. Ref map empty (new process) → stale ref detected
  4. Fresh snapshot triggered → new ref map built
  5. @21 resolved against current DOM → click executed
  6. 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() in src/run.ts wraps and executes agent JavaScript with injected helpers, eliminating import overhead
  • Runtime persistence: BrowserRuntime in src/browser-runtime.ts maintains CDP connection and event buffering across process boundaries
  • Snapshot-driven consistency: src/ref-map.ts generates TTL-cached numeric references with automatic regeneration on stale access
  • Intelligent resolution: src/element-resolver.ts classifies errors and enables appropriate retry strategies
  • Task-space isolation: src/state.ts provides 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →