How Ref-Map Rebuilds on Every Snapshot and What Triggers Auto Re-snapshot in ego-lite

In ego-lite, every snapshot triggers a complete ref-map rebuild via browserSnapshotRefsToRefMap, and automatic re-snapshoting occurs when helpers receive a snapshot reference (@id) while the ref-map is empty.

The ref-map is a core data structure in ego-lite that maps snapshot reference IDs (e.g., @23) to underlying browser metadata like backendNodeId, ARIA role, and accessible name. Maintaining this mapping correctly across page changes is essential for reliable DOM interaction.

How the Ref-Map Rebuilds on Every Snapshot

When you capture a page snapshot in ego-lite, the browser runtime returns an array of ref descriptors—one for each captured element. The system then reconstructs the ref-map from scratch to ensure consistency.

The Rebuild Process in browserSnapshotRefsToRefMap

Located in src/browser-runtime.ts, this function implements a clear-and-repopulate strategy:

// src/browser-runtime.ts
export function browserSnapshotRefsToRefMap(refMap, refs = []) {
  refMap.clear();                         // <‑‑ discard stale entries
  for (const ref of refs) {
    if (!ref || typeof ref !== "object") continue;
    if (ref.backendNodeId == null) continue;
    refMap.add(
      String(ref.backendNodeId),           // ref-id (e.g., "23")
      ref.backendNodeId,
      ref.role,
      ref.name,
      undefined,
    );
  }
}

Three guarantees this provides:

  1. No stale references — previous snapshot data is discarded immediately
  2. Consistent state — the map always reflects the current DOM
  3. Validation — entries without backendNodeId are filtered out

This function is invoked internally by snapshotRaw (in src/driver/observe.ts), meaning every explicit or implicit snapshot refreshes the entire ref-map.

What Triggers Automatic Re-snapshot in ego-lite

ego-lite detects when the ref-map is stale and automatically captures a fresh snapshot before executing operations. This prevents "ghost" references to DOM nodes that no longer exist.

The ensureRefMapForRef Guard

All helpers that accept either a CSS selector or snapshot reference—elementCenter, click, type, and others—first call ensureRefMapForRef from src/ref-state.ts:

// src/ref-state.ts
export async function ensureRefMapForRef(selectorOrRef: unknown) {
  if (ensuring) return;                    // prevent concurrent execution
  if (typeof selectorOrRef !== "string") return;
  if (!parseRef(selectorOrRef)) return;    // not a @ref format
  if (browserRefMap.map.size > 0) return;  // map already populated
  if (!snapshotImpl) return;               // no impl registered

  ensuring = true;
  try {
    await snapshotImpl();                  // <-- triggers fresh snapshot
  } finally {
    ensuring = false;
  }
}

Trigger conditions (all must pass):

  • The argument is a string starting with @ (validated by parseRef)
  • The current ref-map is empty (browserRefMap.map.size === 0)
  • A snapshot implementation is registered

Snapshot Implementation Registration

The actual snapshot function is registered once during module initialization in src/driver/observe.ts:

// src/driver/observe.ts
registerSnapshotForRefRefresh(() => snapshotRaw());

This establishes snapshotRaw() as the callback that ensureRefMapForRef invokes when regeneration is needed.

Common Scenarios That Trigger Auto Re-snapshot

Scenario Why the map empties Result
Page navigation New document, old refs invalid Fresh snapshot before next @ref use
Page reload Runtime context reset Map cleared, auto-rebuild on demand
Manual refMap.clear() Explicit cleanup Next @ref operation triggers rebuild
First operation in session Map never populated Initial lazy initialization

Practical Usage Example

// Example: Click using a snapshot reference
await page.click("@15");              // If map empty, auto-snapshots first

// Example: Explicit snapshot for fresh state
const snapshot = await page.snapshotRaw();  // Rebuilds ref-map immediately
console.log(snapshot.refs.length);          // Inspect captured elements

// Example: Helper implementation pattern
import { ensureRefMapForRef } from "./ref-state";
import { resolveElementCenter } from "./element-resolver";

export async function elementCenter(selectorOrRef: string) {
  await ensureRefMapForRef(selectorOrRef);  // Auto-resnapshot guard
  return resolveElementCenter(
    { sendRaw: cdp },
    undefined,
    browserRefMap,
    selectorOrRef,
  );
}

Key Files in the Ref-Map Lifecycle

File Responsibility
src/ref-map.ts RefMap class definition and parseRef utility
src/ref-state.ts Singleton browserRefMap, ensureRefMapForRef, callback registration
src/browser-runtime.ts browserSnapshotRefsToRefMap — the rebuild implementation
src/driver/observe.ts snapshotRaw, snapshot helpers, one-time callback registration
src/helpers.ts Public API (click, type, elementCenter) that depend on ref-map state

Summary

  • Every snapshot rebuilds the ref-map completely via browserSnapshotRefsToRefMap clearing and repopulating entries from fresh browser data
  • Automatic re-snapshot triggers when a helper receives a @ref string and browserRefMap.map.size === 0
  • The ensureRefMapForRef guard in src/ref-state.ts implements lazy, race-condition-safe regeneration
  • Registration happens once in src/driver/observe.ts, binding snapshotRaw() as the refresh implementation

Frequently Asked Questions

What happens if I use a stale @ref after page navigation?

The ref-map empties on navigation (browser context reset). When you next use @ref, ensureRefMapForRef detects the empty map and triggers snapshotRaw() automatically, then resolves your reference against fresh data. No manual intervention required.

Can I manually force a ref-map rebuild?

Yes—call snapshotRaw() directly. This invokes browserSnapshotRefsToRefMap with newly captured refs, immediately repopulating browserRefMap. Alternatively, page.snapshot() also refreshes the map while returning processed snapshot data.

Why does the map clear rather than update incrementally?

Clear-and-rebuild guarantees consistency. DOM changes (reorders, removals, dynamic inserts) make incremental updates error-prone. A fresh array from the browser runtime ensures the map always matches the actual document state.

Is there a risk of concurrent snapshot triggers?

No—ensureRefMapForRef uses an ensuring flag to prevent re-entrant execution. If multiple @ref operations race while the map is empty, only the first triggers snapshotImpl(); others await completion via the shared promise chain.

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 →