How Ego-Lite Maps backendNodeId to Ref Numbers: Inside the Ref Map System

Ego-Lite maintains a Ref Map that assigns sequential numeric identifiers (e.g., @21) to Chrome DevTools Protocol backend node IDs, enabling concise element references in agent scripts.

Ego-Lite is an open-source browser automation framework that simplifies interaction with the Chrome DevTools Protocol (CDP). When manipulating DOM elements, the system translates low-level backendNodeId values into human-readable ref numbers to streamline agent scripting. This mapping mechanism is central to how Ego-Lite handles element identification across snapshots and CDP calls.

Understanding the Ref Map Architecture

Chrome DevTools Protocol identifies DOM nodes using opaque backendNodeId integers that are difficult to reference manually. Ego-Lite solves this usability challenge through an internal Ref Map data structure.

The Ref Map serves as a bidirectional lookup table stored in src/ref-map.ts. It assigns each unique backendNodeId discovered during snapshot capture a short, sequential ref number prefixed with @ (e.g., @1, @2, @21). This abstraction allows automation scripts to interact with elements using memorable identifiers rather than raw CDP values.

The Four-Stage Mapping Workflow

The conversion from backendNodeId to ref number follows a precise lifecycle implemented across the Ego-Lite runtime:

1. Snapshot Capture

When ego.snapshot() is invoked, the browser runtime traverses the current DOM tree and records every visible node. Each node’s backendNodeId—the raw CDP identifier—is extracted and stored temporarily before ref assignment.

This process is orchestrated in src/browser-runtime.ts, which handles the CDP layer communication and ensures all DOM nodes are accounted for regardless of depth or visibility.

2. Ref Generation

Following snapshot capture, the Ref Map iterates over the collected backendNodeId values and creates numeric references for each. The mapping is stored as a Map<number, BackendNodeId> where the key represents the sequential ref number and the value stores the original CDP identifier.

// Type definition from src/ref-map.ts
type RefMap = Map<number, BackendNodeId>;

// Sequential assignment during snapshot processing
const refNumber = map.size + 1; // Generates 1, 2, 3...
map.set(refNumber, backendNodeId);

3. Runtime Lookup Resolution

When an agent script references an element using ref syntax, Ego-Lite performs a reverse lookup to retrieve the original CDP identifier. The resolution logic in src/element-resolver.ts strips the @ prefix and queries the current Ref Map.

// Simplified lookup function (as implemented in src/ref-map.ts)
function resolveRef(ref: string): BackendNodeId {
  const refNum = Number(ref.slice(1)); // Strip leading '@'
  const backendId = currentRefMap.get(refNum);
  if (!backendId) throw new Error('Ref not found');
  return backendId;
}

This retrieved backendNodeId is then passed to subsequent CDP calls for actions like clicking, typing, or evaluating JavaScript against the specific element.

4. Automatic Refresh Mechanism

The Ref Map is invalidated during navigation or significant DOM mutations. If a script attempts to use a ref when the map is empty or stale, Ego-Lite automatically triggers ego.snapshot() before executing the command. This ensures refs remain valid without requiring manual state management from the agent.

// Automatic refresh occurs transparently
await ego.type('@5', 'hello world'); // Triggers snapshot if map is empty

Implementation in src/ref-map.ts and src/ref-state.ts

The core mapping logic resides in src/ref-map.ts, which exports functions for building the ref table and performing lookups. The module maintains the invariant that ref numbers are dense (no gaps) and start from 1 for each snapshot session.

Related state persistence is handled in src/ref-state.ts, which stores the current Ref Map instance and provides helper functions for determining when a refresh is necessary. This separation of concerns allows the mapping engine to remain stateless while the state module manages snapshot lifecycle and map validity.

Usage Examples

Agent scripts interact with the ref system through high-level commands that accept @<number> syntax:

// Populate the Ref Map explicitly
await ego.snapshot();

// Use refs to interact with elements
await ego.click('@12');
await ego.type('@5', 'hello world');

Behind the scenes, these calls resolve to CDP commands using the stored backendNodeId mappings from src/ref-map.ts. If the map lacks the requested ref, src/browser-runtime.ts orchestrates an automatic snapshot refresh before retrying the operation.

Summary

  • Ego-Lite translates CDP backendNodeId values into human-readable ref numbers (e.g., @21) via an internal Ref Map.
  • The mapping is created during snapshot capture in src/browser-runtime.ts and stored as a Map<number, BackendNodeId> in src/ref-map.ts.
  • Ref resolution occurs in src/element-resolver.ts, converting @ prefixed strings back to backendNodeId values for CDP calls.
  • An automatic refresh mechanism ensures the Ref Map stays synchronized with DOM changes without manual intervention.
  • State management is delegated to src/ref-state.ts, which tracks map validity across navigation events.

Frequently Asked Questions

What happens if I use a ref number that doesn't exist?

If you reference an invalid ref like @999 when no such mapping exists in the current Ref Map, Ego-Lite throws a "Ref not found" error. The system does not automatically create mappings for non-existent refs; you must first capture a snapshot that includes the target element.

How long do ref numbers remain valid?

Ref numbers are valid only for the current snapshot session. If the page navigates or the DOM structure changes significantly, the Ref Map is cleared. However, Ego-Lite's automatic refresh mechanism in src/ref-state.ts triggers a new snapshot transparently when you next attempt to use a ref, regenerating valid mappings for the current page state.

Can I predict which ref number an element will receive?

No, ref numbers are assigned sequentially based on the order elements are discovered during DOM traversal during snapshot capture. The specific number assigned to an element depends on its position in the DOM tree and the traversal algorithm implemented in src/browser-runtime.ts, making refs unsuitable for stable long-term element identification across sessions.

Where is the Ref Map stored during execution?

The active Ref Map instance is maintained in src/ref-state.ts, which exports getter and setter functions for the current map. This module ensures that src/element-resolver.ts and other consumers always access the most recent snapshot data without direct dependency on the snapshot trigger logic.

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 →