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

> Understand @N snapshot refs in ego-lite. Learn how these temporary DOM identifiers work, when they become invalid, and how to re-snapshot effectively for accurate page snapshots.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: deep-dive
- Published: 2026-08-25

---

**@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`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/observe.ts) to element resolution in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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.

```typescript
// 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.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/observe.ts)** – `snapshotRaw()` builds the raw snapshot and registers refresh callbacks
2. **[`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts)** – `RefMap.addWithFrame` ingests node metadata into the global map
3. **[`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) performs the translation:

```typescript
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 `backendNodeId`s 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)

```typescript
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:

```typescript
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

```typescript
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)

```typescript
// 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

```typescript
// 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

```typescript
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`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts))

```typescript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts).

### Validation for Persistent Storage

The framework warns against misuse in [`src/learning/validate-learning-format.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/observe.ts), [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts), [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts), and [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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.