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
@Nmarkers - A
refsmap (defined insrc/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:
src/driver/observe.ts–snapshotRaw()builds the raw snapshot and registers refresh callbackssrc/ref-map.ts–RefMap.addWithFrameingests node metadata into the global mapsrc/helpers.ts– exposessnapshot()/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 bypage.snapshot() - They store
backendNodeIdand metadata in the globalRefMap(src/ref-map.ts) - Four events invalidate refs: navigation, DOM mutation, task‑space switch, or explicit
snapshot()call - Automatic recovery: empty
RefMaptriggers fresh snapshot viaensureSession()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, andsrc/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →