How the Ego-Browser Snapshot and Ref System Persists Across Heredoc Rounds
Ego-Browser maintains a self-healing ref map across heredoc rounds by storing snapshot callbacks that automatically regenerate the browserRefMap whenever a stale @N reference is accessed, allowing JavaScript execution blocks to reuse DOM references without manual snapshot calls.
The ego-browser snapshot and ref system enables persistent DOM interaction across multiple heredoc execution rounds in the citrolabs/ego-lite runtime. Unlike traditional browser automation that loses state between script executions, this architecture maintains a live browser instance with an in-memory reference map that automatically refreshes when needed. Understanding this mechanism is essential for building reliable automation scripts that span multiple stdin heredoc blocks.
The Heredoc Execution Model and Persistent Runtime
When ego-browser executes user-supplied JavaScript, it streams code via heredoc on stdin while keeping the same browser runtime alive between blocks. This persistence means that global state—including the browserRefMap—survives across execution rounds. The system leverages Chrome DevTools Protocol (CDP) to maintain element references using backendNodeId values stored in ref entries.
Snapshot Generation and Ref Population
Capturing DOM State with snapshotRaw()
In src/driver/observe.ts, the snapshotRaw() function orchestrates DOM observation by calling ego.snapshot(). This returns an object containing content and refs, where each ref includes a backendNodeId, role, name, and other accessibility metadata (lines 49‑63). These refs are indexed as @N identifiers that serve as stable handles to DOM elements.
Building the In-Memory Ref Map
The browserSnapshotRefsToRefMap() function in src/browser-runtime.ts (lines 309‑326) clears the global browserRefMap and populates it with new entries. Each entry maps a backendNodeId to its associated metadata, creating a lookup table that resolves @N strings to actual CDP node identifiers. This map lives in memory for the duration of the browser runtime session.
The Self-Healing Ref Resolution Loop
The system implements a lazy refresh mechanism that ensures refs remain valid without requiring manual snapshot calls between heredoc rounds.
Registering the Refresh Callback
During initialization in src/driver/observe.ts (line 65), registerSnapshotForRefRefresh(() => snapshotRaw()) stores a callback that can regenerate the snapshot and ref map on demand. This callback is preserved in the global state managed by src/ref-state.ts, surviving across heredoc executions.
Lazy Map Rehydration via ensureRefMapForRef()
When any helper function receives a selector or ref argument, it first calls ensureRefMapForRef() from src/ref-state.ts (lines 12‑24). If the argument matches the ref pattern (@123) and the browserRefMap is empty, this function automatically executes the stored snapshot callback. This lazy initialization ensures the map is always current before element resolution occurs.
Resolving Elements via CDP
The resolver functions in src/element-resolver.ts—including resolveElementCenter() and resolveElementObjectId()—perform the actual DOM interaction. They lookup the backendNodeId in browserRefMap (lines 70‑78) and fetch element data via CDP commands like DOM.getBoxModel or DOM.resolveNode (lines 101‑115, 166‑186). If a node has become stale, the system falls back to role and name lookups to maintain robustness.
Cross-Round Persistence in Practice
Because the browser runtime persists between heredoc executions, the ref map remains available in memory. When a new heredoc block begins and references an @N ref from a previous round, the ensureRefMapForRef() trigger detects the empty map and invokes snapshotRaw() to rebuild state. This creates a seamless experience where refs survive indefinitely across execution boundaries without user intervention.
Code Example: Refs Across Heredoc Rounds
// First heredoc block - generate refs
await page.snapshot(); // Populates browserRefMap
const btn = await page.$('button'); // Returns "@42"
await btn.click(); // Uses the ref immediately
// Second heredoc block - refs remain valid
await page.elementCenter('@42'); // Triggers ensureRefMapForRef(),
// automatically refreshes map,
// then resolves the element
Internally, the refresh mechanism wires together the observer and resolver:
// src/driver/observe.ts
import { registerSnapshotForRefRefresh } from "./ref-state.js";
registerSnapshotForRefRefresh(() => snapshotRaw());
// src/ref-state.ts
export async function ensureRefMapForRef(selectorOrRef: unknown) {
if (typeof selectorOrRef === "string" && parseRef(selectorOrRef) && browserRefMap.map.size === 0) {
await snapshotImpl(); // Executes stored callback
}
}
Summary
- Persistent runtime: The browser instance stays alive across heredoc rounds, preserving global state.
- Snapshot generation:
snapshotRaw()insrc/driver/observe.tscaptures DOM state and extracts refs withbackendNodeIdvalues. - Ref map storage:
browserSnapshotRefsToRefMap()populates the globalbrowserRefMapinsrc/browser-runtime.ts. - Lazy refresh:
ensureRefMapForRef()insrc/ref-state.tsautomatically triggers snapshot regeneration when accessing refs with an empty map. - CDP resolution:
src/element-resolver.tsconverts refs to DOM nodes via Chrome DevTools Protocol, with stale-node fallbacks.
Frequently Asked Questions
How does ego-browser handle stale element references across heredoc rounds?
When a ref is resolved via resolveElementCenter() or resolveElementObjectId() in src/element-resolver.ts, the system attempts to fetch the node using the stored backendNodeId via CDP. If the node no longer exists, the resolver falls back to searching by role and name attributes (lines 166‑186), ensuring robustness even when the DOM changes between execution rounds.
What triggers the automatic snapshot refresh in the ref system?
The ensureRefMapForRef() function in src/ref-state.ts triggers the refresh. When a helper receives an @N ref string and detects that browserRefMap.map.size === 0, it executes the callback registered by registerSnapshotForRefRefresh(), which invokes snapshotRaw() to rebuild the ref map.
Where is the ref map stored between heredoc executions?
The ref map is stored as a global browserRefMap instance managed in src/browser-runtime.ts and src/ref-state.ts. Because ego-browser maintains the same JavaScript runtime across heredoc stdin blocks, this global state persists in memory without serialization to disk.
Can I use numeric selectors instead of @N refs in ego-browser?
Yes. While the system optimizes for @N refs generated by snapshotRaw(), helpers like elementCenter() and resolveElementCenter() accept both CSS selectors and ref strings. If you pass a standard selector, the system bypasses the ref map resolution and queries the DOM directly via CDP.
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 →