How the Snapshot Reference System Works in ego-lite
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, 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 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, 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 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, 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
innerHTMLupdates may leave stale entries; the first failed lookup triggersensureSnapshot()to refresh the map. - Explicit Snapshots: Agents can call
await snapshot()to manually invokeensureSnapshot()and guarantee up-to-date references before critical operations. - Task-Space Isolation: Each task space maintains independent runtime state in
src/ref-state.ts, ensuring ref maps remain isolated between concurrent operations.
Practical Code Examples
// Click an element using a snapshot ref
await click('@42');
// Force a fresh snapshot before using a ref
await snapshot();
await type('@7', 'hello');
// 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.tsimplements the in-memoryMap<number, ElementHandle>that stores current ID-to-element mappings.- Automatic re-snapshotting occurs whenever
src/element-resolver.tsdetects a cache miss, ensuring references remain valid across DOM changes. - Transient vs. permanent errors in
src/driver/waits.tsdistinguish 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, 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 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. 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.
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 →