How Ego-Browser Rebuilds the Ref Map on `snapshotRaw()` Calls

Ego-Browser clears its Ref Map before each snapshot and repopulates it with fresh refs from the returned snapshot data, ensuring reference IDs like @12 always resolve to current backend nodes.

The snapshotRaw() method in ego-browser is the foundation of agent-driven web automation. Each call triggers a coordinated rebuild of the internal Ref Map—the data structure that maps textual reference IDs to underlying browser backend nodes. This article breaks down the exact mechanism, with reference to the source code in citrolabs/ego-lite.

The Three-Step Rebuild Process

When an agent invokes snapshotRaw(), ego-browser executes a precise sequence to maintain reference integrity:

Step 1: Clear the Existing Ref Map

Before capturing new snapshot data, the runtime empties the current map to prevent stale references from persisting. This happens through a hook registered in ref-state.ts.

In [src/ref-state.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/ref-state.ts), the registration system stores a refresh function:

// ref-state.ts
let snapshotImpl: (() => Promise<unknown>) | null = null;

export function registerSnapshotForRefRefresh(fn: () => Promise<unknown>) {
  snapshotImpl = fn;
}

export async function refreshSnapshot() {
  if (!snapshotImpl) return;
  await snapshotImpl();
}

The actual clearing happens during this refresh phase, implemented in snapshotImpl.

Step 2: Capture the Raw Snapshot

The snapshotRaw() function in [src/driver/observe.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/driver/observe.ts) delegates to the embedded ego runtime:

// driver/observe.ts – snapshotRaw implementation
export async function snapshotRaw(): Promise<{ content: string; refs: RefInfo[] }> {
  const result = await browserEgo().snapshot();
  return {
    content: result.content,
    refs: result.refs.map(r => ({
      id: r.id,
      backendNodeId: r.backendNodeId,
      role: r.role,
      name: r.name,
      nth: r.nth,
      frameId: r.frameId
    }))
  };
}

The response contains content (the serialized DOM) and refs (an array of reference metadata objects). Each ref includes id, backendNodeId, role, name, nth, and frameId.

Step 3: Repopulate the Ref Map

After the raw snapshot resolves, ego-browser iterates through result.refs and rebuilds the mapping. This occurs in the snapshotImpl implementation within ref-state.ts:

// ref-state.ts – snapshotImpl rebuilds the map
async function snapshotImpl() {
  refMap.clear();                    // Step 1: clear stale entries
  const raw = await snapshotRaw();   // Step 2: fetch fresh snapshot
  
  // Step 3: repopulate with new refs
  for (const r of raw.refs) {
    refMap.addWithFrame(
      r.id,
      r.backendNodeId,
      r.role,
      r.name,
      r.nth,
      r.frameId
    );
  }
}

The refMap.addWithFrame() method is defined in src/ref-map.ts and stores each reference with its frame context for cross-frame element resolution.

Hook Registration Architecture

The connection between the observation layer and reference state management is established at module initialization in driver/observe.ts:

// driver/observe.ts – hook installation
registerSnapshotForRefRefresh(() => snapshotRaw());

This registration pattern decouples the snapshot-taking logic from the ref management logic, allowing ref-state.ts to orchestrate the rebuild without hard-coding dependencies.

Working Example: Snapshot and Reference Resolution

Here's how the rebuild process enables reliable element interaction:

// Agent code
const page = await browser.newPage();

// Triggers full ref map rebuild
const raw = await page.snapshotRaw();
console.log(raw.refs);
// → [{ id: "12", backendNodeId: 45, role: "button", name: "Submit", nth: 0, frameId: 0 }]

// Subsequent click resolves via the fresh Ref Map
await page.click("@12");  // Maps "@12" → backendNodeId 45

Without the rebuild mechanism, stale backendNodeId values from previous snapshots would cause element resolution failures after DOM mutations.

Key Source Files

File Responsibility
src/ref-map.ts RefMap class with clear(), addWithFrame(), and lookup methods
src/ref-state.ts Hook registration (registerSnapshotForRefRefresh), snapshotImpl orchestration
src/driver/observe.ts snapshotRaw() implementation, hook installation
src/helpers.ts Exports snapshotRaw to agent-facing API

Summary

  • Ref Map clearing happens via refMap.clear() in the refresh hook before every snapshot
  • Fresh snapshot data comes from browserEgo().snapshot() returning { content, refs }
  • Map repopulation uses RefMap.addWithFrame() to store each ref with complete metadata
  • Hook architecture decouples driver/observe.ts from ref-state.ts through registerSnapshotForRefRefresh

Frequently Asked Questions

What triggers the Ref Map rebuild in ego-browser?

Every call to snapshotRaw() triggers the rebuild through the registered refresh hook. The hook clears the map, fetches new snapshot data, and repopulates entries in sequence.

Why does the Ref Map need to be cleared before each snapshot?

DOM mutations—such as element insertion, removal, or navigation—invalidate backendNodeId values. Clearing ensures no stale references persist that would cause click() or fill() operations to target wrong or non-existent nodes.

What information does each ref entry contain?

Each ref includes: id (the @N string), backendNodeId (Chrome DevTools Protocol node ID), role (ARIA role), name (accessible name), nth (position among similar elements), and frameId (for cross-frame resolution).

Can agents use snapshotRaw() directly or only through snapshot()?

Agents can call snapshotRaw() directly when they need the raw { content, refs } structure. The standard snapshot() method wraps this with additional processing; both trigger identical Ref Map rebuild behavior.

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 →