How @N Snapshot Refs Work in ego‑lite: A Complete Guide to Creation, Validity, and Re‑Snapshotting

@N snapshot refs are temporary numeric identifiers that map to specific DOM nodes in a semantic page snapshot, and they require re‑snapshotting after any navigation, DOM mutation, tab switch, or when the RefMap is manually cleared.

In the ego‑lite browser automation framework (citrolabs/ego-lite), these lightweight references provide a stable way to target elements without brittle selectors. This guide explains the internal mechanics—from snapshot generation in src/driver/observe.ts to element resolution in src/element-resolver.ts—and exactly when you must trigger a fresh snapshot to keep refs valid.


What @N Snapshot Refs Are

When you call page.snapshot() or page.snapshotRaw(), the runtime captures the page's current state as a semantic page snapshot. This snapshot contains two critical pieces:

  • The page's readable text content with embedded @N markers
  • A refs map (defined in src/ref-map.ts) keyed by numeric strings like "123"

Each entry stores the node's backendNodeId, role, name, and optional selector. These entries populate the global in‑memory RefMap via RefMap.addWithFrame, enabling fast element lookup without re‑querying the DOM.

// The snapshot string contains embedded refs you can use directly
const snap = await page.snapshot();
console.log(snap);
// Output: "0  Click here [@23]\n1  Search input [@45]..."

The @23 and @45 are your handles—temporary, lightweight, and tied to that specific snapshot.


How @N Refs Are Created and Resolved

Snapshot Generation Pipeline

The snapshot flow spans three core files:

  1. src/driver/observe.tssnapshotRaw() builds the raw snapshot and registers refresh callbacks
  2. src/ref-map.tsRefMap.addWithFrame ingests node metadata into the global map
  3. src/helpers.ts – exposes snapshot()/snapshotRaw() and handles automatic refresh logic

Element Resolution Path

When a helper like click() or type() receives an @N locator, src/element-resolver.ts performs the translation:

await page.click('@23');  // element-resolver.ts looks up ref "23" in RefMap

The resolver checks the current RefMap. If the ref exists, it returns the associated backendNodeId for direct CDP (Chrome DevTools Protocol) interaction. If the map is empty or the ref is missing, ensureSession() triggers automatic recovery.


When @N Snapshot Refs Become Invalid (Require Re‑Snapshotting)

Because refs capture the DOM at a single moment, they are only valid while that snapshot remains fresh. Four situations force re‑snapshotting:

Situation Mechanism Why Refs Break
Navigation page.goto(), nav.goto(), link clicks DOM tree is destroyed; old backendNodeIds no longer exist
DOM mutation AJAX updates, client‑side rendering, element insertion/removal Referenced nodes may move, change role, or be garbage‑collected
Task‑space switch Agent takes control of different tab or frame Page context changes; RefMap belongs to previous context
Explicit refresh Developer calls await page.snapshot() again Manual invalidation to ensure predictable ref stability

The runtime handles the first three transparently: if a helper receives @N and RefMap is empty, ensureSession() automatically snapshots before resolution. This prevents hard failures but adds a network round‑trip.


Controlling Re‑Snapshotting: Automatic vs. Explicit

Automatic Refresh (Default Behavior)

await page.goto('https://example.com');   // navigation clears RefMap
await page.click('@23');                  // triggers automatic fresh snapshot
// Runtime executes: snapshot() → resolve '@23' from new map → click

This is safe but slower. The system guarantees correctness at the cost of latency.

Explicit Refresh (Performance‑Optimized)

When you know the DOM state, call snapshot() manually to avoid on‑demand refresh:

await page.snapshot();            // force fresh snapshot now
await page.type('@45', 'Hello');  // immediate resolution, no extra round‑trip
await page.click('@67');          // still valid, same snapshot

Best practice: Explicitly re‑snapshot after known mutation points (form submission, infinite scroll load, SPA route changes) when chaining multiple @N operations.


