How the Element Resolver Distinguishes Between Transient and Permanent Resolution Failures in ego-lite

The element resolver uses a kind field on ElementResolutionError to classify failures as "transient" (retryable) or "permanent" (non-retryable), with the distinction determined by failure type, selector evaluation results, and ref-ID validity.

The element resolver in ego-lite converts CSS selectors or ref-IDs (like @21) into concrete element coordinates or object IDs. When resolution fails, the system must decide whether retrying could succeed. This article examines how ego-lite implements this classification based on the source code in citrolabs/ego-lite.

The kind Field: Core Mechanism

Every ElementResolutionError carries a kind property with two possible values:

  • "transient" — The failure may resolve on retry (e.g., stale ref, missing snapshot data)
  • "permanent" — The failure will persist regardless of retries (e.g., invalid selector, ambiguous matches)

Callers inspect this field to implement appropriate retry logic or immediate error surfacing.

How Resolution Failures Are Classified

The resolver applies four distinct rules to determine kind, implemented in element-resolver.ts.

Multiple Selector Matches: Permanent Failure

When a selector matches more than one element, the resolver cannot determine user intent. The helper matchCountKind() parses error messages for patterns like "matched 3 elements" and returns "permanent" when the count exceeds 1.

// element-resolver.ts lines 46-50
if (matchCount > 1) {
  return "permanent";  // Cannot disambiguate multiple matches
}
return "transient";    // Zero matches might resolve after DOM changes

This conservative approach treats ambiguity as unrecoverable.

Invalid Selector Syntax: Permanent Failure

Evaluation errors that don't match the "matched N elements" pattern indicate broken selector syntax or runtime exceptions. The selectorResolutionError() wrapper unconditionally assigns kind: "permanent".

// element-resolver.ts lines 52-60
function selectorResolutionError(originalError: Error): ElementResolutionError {
  // Parse message for match count to determine if retryable
  const kind = matchCountKind(originalError.message);
  return new ElementResolutionError(
    `Selector resolution failed: ${originalError.message}`,
    kind === "transient" ? "transient" : "permanent"
  );
}

Syntax errors cannot be fixed by waiting—the selector itself must change.

Missing or Stale Ref-ID: Transient Failure

Ref-IDs reference cached backend nodes that may become invalid after DOM mutations. The resolver throws "transient" because a fresh snapshot can repopulate the ref map.

// element-resolver.ts lines 74-75
throw new ElementResolutionError(`Unknown ref: ${refId}`, "transient");

This enables automatic recovery through snapshot refresh cycles.

Unavailable Box Model: Transient Failure

When a node exists but lacks render geometry (not yet displayed or hidden), the error propagates unchanged with "transient" status. Lines 94-99 in resolveElementCenter preserve this classification, allowing retries after navigation or visibility changes.

Practical Implementation Patterns

Basic Error Handling with Conditional Retry

try {
  const { x, y } = await ego.resolveElementCenter(selectorOrRef);
  // Use coordinates...
} catch (e) {
  if (e instanceof ego.ElementResolutionError && e.kind === "transient") {
    await ego.waitForIdle(0.5);
    return await ego.resolveElementCenter(selectorOrRef);  // Retry once
  }
  // Permanent: surface to user
  console.error("Resolution failed permanently:", e.message);
  throw e;
}

Robust Multi-Attempt Resolution for Stale Refs

async function safeResolveRef(ref, maxAttempts = 3) {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await ego.resolveElementObjectId(ref);
    } catch (e) {
      if (e.kind === "transient") {
        await ego.takeSnapshot();  // Force ref map refresh
        continue;
      }
      throw e;  // Permanent failure aborts immediately
    }
  }
  throw new Error(`Failed to resolve ref after ${maxAttempts} attempts`);
}

Source File Reference

File Location Purpose
element-resolver.ts package/ego-browser/src/ Core resolution logic with kind assignment
element-resolver.test.mjs package/ego-browser/src/ Test verification of transient/permanent behavior

The implementation ensures that retry resources are spent only on failures where DOM state changes could yield success, while permanent configuration issues receive immediate, clear feedback.

Summary

  • ElementResolutionError.kind enables programmatic distinction between retryable and fatal resolution failures
  • Permanent failures: multiple selector matches, invalid syntax, evaluation exceptions
  • Transient failures: stale ref-IDs, missing snapshot data, unavailable box models
  • The matchCountKind() helper parses error messages to disambiguate zero matches (transient) from multiple matches (permanent)
  • Callers should implement exponential backoff with snapshot refresh for transient errors and immediate user notification for permanent errors

Frequently Asked Questions

What happens if I retry a permanent resolution failure?

Retrying a permanent failure wastes resources and delays error reporting. The same invalid selector or ambiguous match will persist across all attempts. The ego-lite source code guarantees this through the hard-coded "permanent" assignment in selectorResolutionError().

How does the resolver handle zero selector matches versus multiple matches?

Zero matches receive "transient" classification because the DOM may eventually contain the target element. Multiple matches receive "permanent" because the resolver cannot determine which matching element the caller intended. The matchCountKind() function implements this split at lines 46-50.

Can transient failures become permanent?

No—a transient classification never converts to permanent within a single resolution attempt. However, repeated transient failures may exhaust caller retry limits, causing the operation to fail with a timeout or attempt-exceeded error rather than a resolution error.

Where should I implement retry logic for transient errors?

Implement retry loops in calling code rather than inside the resolver. The resolver remains stateless and deterministic, leaving retry policy (attempt count, delays, snapshot triggers) to application-specific handlers. The safeResolveRef() example above demonstrates this pattern.

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 →