# How Ego-Lite Handles Data Flow Across Short-Lived Processes: A Deep Dive

> Explore how Ego-Lite manages data flow across short-lived processes. Learn about its unique state persistence, ref maps, and isolation techniques for efficient agent execution.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: deep-dive
- Published: 2026-08-28

---

**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`](https://github.com/citrolabs/ego-lite/blob/main/src/run.ts), [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts), [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts), and [`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/src/run.ts). The CLI receives JavaScript via STDIN and immediately hands it to `runMain()`.

```typescript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) assembles the complete public API:

```typescript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/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

```typescript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) module handles this transparently:

```typescript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts) module implements a global singleton that tracks:

```typescript
// 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

```js
// 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

```js
// 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

```js
// 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`](https://github.com/citrolabs/ego-lite/blob/main/driver/nav.ts) | Page loads, history, frame management |
| Pointer | [`driver/pointer.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/pointer.ts) | Mouse movements, clicks, scrolls |
| Keyboard | [`driver/keyboard.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/keyboard.ts) | Key presses, combinations, text input |
| Uploads | [`driver/uploads.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/uploads.ts) | File selection and transfer |
| Screencast | [`driver/screencast.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/screencast.ts) | Frame capture and streaming |
| Waits | [`driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/src/run.ts) wraps and executes agent JavaScript with injected helpers, eliminating import overhead
- **Runtime persistence**: `BrowserRuntime` in [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) maintains CDP connection and event buffering across process boundaries
- **Snapshot-driven consistency**: [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts) generates TTL-cached numeric references with automatic regeneration on stale access
- **Intelligent resolution**: [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) classifies errors and enables appropriate retry strategies
- **Task-space isolation**: [`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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.