How ego‑browser Handles CDP Session‑Lost Errors and Re‑Attaches Sessions

ego‑browser automatically detects Chrome DevTools Protocol (CDP) session‑lost errors, invalidates stale sessions, and retries requests after re‑attaching to a fresh target—making session recovery transparent to callers.

The ego‑browser runtime (part of the citrolabs/ego-lite repository) implements a resilient CDP transport layer that shields agent scripts from transient browser disconnections. This article explains how the session‑loss detection and automatic re‑attachment mechanism works in practice, with reference to the actual source implementation.

Detecting Session‑Lost Errors with Pattern Matching

Session loss manifests through several distinct error messages from the Chrome DevTools Protocol. In src/browser-runtime.ts, ego‑browser defines a regular expression constant that captures all common variants:

// browser-runtime.ts lines 9-10
const SESSION_LOST = /Session .+? not found|Target closed|No session/i;

This pattern matches errors like:

  • "Session 12345 not found"
  • "Target closed"
  • "No session"

When any CDP response contains an error whose message field matches this regex, the runtime flags the condition as a recoverable session loss rather than a permanent failure.

The Invalidation and Retry Flow

Every CDP request flows through the browserCdp() wrapper, which orchestrates detection, cleanup, and recovery. The mechanism operates in four coordinated stages:

1. Clearing Stale State with invalidateSession()

Once a session‑lost error is detected, invalidateSession() (lines 46‑53) performs immediate cleanup:

// browser-runtime.ts lines 46-53 (conceptual)
function invalidateSession() {
  state.sessionId = null;           // drop the dead session ID
  state.sessionAt = null;           // reset timestamp
  state.dialog = null;              // clear pending dialog state
  // remove page-level event subscriptions...
}

This ensures no subsequent call attempts to reuse an invalid session identifier.

2. Establishing a Fresh Session with ensureSession()

The ensureSession() method (lines 107‑139) implements the re‑attachment logic:

  • Queries available targets via Target.getTargets
  • Selects an active tab (or respects a preferred target if configured)
  • Calls Target.attachToTarget to obtain a new sessionId
  • Enables required domains: Page.enable, Runtime.enable, DOM.enable
  • Caches the new session ID and timestamp in runtime state
// Typical flow inside ensureSession()
const { sessionId } = await browserCdp(
  'Target.attachToTarget',
  { targetId: activeTarget.targetId, flatten: true },
  undefined,  // no sessionId for browser-level calls
);
state.sessionId = sessionId;
state.sessionAt = Date.now();

3. The browserCdp() Wrapper Orchestrates Retry

The wrapper function (lines 95‑104) surrounds every CDP invocation with the recovery logic:

// browser-runtime.ts lines 95-104 (simplified structure)
async function browserCdp(method, params, sessionIdOverride) {
  try {
    return await rawCdp(method, params, effectiveSessionId);
  } catch (error) {
    if (SESSION_LOST.test(error.message)) {
      invalidateSession();
      await ensureSession();           // re-attach
      return rawCdp(method, params, state.sessionId);  // retry
    }
    throw error;  // non-recoverable errors propagate
  }
}

The original request is automatically re‑issued with the fresh session ID, completely transparent to the caller.

4. Proactive Cleanup on Detach Events

Even when CDP calls succeed, the runtime listens for lifecycle events that indicate session termination. In handleMessage() (lines 52‑66), notifications like Target.detachedFromTarget or Target.targetDestroyed trigger the same invalidateSession() cleanup, keeping internal state consistent before the next request attempts to use the dead session.

Practical Usage: Recovery Without Boilerplate

The recovery mechanism operates below the public API surface. Developers using helpers like navigate(), click(), or js() receive automatic protection without explicit handling:

// Navigation that survives a mid-flight session loss
await navigate('https://example.com');
await click('button#submit');  // Internally calls cdp() → browserCdp()
// JavaScript evaluation with transparent retry
const result = await js(`
  document.title;
`);
// If the session dies during evaluation, ego‑browser recovers and returns the result
// Low-level CDP access still benefits from the wrapper
const metrics = await browserCdp(
  'Performance.getMetrics',
  {},
  undefined,  // Let runtime resolve session
);

Key Source Files and Responsibilities

File Role Direct Link
src/browser-runtime.ts Core CDP transport, browserCdp() wrapper, ensureSession(), invalidateSession() browser-runtime.ts
src/state.ts Mutable runtime state: sessionId, sessionAt, dialog, target preferences state.ts
src/cdp-eval.ts High-level helpers cdp() and js() that delegate to browserCdp() cdp-eval.ts
src/ego-errors.ts Error normalization utilities for the ego bridge ego-errors.ts

Summary

  • Pattern-based detection: The SESSION_LOST regex in browser-runtime.ts identifies recoverable session errors across multiple message variants.
  • Atomic invalidation: invalidateSession() clears all stale state—including session ID, timestamps, and dialog state—to prevent reuse of dead identifiers.
  • Automatic re‑attachment: ensureSession() locates active targets, attaches fresh CDP sessions, and re-enables required domains.
  • Transparent retry: The browserCdp() wrapper catches session‑lost errors, triggers recovery, and re‑issues the original request without caller intervention.
  • Proactive maintenance: Detach and destroy events from the Target domain preemptively clean up state before errors occur.

Frequently Asked Questions

How does ego‑browser distinguish between recoverable session loss and permanent CDP errors?

ego‑browser uses the SESSION_LOST regular expression to match specific error message patterns like "Session … not found" or "Target closed". Errors that do not match this pattern—such as protocol syntax errors or domain-not-enabled failures—are re‑thrown immediately without retry. This selective handling prevents wasted re‑attachment attempts on genuinely unrecoverable conditions.

What happens if ensureSession() cannot find an active target to attach?

If no active tab exists and no preferred target is configured, ensureSession() will fail with a clear error after exhausting available targets. The caller receives this failure directly, as it indicates a browser state that automatic recovery cannot resolve (e.g., all tabs closed or browser process terminated).

Does the retry mechanism introduce race conditions with concurrent CDP calls?

The runtime maintains session state in a centralized state object. While invalidateSession() and ensureSession() are synchronous or awaited sequentially, concurrent callers during recovery may observe transient states. In practice, the browserCdp() wrapper's try-catch-retry structure serializes recovery per call, and the shared sessionId ensures subsequent calls use the fresh attachment.

Can I disable automatic session re‑attachment for debugging purposes?

The current implementation in citrolabs/ego-lite does not expose a configuration flag to disable recovery. The browserCdp() wrapper always applies the retry logic for SESSION_LOST matches. To observe raw errors, you would need to patch browser-runtime.ts locally or invoke rawCdp() directly, bypassing the wrapper entirely.

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 →