Code Examples: Complete @N Ref Workflows

Basic Snapshot and Reuse

import { Browser } from '@citrolabs/ego-lite';

const browser = await Browser.launch();
const page = await browser.newPage();

// Capture initial state
await page.goto('https://example.com');
const snap = await page.snapshot();  // populates RefMap with @0, @1, @2...

// Use refs across multiple operations
await page.click('@2');
await page.type('@5', 'search query');
await page.click('@8');  // submit button

Handling Navigation (Automatic Refresh)

// After navigation, old refs are invalid
await page.click('@3');                  // works: uses current snapshot
await page.goto('https://other.com');    // RefMap cleared internally
await page.click('@3');                  // works: auto‑snapshots first
// Warning: @3 now points to a DIFFERENT element on the new page

Explicit Refresh for Batch Operations

// Efficient pattern: one snapshot, many actions
await page.goto('https://dashboard.com');
await page.snapshot();                   // single snapshot point

await page.click('@12');                 // menu
await page.click('@15');                 // submenu
await page.type('@20', 'data');          // form
await page.click('@22');                 // save
// All four ops use the same RefMap—no intermediate re‑snapshotting

Post‑Mutation Explicit Control

await page.click('@5');                  // open modal (client‑side render)
// Modal injects new DOM nodes—old refs may shift
await page.snapshot();                   // capture new state including modal

await page.type('@30', 'input value');   // ref created in fresh snapshot
await page.click('@31');                 // modal confirm button

Internal Implementation Details

RefMap Structure (src/ref-map.ts)

// Simplified representation of stored ref entries
interface RefEntry {
  backendNodeId: number;   // CDP node identifier
  role: string;            // ARIA role or element type
  name?: string;           // Accessible name
  selector?: string;       // Optional fallback selector
}

The RefMap class manages lifecycle: addWithFrame populates entries, clear invalidates on navigation, and lookup methods power src/element-resolver.ts.

Validation for Persistent Storage

The framework warns against misuse in src/learning/validate-learning-format.ts: temporary snapshot refs should not appear in site‑skill manifests since they expire. This protects against serializing invalid references.


Summary

  • @N snapshot refs are numeric handles (@23, @45) tied to a specific DOM state captured by page.snapshot()
  • They store backendNodeId and metadata in the global RefMap (src/ref-map.ts)
  • Four events invalidate refs: navigation, DOM mutation, task‑space switch, or explicit snapshot() call
  • Automatic recovery: empty RefMap triggers fresh snapshot via ensureSession() before element resolution
  • Explicit control: manual snapshot() calls eliminate refresh latency for predictable, multi‑step workflows
  • Implementation spans src/driver/observe.ts, src/ref-map.ts, src/helpers.ts, and src/element-resolver.ts

Frequently Asked Questions

What happens if I use an @N ref after the page navigates?

The ref becomes invalid because navigation clears the RefMap. However, the runtime automatically triggers a fresh snapshot before attempting resolution—your code won't crash, but @23 on the new page likely points to a different element than before. Always verify ref semantics after navigation.

How do I prevent automatic re‑snapshotting for performance?

Call await page.snapshot() explicitly at a known stable point, then execute multiple @N operations in sequence. This populates RefMap once and reuses it, avoiding per‑operation refresh round‑trips. Avoid interleaving with actions that mutate the DOM.

Can I persist @N refs between browser sessions?

No. src/learning/validate-learning-format.ts explicitly warns against this. @N refs are temporary, in‑memory identifiers tied to a specific backendNodeId that exists only for that browsing session. For persistent element targeting, use stable selectors stored in site‑skill manifests.

Why does my @N ref resolve to the wrong element after an AJAX update?

DOM mutations—especially element insertion or reordering—shift the ref mapping. The numeric ID @45 may now point to a different node, or the original node may have a new backendNodeId. Re‑snapshot immediately after mutations to synchronize the RefMap with current DOM 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 →