# How the ego-lite Snapshot System Rebuilds the RefMap: A Technical Deep Dive

> Learn how the ego-lite snapshot system rebuilds the RefMap by clearing stale entries, fetching a fresh DOM tree via CDP, and populating new mappings with auxiliary metadata. Technical deep dive into citrolabs/ego-lite.

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

---

**The ego-lite browser harness rebuilds the RefMap by clearing stale entries, fetching a fresh DOM tree via Chrome DevTools Protocol (CDP), and populating a new mapping of numeric snapshot references to backend node IDs with auxiliary metadata.**

The **ego-lite** testing framework creates semantic snapshots after every navigation or DOM-changing operation to maintain stable element references. Central to this process is the **RefMap**, a specialized structure that links human-readable numeric references like `@1` or `@23` to underlying CDP backend node IDs. Understanding how the system reconstructs this map on every snapshot is crucial for debugging element resolution failures and writing resilient browser automation scripts.

## The Three-Phase RefMap Rebuild Process

According to the citrolabs/ego-lite source code, the rebuild happens in three distinct phases coordinated by the browser runtime.

### Phase 1: Clear Stale References

Before processing a new snapshot, the runtime must invalidate previous mappings. The **RefMap** instance is completely emptied to prevent stale references from contaminating the new snapshot.

In [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts), the `clear()` method removes all stored entries:

```ts
// src/ref-map.ts
clear() {
  this.map.clear();
}

```

This method is invoked via `refState.clear()` in [`src/ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-state.ts), ensuring that any temporary refs from previous snapshots—such as `@99` from an earlier page state—are discarded before the new DOM is processed.

### Phase 2: Collect DOM Nodes via CDP

The runtime fetches the current DOM state using CDP commands. It calls `DOM.getDocument` and `DOM.describeNode` (as implemented in [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts)) to retrieve the complete node tree.

During this collection phase, the runtime walks the returned tree and assigns a fresh numeric ID to each element that possesses a stable selector. This assignment happens within an internal helper function, `populateRefMap(node, refMap)`, which recursively traverses the DOM hierarchy to identify trackable elements.

### Phase 3: Populate with Fresh Metadata

For every node discovered during the CDP walk, the runtime invokes `refMap.addWithFrame()` to store the relationship between the new numeric reference and the backend identifier. This method, defined in [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts), stores not just the `backendNodeId` but also auxiliary data including the element's **role**, **name**, optional **nth** index, and **frameId** for cross-frame support.

```ts
// src/ref-map.ts
addWithFrame(refId, backendNodeId, role, name, nth = undefined, frameId = undefined) {
  this.map.set(refId, {
    backendNodeId,
    role,
    name,
    nth,
    selector: undefined,
    frameId,
  });
}

```

This metadata enables downstream helpers like `elementResolver` (in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts)) to resolve `@ref` strings accurately against the fresh map.

## Snapshot Workflow Orchestration

The **BrowserRuntime** class coordinates these phases into a cohesive workflow. In [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts), the `takeSnapshot()` method acts as the entry point:

- **Start**: `BrowserRuntime.takeSnapshot()` initiates CDP communication to fetch the DOM tree.
- **Reset**: `refState.clear()` wipes the existing RefMap so stale refs are discarded.
- **Walk**: The internal `populateRefMap` helper assigns new `@N` IDs and populates entries via `addWithFrame`.
- **Finish**: The method returns a **Snapshot** object containing the newly-built RefMap, which agents use for subsequent operations.

Because each snapshot rebuilds the map from scratch, any temporary refs from previous states become automatically invalid. When a stale ref is encountered—such as trying to click `@23` after the DOM has changed—the runtime detects the `ElementResolutionError`, triggers a fresh snapshot, and rebuilds the RefMap before retrying the operation.

## Practical Implementation Examples

To take a snapshot and inspect the underlying RefMap structure:

```ts
// Example: taking a snapshot and using a ref
import { snapshot } from "ego-browser";

// Trigger a snapshot – the RefMap is rebuilt here
const snap = await snapshot();          // Internally clears old map + repopulates it

// The snapshot contains a map you can inspect (for debugging)
console.log(snap.refMap.get("23"));     // => { backendNodeId: 456, role: "button", … }

// Use a ref in a later helper call; the runtime will resolve it against the fresh map
await click("@23");                     // elementResolver resolves @23 → backendNodeId

```

The system also handles automatic recovery when references become stale:

```ts
// Example: automatic re‑snapshot when a stale ref is encountered
try {
  await click("@99");   // @99 was from an older snapshot
} catch (e) {
  // The error is a transient ElementResolutionError → the runtime retries:
  // 1️⃣ take a new snapshot (clears & rebuilds RefMap)  
  // 2️⃣ resolve the ref again
}

```

## Key Source Files

The RefMap rebuild process spans several core modules in the citrolabs/ego-lite repository:

- **[`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts)**: Defines the `RefMap` class with `clear()`, `addWithFrame()`, and lookup methods.
- **[`src/ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-state.ts)**: Holds the current `RefMap` instance and manages its lifecycle between snapshots.
- **[`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts)**: Coordinates CDP communication and triggers the rebuild workflow via `takeSnapshot()`.
- **[`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts)**: Resolves `@ref` strings by querying the freshly built RefMap.
- **[`src/driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/observe.ts)**: Exposes the public `snapshot()` helper that agents invoke to trigger the rebuild process.

## Summary

- **The RefMap rebuilds from scratch** on every snapshot to ensure references always point to live DOM nodes.
- **Three phases** drive the process: clearing stale entries via `refMap.clear()`, collecting fresh DOM data via CDP `DOM.getDocument`, and populating metadata via `addWithFrame()`.
- **Stale references auto-invalidate** because the map is cleared before each rebuild, forcing the runtime to take a fresh snapshot when encountering outdated `@ref` values.
- **Cross-frame support** is built into the population phase through the `frameId` parameter in `addWithFrame()`.
- **Entry points** for triggering rebuilds include `BrowserRuntime.takeSnapshot()` and the public `snapshot()` export in the observe driver.

## Frequently Asked Questions

### What triggers a RefMap rebuild in ego-lite?

A rebuild triggers automatically whenever `snapshot()` is called, which occurs after navigation events, DOM mutations, or explicit agent requests. The `BrowserRuntime.takeSnapshot()` method coordinates the process by first clearing the existing map in `refState` before collecting new CDP data.

### How does ego-lite handle stale references from previous snapshots?

Stale references become invalid immediately after a rebuild because `refMap.clear()` empties all entries before populating new ones. If an agent attempts to use an outdated `@ref` string, the `elementResolver` throws an `ElementResolutionError`, prompting the runtime to automatically take a fresh snapshot and rebuild the RefMap before retrying the operation.

### What is the relationship between @ref strings and backend node IDs?

The `@ref` string (e.g., `@23`) acts as a human-readable alias for a numeric `refId` stored in the RefMap. This `refId` maps to a `backendNodeId`—the actual identifier used by the Chrome DevTools Protocol to interact with specific DOM nodes. The RefMap maintains this indirection layer to provide stable references despite DOM changes.

### Can I manually inspect the RefMap during test execution?

Yes. When you call `await snapshot()`, the returned Snapshot object exposes the `refMap` property, which you can query using `snap.refMap.get("23")` to inspect the stored metadata including `backendNodeId`, `role`, `name`, and `frameId` for debugging purposes.