How ego-browser's CDP Session Management Handles Tab Switching and Reconnection

ego-browser automates Chrome DevTools Protocol (CDP) session management through a self-healing runtime that transparently tracks active tabs, re-attaches to switched tabs, and silently recovers from session losses without manual intervention.

The ego-browser (part of the citrolabs/ego-lite repository) implements a sophisticated CDP session management layer in src/browser-runtime.ts. This lightweight runtime lives inside the closed-source ego lite browser and ensures that all CDP calls execute against the correct tab—even when users switch tabs, tabs crash, or network connections drop.

Session Initialization via ensureSession()

Every CDP request flows through browserCdp(). When no explicit sessionId is provided and the method isn't browser-level (Target.* or Browser.*), the runtime calls ensureSession() to locate or create a valid session.

As implemented in src/browser-runtime.ts:

export async function ensureSession() {
  // reuse a fresh session if its TTL (2 s) has not expired
  if (state.sessionId && Date.now() - state.sessionAt < SESSION_TTL_MS) {
    return state.sessionId;
  }

  // avoid parallel work – only one inflight attach at a time
  if (state.sessionInflight) return state.sessionInflight;

  state.sessionInflight = (async () => {
    const result = assertNoEgoError(await browserEgo().listTabs());
    const tabs = result?.tabs || result?.targetInfos || [];

    // ① If the user (or an agent) has designated a preferred tab,
    //    `setPreferredTarget()` stores its targetId in `state.preferredTargetId`.
    const preferred = state.preferredTargetId
      ? tabs.find(t => t.targetId === state.preferredTargetId)
      : null;

    // ② Otherwise pick the active tab, falling back to the last tab.
    const active = preferred || tabs.find(t => t.active) || tabs[tabs.length - 1];
    if (!active) throw new Error("no active tab to attach session");

    const targetId = active.targetId;

    // If the tab changed, attach a new CDP session.
    if (targetId !== state.sessionTargetId || !state.sessionId) {
      const attached = await rawCdp(
        "Target.attachToTarget",
        { targetId, flatten: true }, undefined);
      state.sessionId = attached.result?.sessionId || attached.sessionId;
      state.sessionTargetId = targetId;
    }

    // Enable page-level events (e.g. dialogs) for the new session.
    await enablePageEvents(state.sessionId);
    state.sessionAt = Date.now();
    return state.sessionId;
  })();

  return state.sessionInflight;
}

The function implements three key behaviors:

  • TTL-based caching – Sessions remain valid for 2 seconds (SESSION_TTL_MS = 2000) to avoid excessive attach/detach cycles
  • DeduplicationsessionInflight prevents multiple concurrent attachment attempts
  • Automatic target selection – Falls back from preferred → active → last tab

Explicit Tab Switching with setPreferredTarget()

Agents control CDP session targeting through two state mutators exposed in src/browser-runtime.ts:

export function setPreferredTarget(targetId) {
  state.preferredTargetId = targetId || null;
}
export function clearPreferredTarget() {
  state.preferredTargetId = null;
}

When an agent needs to execute commands on a specific tab, it calls setPreferredTarget(targetId). The next ensureSession() invocation:

  1. Queries available tabs via ego.listTabs()
  2. Locates the matching targetId in the returned array
  3. Attaches a fresh session via Target.attachToTarget

Higher-level drivers in src/driver/nav.ts use this API for task-space management. When claiming a task space, the driver pins the session to that tab's targetId so subsequent helper calls remain isolated.

Example usage:

// Pin CDP operations to a specific tab
await setPreferredTarget('target-1234');
await browserCdp('Page.navigate', { url: 'https://example.com' });

Silent Reconnection on Session Loss

The runtime detects session-lost errors through a precise RegExp pattern defined in src/browser-runtime.ts:

const SESSION_LOST = /Session (?:with given id )?not found|Target closed|No session/i;

When browserCdp() encounters such an error on an implicit session call, it executes a three-phase recovery:

catch (error) {
  const lost = SESSION_LOST.test(error?.message || "");
  if (lost && !explicit && !BROWSER_LEVEL(method)) {
    invalidateSession();               // ⟹ clears pageEnabledSessions, pendingDialogs, …
    const fresh = await ensureSession();
    return rawCdp(method, params, fresh, timeoutMs); // retry
  }
  throw error;
}

The invalidateSession() function (also in src/browser-runtime.ts) clears all cached state:

  • state.sessionId
  • state.sessionTargetId
  • Page event subscriptions
  • Pending dialog state

This allows transparent recovery from crashes, target closures, or user-initiated tab changes without propagating errors to calling code.

Event Handling and Automatic Cleanup

The runtime listens for CDP lifecycle events and automatically invalidates stale sessions. When handleMessage() receives Target.detachedFromTarget or Target.targetDestroyed, it triggers invalidateSession() to prevent reuse of dead session IDs.

Page-level events are buffered in a capped array (MAX_BUFFERED_EVENTS = 10000) accessible via drainBrowserEvents(). This decouples event consumption from session attachment, ensuring no events are lost during reconnection.

State Management Architecture

Session state lives in src/state.ts as a global mutable singleton:

State Key Purpose
sessionId Active CDP session identifier
sessionTargetId targetId the session is attached to
preferredTargetId User/agent-override for tab selection
sessionAt Timestamp for TTL checking
sessionInflight Promise for deduplicating concurrent attaches

This centralized state enables consistent behavior across the browser-runtime module while remaining accessible to driver code.

Summary

  • ensureSession() – Lazily creates or reuses CDP sessions with 2-second TTL caching and automatic target selection
  • setPreferredTarget() – API for explicit tab pinning, used by task-space drivers in src/driver/nav.ts
  • Session loss detection – RegExp-based matching with automatic invalidateSession()ensureSession() → retry flow
  • Self-healing design – No manual session ID management required; crashes and tab switches recover transparently
  • Event safety – Capped buffering and automatic cleanup on detach events prevent state corruption

Frequently Asked Questions

How does ego-browser handle CDP session timeouts?

The runtime implements a 2-second TTL (SESSION_TTL_MS = 2000) in ensureSession(). If a cached session is younger than this threshold, it's reused; otherwise, fresh tab discovery and attachment occur. This prevents stale sessions while avoiding excessive Target.attachToTarget calls.

Can multiple concurrent CDP calls cause race conditions in session creation?

No. The state.sessionInflight promise acts as a deduplication mutex. All concurrent calls await the same in-flight attachment promise, ensuring exactly one Target.attachToTarget executes even under heavy parallel load.

What happens if the preferred target tab closes mid-operation?

The next CDP call fails with a session-lost error, triggering automatic recovery. invalidateSession() clears the cached sessionId and preferredTargetId, then ensureSession() falls back to the current active tab. Callers receive the successful retry result without handling the error explicitly.

How do I switch back from preferred-target mode to automatic tab selection?

Call clearPreferredTarget() to nullify state.preferredTargetId. Subsequent ensureSession() calls resume automatic selection via tabs.find(t => t.active) or the last available tab.

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 →