How to Debug Element Resolution Failures with the @N Reference System in Ego Lite

To debug element resolution failures with the @N reference system, inspect the RefMap entry for the numeric ID, check whether the ElementResolutionError is transient (retry with fresh snapshot) or permanent (fix locator specificity), and verify the fallback role/name lookup in the Accessibility tree when backendNodeId is stale.

The Ego Lite browser harness uses a numeric reference system (@N) to track UI elements across browser sessions via the RefMap data structure. When automation scripts fail to resolve these references, understanding the distinction between transient DOM changes and permanent locator issues is essential for stable test execution. This guide walks through the resolution pipeline implemented in citrolabs/ego-lite to help you diagnose and fix these failures quickly.

Understanding the @N Reference Resolution Pipeline

The resolution flow in element-resolver.ts follows a strict five-step pipeline when locating elements via numeric references:

  1. Parse the reference string – The parseRef function in ref-map.ts strips the leading @ or ref= prefix and validates that the remaining string consists only of digits.
  2. Lookup in RefMap – The system calls RefMap.get(refId) to retrieve the stored entry containing backendNodeId, ARIA role, accessible name, and optional frame information.
  3. Attempt cached backendNodeId resolution – If a backendNodeId exists, the resolver invokes CDP methods like DOM.getBoxModel or DOM.resolveNode. If this throws an ElementResolutionError due to a missing box model (element not rendered), the error is re-thrown for retry logic. If the node is stale (DOM changed), execution silently proceeds to step 4.
  4. Execute role/name fallback – The findBackendNodeIdByRoleName function traverses the Accessibility tree to locate the element by its stored ARIA role and name. Failure here generates a transient ElementResolutionError.
  5. Return geometry or objectId – Upon successful resolution, the system calculates the element center via boxModelCenter or returns an objectId for subsequent CDP commands.

Distinguishing Transient from Permanent Errors

The resolver categorizes failures into two distinct kinds that dictate your debugging strategy:

Transient Errors occur when:

  • The reference ID is unknown (missing from current snapshot)
  • The cached backendNodeId is stale due to DOM mutations
  • The element lacks a box model because it is not yet rendered or is zero-sized

Action: Retry the operation. The harness automatically triggers a fresh snapshot and re-attempts resolution.

Permanent Errors occur when:

  • A selector matches multiple elements when uniqueness is required
  • The selector syntax is invalid
  • The AX node lacks a backendDOMNodeId (non-renderable elements)

Action: Fix the locator to be more specific or adjust your reference usage to target renderable elements only.

Step-by-Step Debugging Workflow

When an @N reference fails, follow this systematic approach to identify the root cause:

Inspect the RefMap Entry

Use RefMap.get(id) to examine the stored data for the failing reference. If backendNodeId is undefined, the system will rely entirely on the role/name fallback.

console.log('RefMap entry for @5:', refMap.get('5'));
// Output: { backendNodeId: 11234, role: 'textbox', name: 'Search', ... }

Check the Error Kind

Catch ElementResolutionError and examine the kind property. Transient errors warrant an automatic retry, while permanent errors require locator modification.

if (e instanceof ElementResolutionError) {
  if (e.kind === 'transient') {
    await snapshot(); // Force fresh snapshot
  } else {
    console.error('Permanent error:', e.message);
  }
}

Validate the Snapshot

Force a fresh snapshot using await snapshot() (exposed via the helper context) to capture the current DOM state. This is particularly useful when elements load asynchronously.

Confirm Role-Name Pairs

When the fallback logic activates, verify that the element's ARIA role and accessible name match the RefMap entry. The lookup is performed by findBackendNodeIdsByRoleName, which filters the AX tree for matching nodes.

Review Resolver Logs

The resolver provides specific error messages through the ElementResolutionError constructor:

  • Could not locate element with role=${role} name=${name}
  • Element has no box model (not rendered or zero-sized)

Practical Code Examples

Basic Reference Resolution

// Resolve center coordinates for element @12
const { x, y, sessionId } = await resolveElementCenter(
  cdp,
  currentSessionId,
  refMap,
  '@12'
);

If the reference is unknown or stale, this call throws an ElementResolutionError with kind 'transient', triggering automatic retry logic in the harness.

Manual Error Handling

try {
  const center = await resolveElementCenter(cdp, sid, refMap, '@7');
  console.log('Clicked at', center.x, center.y);
} catch (e) {
  if (e instanceof ElementResolutionError) {
    if (e.kind === 'transient') {
      console.warn('Transient failure – retrying after snapshot');
      await snapshot();
      // Retry logic here
    } else {
      console.error('Permanent resolution error:', e.message);
    }
  } else {
    throw e;
  }
}

Understanding the Role-Based Fallback

When backendNodeId is missing or stale, the resolver automatically falls back to the stored role/name pair:

// RefMap contains: { role: 'button', name: 'Submit' }
await resolveElementCenter(cdp, sid, refMap, '@3');
// Internally invokes:
//   findBackendNodeIdByRoleName(cdp, sid, 'button', 'Submit')
//   followed by DOM.getBoxModel on the resolved node

Key Source Files for Debugging

File Purpose
element-resolver.ts Core implementation of resolution logic, error handling, and fallback mechanisms
ref-map.ts Stores snapshot-to-reference mappings and implements parseRef
element-resolver.test.mjs Test suite demonstrating success and failure scenarios
driver/observe.ts High-level helper usage showing integration patterns

Summary

  • The @N system relies on RefMap entries containing backendNodeId, role, and name to locate elements across browser sessions.
  • Transient errors indicate timing or staleness issues; retry with a fresh snapshot via snapshot().
  • Permanent errors indicate locator ambiguity or invalid selectors; refine your reference criteria.
  • Fallback logic traverses the Accessibility tree by role/name when cached node IDs fail.
  • Debug via RefMap inspection and ElementResolutionError kind classification to determine appropriate corrective action.

Frequently Asked Questions

What causes a transient ElementResolutionError?

A transient error occurs when the RefMap lacks the requested ID, the cached backendNodeId is stale due to DOM changes, or the element has no box model because it is not yet rendered. According to the source code in element-resolver.ts, these conditions prompt an automatic retry after a fresh snapshot.

How does the fallback mechanism work when backendNodeId is stale?

When CDP methods like DOM.getBoxModel fail due to a stale node ID, the resolver silently invokes findBackendNodeIdByRoleName to search the Accessibility tree using the stored ARIA role and name. If this lookup succeeds, the resolver proceeds with the fresh node; otherwise, it throws a transient error.

What is the difference between @N references and CSS selectors?

The @N system uses numeric references that map to cached backendNodeId values and accessibility metadata in the RefMap, enabling fast re-resolution across page changes. CSS selectors are parsed as locator strings and resolved via querySelector without the caching and fallback layers provided by the reference system.

How can I force a fresh snapshot to resolve a stale reference?

Expose the snapshot() function from your helper context and invoke it manually when catching a transient error. This updates the RefMap with current backendNodeId values and accessibility data, allowing subsequent resolveElementCenter calls to succeed.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →