How @N Element References Are Resolved and Mapped Across Page Snapshots in ego-lite

@N element references in ego-lite work by mapping numeric IDs to stored backendNodeId values in a snapshot-scoped RefMap, with automatic fallback to role/name matching if the DOM node becomes stale.

The @N syntax is a shorthand notation used throughout ego-lite to reference DOM elements captured in the most recent page snapshot. This system enables reliable element targeting for AI-driven browser automation without requiring complex CSS or XPath selectors.

The RefMap: Core Data Structure for @N References

When snapshot() is invoked, the runtime traverses the DOM and accessibility tree to build a RefMap instance called browserRefMap in [src/ref-state.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/ref-state.ts). This map serves as the authoritative source for all @N reference resolution.

Storing Element References

The RefMap.addWithFrame() method records each element with the following fields:

Field Purpose
backendNodeId Chrome DevTools Protocol (CDP) backend node identifier
role / name Accessibility role and accessible name (fallback lookup)
nth Position when multiple elements match the same selector
frameId Target CDP session/frame for iframe traversal

The reference ID is simply the element's insertion index. The first stored element becomes @1, the second @2, and so on.

Resolving @N References to CDP Commands

Element resolution happens in [src/element-resolver.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/element-resolver.ts). The process follows this sequence:

  1. parseRef() (lines 70–76) extracts the numeric ID from strings like "@3" or ref=3

  2. Map lookup retrieves the stored entry via refMap.get(refId)

  3. Direct CDP invocation uses the stored backendNodeId for commands like DOM.getBoxModel or DOM.resolveNode (lines 81–90 and 167–178)

  4. Stale node fallback triggers findBackendNodeIdByRoleName() if the original backendNodeId no longer exists, ensuring predictable errors rather than silent mis-clicks

const refId = parseRef(selectorOrRef);        // "@3" → 3
const entry = refMap.get(refId);              // fetch stored data
const result = await send(cdp, "DOM.getBoxModel",
            { backendNodeId: entry.backendNodeId }, effectiveSessionId);

Cross-Snapshot Reference Mapping Behavior

** References are strictly snapshot-scoped.** The RefMap is completely rebuilt on every snapshot call—previous numeric IDs carry no meaning across snapshot boundaries.

If automation code attempts to resolve a reference while browserRefMap is empty (common after navigation), the helper layer in [src/helpers.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts) automatically triggers a fresh snapshot before proceeding. This design guarantees that every @N reference always points to a valid, current element.

Practical Usage Examples

// 1️⃣ Take a snapshot – the runtime builds the RefMap internally
await snapshot();   // now @1 … @N are valid

// 2️⃣ Click the element recorded as @4
await click("@4");

// 3️⃣ Resolve the center of @2 for coordinate-based actions
const {x, y, sessionId} = await resolveElementCenter(
    cdp, undefined, browserRefMap, "@2");

// 4️⃣ Navigation invalidates references – snapshot refreshes the map
await navigate("https://example.com");
await snapshot();   // fresh @1…@M for the new page
await click("@1");   // refers to the new page's first recorded element

Key Implementation Files

File Responsibility
[src/ref-map.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/ref-map.ts) RefMap class definition and parseRef() helper
[src/ref-state.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/ref-state.ts) Global browserRefMap instance management
[src/element-resolver.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/element-resolver.ts) Resolution logic: resolveElementCenter(), resolveElementObjectId()
[src/helpers.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts) Auto-snapshot trigger when resolving empty-map references
src/element-resolver.test.mjs Test coverage for reference lifecycle and fallback behavior

Summary

  • @N syntax maps to sequential indices in a snapshot-specific RefMap
  • Resolution path: parse → map lookup → CDP command with backendNodeId
  • Stale node handling falls back to role/name matching for safe failures
  • Snapshot scoping ensures references never outlive their validity window
  • Automatic refresh guarantees valid references through implicit snapshot calls

Frequently Asked Questions

What happens if I use @N after the page changes?

The reference resolution will fail with a retryable error. According to the ego-lite source code, if the stored backendNodeId is stale, the system attempts findBackendNodeIdByRoleName() to re-locate by accessibility attributes before raising a transient error. For reliable automation, always call snapshot() after significant DOM changes.

Can @N references work across iframes?

Yes. The RefMap stores frameId with each entry, and element-resolver.ts uses effectiveSessionId to route CDP commands to the correct frame context when resolving cross-frame @N references.

Why are reference IDs not persistent across snapshots?

The RefMap is deliberately re-created on every snapshot to prevent stale element references. This design choice in ref-map.ts ensures that numeric IDs always correspond to currently-captured DOM state, eliminating an entire class of automation bugs from outdated references.

How do I debug which element @N refers to?

Inspect browserRefMap after snapshotting to see the underlying backendNodeId, role, and name fields. The test suite in element-resolver.test.mjs demonstrates programmatic access to these entries for verification purposes.

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 →