How the Ref-Map Gets Rebuilt on Snapshots to Keep @N References Valid in ego-lite

The ref-map is rebuilt after every snapshot by clearing the existing RefMap and repopulating it with fresh @N to backendNodeId mappings from the latest snapshot data, ensuring references always point to current DOM nodes.

In the citrolabs/ego-lite browser automation framework, @N references provide short-lived identifiers to DOM elements. When the page state changes, these references must remain accurate. The framework achieves this by automatically rebuilding the global ref-map each time a new snapshot is captured.

The Three-Step Ref-Map Rebuild Process

When ego-lite captures a page snapshot, it orchestrates a coordinated rebuild of the reference map through three distinct phases.

Step 1: Capture the Raw Snapshot

The process begins in src/driver/observe.ts, where the snapshotRaw() function invokes the underlying ego runtime via ego.snapshot. This returns a structured snapshot object containing a refs array. Each entry in this array maps a snapshot-generated reference number to its corresponding backendNodeId, along with metadata including role, name, and optional frameId.

Step 2: Register the Refresh Callback

Immediately after capturing the snapshot, observe.ts registers a callback using registerSnapshotForRefRefresh(() => snapshotRaw()). This callback is stored in src/state.ts and executes automatically after every successful snapshot operation.

Step 3: Clear and Repopulate the RefMap

The registered callback triggers refreshRefs(), implemented in src/ref-state.ts. This function performs the actual rebuild:

  • It calls browserRefMap.clear() to wipe the previous mappings (implemented in src/ref-map.ts as RefMap.clear())
  • It iterates over the refs array from the latest snapshot
  • For each reference, it invokes browserRefMap.addWithFrame(refId, backendNodeId, role, name, nth, frameId) to store a fresh mapping from the textual @N identifier to the current backendNodeId and frame context

Runtime Resolution and Automatic Recovery

When automation scripts invoke helpers like click("@12") or js("@5"), the system resolves these identifiers through RefMap.get(). If the map lacks the requested reference, src/element-resolver.ts automatically triggers a new snapshot. This ensures the ref-map is always current before resolving element queries.

// Taking a snapshot automatically rebuilds the ref-map
const snap = await page.snapshot();  // Contains refs like "@3", "@7"...

// Using a reference triggers lookup against the fresh map
await page.click("@3");  // Resolves via RefMap.get("@3")

Manual Ref-Map Refresh

While the system handles ref-map rebuilds automatically, you can force a manual refresh when necessary:

import { refreshRefs } from "ego-browser/src/ref-state.js";

// Forces a fresh snapshot and complete ref-map rebuild
await refreshRefs();

Summary

  • Snapshot-driven rebuilds: Every call to snapshotRaw() in src/driver/observe.ts triggers a complete ref-map refresh through the registered callback mechanism
  • Complete replacement: The RefMap is cleared entirely and rebuilt from the refs array rather than incrementally updated
  • Backend node mapping: Each @N reference maps to a backendNodeId stored with optional frame context via addWithFrame()
  • Automatic recovery: src/element-resolver.ts detects stale or missing references and initiates new snapshots automatically
  • Key files: src/ref-map.ts defines the map structure, src/ref-state.ts manages the singleton instance, and src/driver/observe.ts orchestrates the snapshot cycle

Frequently Asked Questions

What triggers a ref-map rebuild in ego-lite?

A rebuild triggers automatically after every successful snapshot. The registerSnapshotForRefRefresh() callback in src/driver/observe.ts executes refreshRefs(), which clears and repopulates the map from the latest snapshot data.

Why does the ref-map get cleared instead of updated incrementally?

The browserRefMap.clear() approach ensures consistency. Since snapshots capture the complete DOM state, incremental updates risk leaving stale entries. A full rebuild guarantees that every @N reference points to a node that actually exists in the current snapshot.

How does ego-lite handle invalid or expired @N references?

When src/element-resolver.ts encounters a reference not present in the current map, it automatically triggers a fresh snapshot. This rebuilds the ref-map with current data, allowing the reference to resolve against the new DOM state.

Can I use @N references across page navigations?

No, @N references are short-lived identifiers tied to a specific snapshot. After navigation, you must capture a new snapshot to generate valid references for the new page state.

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 →