# How the Snapshot Reference System Works in ego-lite

> Discover how ego-lite's snapshot reference system maps @ identifiers to backendNodeIds, rebuilding references with ensureSnapshot() upon DOM changes. Learn its efficient DOM mutation handling.

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

---

**The snapshot reference system in ego-lite maps compact numeric identifiers prefixed with `@` (e.g., `@21`) to Chrome DevTools Protocol backendNodeIds, automatically rebuilding the reference cache via `ensureSnapshot()` whenever DOM mutations invalidate existing mappings.**

ego-lite is an open-source browser automation framework that eliminates fragile CSS selectors in favor of stable numeric references. The snapshot reference system provides agents with short identifiers like `@42` that resolve to DOM element handles through an automatically refreshing cache mechanism built on the Chrome DevTools Protocol (CDP).

## Core Architecture of the Reference System

### The Ref Map

In [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts), ego-lite maintains an in-memory `Map<number, ElementHandle>` that stores the current mapping from numeric IDs to DOM element handles. This map is rebuilt on every snapshot, ensuring that fresh references always reflect the latest page state. The map uses CDP's stable `backendNodeId` values as keys, allowing agents to address elements with compact `@<id>` strings rather than volatile XPath or CSS selectors.

### Ref State and Snapshot Coordination

The [`src/ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-state.ts) file implements the public API for snapshot management, exposing `ensureSnapshot()` and `getRef()` helpers. When a reference lookup fails, the system automatically triggers a new snapshot to repopulate the cache. This state module tracks the last snapshot timestamp and determines when the Ref Map requires reconstruction.

### CDP Snapshot Integration

According to [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts), the snapshot logic captures full DOM/AX tree data via CDP commands including `Page.getLayoutMetrics` and `DOM.getDocument`. The runtime walks the DOM tree, extracts each node's `backendNodeId`, and populates the Ref Map through the snapshot handler. When a helper encounters a missing ref, it asks the runtime to re-snapshot and retry.

## How References Resolve Elements

### The Resolution Workflow

When an agent calls `click('@21')`, [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) parses the locator string to extract the numeric ID. The resolver checks the current Ref Map for the corresponding `ElementHandle`. If found, it returns the handle immediately; if missing, it invokes `ensureSnapshot()` to rebuild the cache and retries the lookup. This automatic re-snapshotting ensures refs remain valid across navigation and DOM changes without manual intervention.

### Automatic Re-snapshotting on Cache Misses

The system detects cache misses during resolution and automatically refreshes the Ref Map. Because the map is rebuilt on every snapshot, the same numeric ID may point to a different element after a page change, which is exactly what agents need when working across multiple scrolling or navigation steps. This guarantees that references are always fresh without requiring agents to manage snapshots manually.

## Error Handling and Retry Logic

Resolution failures generate `ElementResolutionError` instances with `transient` or `permanent` flags. Transient errors indicate the missing ref will likely appear after a re-snapshot, while permanent errors signal fundamentally invalid selectors. In [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts), the retry logic respects this distinction: transient errors trigger a re-snapshot and retry cycle, while permanent errors abort immediately. This prevents infinite loops on malformed references while allowing recovery from temporary DOM instability.

## Lifecycle and Cache Invalidation

The snapshot reference system invalidates its cache during specific lifecycle events:

- **Navigation/Reload**: The existing Ref Map clears automatically, forcing a fresh snapshot on the next reference access.
- **DOM Mutations**: Changes like `innerHTML` updates may leave stale entries; the first failed lookup triggers `ensureSnapshot()` to refresh the map.
- **Explicit Snapshots**: Agents can call `await snapshot()` to manually invoke `ensureSnapshot()` and guarantee up-to-date references before critical operations.
- **Task-Space Isolation**: Each task space maintains independent runtime state in [`src/ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-state.ts), ensuring ref maps remain isolated between concurrent operations.

## Practical Code Examples

```javascript
// Click an element using a snapshot ref
await click('@42');

```

```javascript
// Force a fresh snapshot before using a ref
await snapshot();
await type('@7', 'hello');

```

```javascript
// Handling a transient failure with retry logic
try {
  await click('@99');
} catch (e) {
  if (e instanceof ElementResolutionError && e.transient) {
    // Re-snapshot performed automatically on retry
    await click('@99');
  } else {
    throw e;
  }
}

```

## Summary

- **[`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts)** implements the in-memory `Map<number, ElementHandle>` that stores current ID-to-element mappings.
- **Automatic re-snapshotting** occurs whenever [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) detects a cache miss, ensuring references remain valid across DOM changes.
- **Transient vs. permanent errors** in [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts) distinguish between temporary unavailability (retryable) and invalid selectors (fatal).
- The system leverages **CDP backendNodeIds** for the document lifecycle, providing stable identifiers without CSS dependency.
- **Task-space isolation** ensures concurrent operations maintain independent reference maps.

## Frequently Asked Questions

### What does the @ symbol mean in ego-lite selectors?

The `@` prefix denotes a snapshot reference that resolves to a CDP `backendNodeId`. When you write `click('@21')`, ego-lite looks up the numeric ID `21` in the current Ref Map to find the corresponding DOM element handle, bypassing the need for CSS selectors or XPath expressions.

### How does ego-lite handle stale references after page navigation?

After navigation or page reload, the Ref Map clears automatically. The next reference lookup triggers `ensureSnapshot()` in [`src/ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-state.ts), which rebuilds the map using fresh CDP data. This ensures that numeric IDs always resolve to elements in the current document state, even though the same ID may now point to a different element.

### What is the difference between transient and permanent resolution errors?

`ElementResolutionError` thrown by [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) includes a `transient` flag. Transient errors indicate the element likely exists but is temporarily missing from the cache due to DOM mutations, triggering automatic re-snapshotting and retry in [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts). Permanent errors indicate invalid selectors (e.g., non-existent IDs) that will never resolve.

### When should I manually trigger a snapshot?

While the system auto-refreshes on cache misses, you can manually invoke `await snapshot()` before critical sequences to ensure the Ref Map reflects the absolute latest DOM state. This is useful when you know a previous action caused significant DOM changes and you want to avoid the small overhead of automatic re-snapshotting during the next reference lookup.