How Snapshot Refs (@N) Work in ego-lite: Validity and Lifecycle Rules

Snapshot refs (@N) are short-lived numeric handles that point to DOM elements captured in the current semantic page snapshot and become invalid after navigation, DOM mutations, or a 2-second TTL expiration.

In the citrolabs/ego-lite browser automation framework, snapshot refs provide a temporary way to reference specific DOM nodes without relying on stable CSS selectors or XPath expressions. These refs are generated when you call the snapshot() helper and are maintained in a transient map that the runtime automatically invalidates under specific conditions.

What Are Snapshot Refs (@N)?

A snapshot ref is written as @N, where N represents a numeric identifier assigned to an element during the most recent semantic page snapshot. When the runtime captures a snapshot, it records a mapping between these numeric identifiers and the underlying Chrome DevTools Protocol (CDP) backendNodeIds.

According to the source code in src/ref-map.ts, the RefMap class stores each entry as an object containing the backend node ID, accessibility role, element name, index position, frame ID, and selector information:

this.map.set(refId, {
  backendNodeId,
  role,
  name,
  nth,
  selector: undefined,
  frameId,
});

The helper function parseRef() located at lines 43-55 in src/ref-map.ts recognizes ref strings written as @123 (or ref=123) and extracts the numeric identifier so the resolver can perform the lookup.

How Snapshot Refs Are Created and Stored

Snapshot refs originate from the semantic snapshot mechanism in src/ref-state.ts. When a script invokes the snapshot() helper, the browser runtime creates a fresh RefMap instance that associates numeric IDs with live DOM nodes.

The resolution process works as follows:

  1. The script requests an element using a ref like @7
  2. parseRef() in src/ref-map.ts extracts the numeric string "7"
  3. The ElementResolver (src/element-resolver.ts) looks up this ID in the current RefMap
  4. If found, the resolver returns the corresponding DOM element; if not, it triggers a re-snapshot or throws a transient error

Because refs depend on the current snapshot state, they are tied to a specific point in time and are not persistent across page changes.

When Snapshot Refs Become Invalid

Refs remain valid only while the snapshot map remains fresh. The ego-lite runtime clears or refreshes the map in several specific situations:

When a new page loads or the current URL changes, the snapshot is discarded immediately. All previous refs are removed from memory because they pointed to nodes in the previous document.

DOM Mutations

Significant structural changes—such as when an element is removed or its position in the DOM changes—trigger a new snapshot. The old ref no longer points to a live node and is removed from the map.

TTL Expiration

The runtime automatically discards the snapshot after 2 seconds of inactivity. This timeout is implemented in src/browser-runtime.ts to prevent stale references from accumulating during long-running scripts.

Explicit Clearing

Calling clearPreferredTarget() or similar helper functions that reset the snapshot state will immediately clear all stored refs from the RefMap.

Failed Resolution Attempts

When code attempts to resolve a ref that is not present in the current map, the runtime treats this as a transient failure and triggers an automatic re-snapshot. The original ref is considered expired, and the operation retries with a fresh snapshot.

Best Practices for Using Snapshot Refs

Snapshot refs are intended for short-term use only, specifically within a single helper call or a quick series of actions on the same page state. For longer-lived interactions, you should avoid refs in favor of stable targeting strategies.

Use stable locators such as css=…, xpath=…, or loc=… for any persistent element references. These selectors query the DOM live and never suffer from snapshot invalidation.

Working with Snapshot Refs: Code Examples

The following example demonstrates capturing a snapshot and using a ref before it expires:

const { snapshot, click } = ego;

// Take a snapshot and capture a ref
const snap = await snapshot();  // Creates a fresh RefMap
const btnRef = snap.refs.find(r => r.name === 'submit');  // e.g., "@7"

// Use the ref immediately while the snapshot is fresh
await click(btnRef);  // Resolves @7 to the element and clicks it

After navigation or a 2-second delay, the same ref becomes invalid:

// Navigation occurs here...

// This will fail because the snapshot was cleared
await click(btnRef);  
// Throws: ElementResolutionError: Unknown ref: 7 (transient)

For long-running scripts, prefer stable locators that query the DOM directly:

// This locator remains valid regardless of snapshot state
await click('css=button[type="submit"]');

Key Source Files

The snapshot ref lifecycle is implemented across four core files in the citrolabs/ego-lite repository:

  • src/ref-map.ts – Defines the RefMap class and the parseRef() parser that converts @N strings to numeric identifiers.
  • src/ref-state.ts – Tracks snapshot freshness and manages when the ref map is cleared or refreshed.
  • src/element-resolver.ts – Resolves selector strings (including refs) to actual DOM elements using the current RefMap.
  • src/format.ts – Documents the accepted selector syntax, including the @N format for snapshot refs.

Summary

  • Snapshot refs (@N) are temporary numeric handles tied to a specific semantic page snapshot captured by the ego-lite runtime.
  • They are stored in RefMap (src/ref-map.ts) and resolved via parseRef() and the ElementResolver.
  • Refs become invalid after navigation, DOM mutations, 2 seconds of inactivity (TTL), or explicit clearing.
  • Failed ref lookups trigger automatic re-snapshotting, marking the original ref as transient.
  • For automation scripts, use stable locators (css=…, xpath=…) instead of refs for any element interactions that persist beyond immediate snapshot use.

Frequently Asked Questions

How long do snapshot refs (@N) remain valid?

Snapshot refs remain valid for approximately 2 seconds after creation or until the next navigation or DOM mutation occurs. The runtime enforces a TTL (time-to-live) expiration of 2 seconds of inactivity, after which the RefMap is automatically cleared.

What happens if I use a snapshot ref after the page changes?

If you attempt to use a snapshot ref after navigation or significant DOM changes, the ElementResolver will throw an ElementResolutionError indicating the ref is unknown or transient. The runtime may automatically attempt a re-snapshot, but the original numeric handle will not resolve to the intended element.

Should I use @N refs or CSS selectors for element targeting?

Use @N refs only for short-term, immediate actions within the same snapshot context. For any long-running scripts or persistent element references, prefer CSS selectors, XPath expressions, or named locators (loc=…) because they query the DOM live and do not expire when the snapshot clears.

Where is the snapshot ref data stored in the ego-lite source code?

Snapshot ref data is stored in the RefMap class defined in src/ref-map.ts. This class maintains a mapping between numeric ref IDs and CDP backendNodeIds. The freshness of this data is managed by src/ref-state.ts, while resolution logic resides in src/element-resolver.ts.

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 →