How ego-browser Manages Session Persistence and Reattachment in CDP

ego-browser implements a "cached-with-TTL" session model that automatically reattaches to Chrome DevTools Protocol (CDP) targets when connections drop, using a 2-second freshness window and transparent retry logic.

The ego-browser runtime, hosted within the citrolabs/ego-lite framework, maintains persistent Chrome DevTools Protocol sessions across intermittent agent script executions. Unlike standard CDP clients that assume continuous connectivity, ego-browser implements sophisticated ego-browser session persistence and reattachment logic to handle idle periods, target crashes, and navigation events without manual intervention.

Session Lifecycle and Freshness Windows

The runtime defines strict temporal boundaries for session validity in package/ego-browser/src/browser-runtime.ts. These constants govern how long a session ID remains usable before forced revalidation:

const RESPONSE_TIMEOUT_MS = 15000;   // CDP request timeout
const SESSION_TTL_MS      = 2000;    // "freshness" window for a cached session
const MAX_BUFFERED_EVENTS = 10000;   // Upper limit for the internal event buffer

The SESSION_TTL_MS (2 seconds) represents the maximum age of a cached state.sessionId. When an agent script executes after this window, the runtime automatically discards the stale identifier and negotiates a new attachment. This prevents the use of expired sessions that Chrome may have garbage collected during idle periods.

(📂 browser-runtime.ts L4‑L6)

Lazy Session Initialization via ensureSession()

All high-level CDP operations route through ensureSession() unless the caller provides an explicit sessionId or invokes a browser-level command (Target.* or Browser.*). This function implements a three-tier caching strategy to minimize redundant attachments.

Cache Checks and Promise Sharing

The implementation first validates cached state, then deduplicates concurrent attachment attempts:

export async function ensureSession() {
  // 1️⃣ Return cached session if still recent
  if (state.sessionId && Date.now() - state.sessionAt < SESSION_TTL_MS) {
    return state.sessionId;
  }

  // 2️⃣ If another call is already inflight, share its promise
  if (state.sessionInflight) {
    return state.sessionInflight;
  }

  // 3️⃣ Otherwise create a new session (see below)
  state.sessionInflight = (async () => {
    // …attach logic …
  })();

  return state.sessionInflight;
}

(📂 browser-runtime.ts L7‑L15)

Attaching to Chrome Targets

When cache misses occur, the runtime queries the host process via ego.listTabs() to identify available targets. It prioritizes user-specified tabs over the active tab, then establishes a new CDP session via Target.attachToTarget:

const result = assertNoEgoError(await browserEgo().listTabs());
const tabs = result?.tabs || result?.targetInfos || [];
const preferred = state.preferredTargetId
  ? tabs.find((t) => t.targetId === state.preferredTargetId)
  : null;
const active = preferred || tabs.find((t) => t.active) || tabs[tabs.length - 1];
const targetId = active.targetId;

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;
}

(📂 browser-runtime.ts L16‑L35)

Enabling Page Events

After successful attachment, the runtime enables the Page domain to receive events like dialogs and screencasts. This occurs exactly once per session to avoid protocol errors:

await enablePageEvents(state.sessionId);
state.sessionAt = Date.now();   // Reset the "freshness" timestamp
return state.sessionId;

(📂 browser-runtime.ts L36‑L44)

Automatic Reattachment on Session Loss

When CDP requests fail, browserCdp() inspects error messages against the SESSION_LOST regular expression. If the error indicates a detached session and the call wasn't explicitly targeting a specific session, the runtime executes transparent recovery.

The Recovery Loop

The error handler invalidates stale state and retries exactly once:

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

(📂 browser-runtime.ts L95‑L103)

Invalidating Session State

The invalidateSession() function clears all cached identifiers and cleans up associated memory:

export function invalidateSession() {
  if (state.sessionId) {
    pageEnabledSessions.delete(state.sessionId);
    pendingDialogs.delete(state.sessionId);
  }
  state.sessionId = null;
  state.sessionTargetId = null;
  state.sessionAt = 0;
}

(📂 browser-runtime.ts L46‑L54)

Reactive Cleanup via CDP Events

The runtime listens for Target.detachedFromTarget and Target.targetDestroyed events. When these target the currently attached session, invalidateSession() triggers immediately, preventing the reuse of dead connections:

if (
  data.method === "Target.detachedFromTarget" ||
  data.method === "Target.targetDestroyed"
) {
  // …snip…
  if (targetId && targetId === state.sessionTargetId) {
    invalidateSession();
  }
}

(📂 browser-runtime.ts L52‑L64)

State Management Architecture

All session metadata resides in a singleton state object exported from package/ego-browser/src/state.ts. This centralized store contains sessionId, sessionTargetId, sessionAt, and sessionInflight flags that coordinate attachment across asynchronous operations.

// Conceptual structure from state.ts
export const state = {
  sessionId: null,       // Current CDP session identifier
  sessionTargetId: null, // Chrome target ID
  sessionAt: 0,          // Timestamp of last attachment
  sessionInflight: null  // Promise for deduplication
};

(📂 state.ts L24‑L38)

Practical Implementation Examples

Automatic Session Handling

Standard page-level CDP calls require no manual session management:

// Inside an ego-browser script
await page.waitForSelector('button#submit');   // Triggers ensureSession()
await cdp('Runtime.evaluate', { expression: 'document.title' });

Forcing Reattachment After Navigation

To handle domain changes or tab switches, manually invalidate the cache:

// Force a fresh attachment
await invalidateSession();      // Clears cached session
await ensureSession();          // Re-attaches to current active tab

Handling Persistent Failures

Custom logic can distinguish between transient and permanent CDP errors:

try {
  const result = await cdp('DOM.getDocument');
  console.log('DOM root:', result.root.nodeId);
} catch (e) {
  // Runtime already retried once; failure here indicates genuine protocol error
  console.error('Unable to talk to CDP:', e);
}

Target Selection Across Multiple Tabs

When multiple tabs are open, pin the session to a specific target:

setPreferredTarget('target-id-of-your-tab'); // Stored in state.preferredTargetId

Summary

  • Time-bound caching: Sessions remain valid for SESSION_TTL_MS (2 seconds) before requiring revalidation.
  • Lazy attachment: ensureSession() creates connections only when needed, using promise deduplication to prevent race conditions.
  • Transparent recovery: The runtime detects session loss via error message inspection and automatically reattaches once before surfacing errors.
  • Event-driven invalidation: CDP lifecycle events trigger immediate cache clearing, ensuring no stale session IDs persist after target destruction.
  • Centralized state: The state singleton in state.ts coordinates all session metadata across the browser runtime.

Frequently Asked Questions

How long does ego-browser cache a CDP session before requiring reattachment?

The runtime caches session IDs for 2 seconds (SESSION_TTL_MS). After this freshness window expires, the next CDP request triggers ensureSession() to validate or recreate the connection. This TTL balances performance against Chrome's tendency to garbage collect idle sessions.

What happens when a CDP session drops unexpectedly?

When browserCdp() detects a session-lost error (matching the SESSION_LOST regex), it automatically calls invalidateSession() to clear cached state, invokes ensureSession() to negotiate a new attachment, and retries the original command exactly once. This transparent recovery happens without throwing errors to the caller unless the reattachment also fails.

How does ego-browser select which Chrome tab to attach to?

The ensureSession() implementation queries available targets via ego.listTabs(), then applies a selection hierarchy: first checking for a user-defined preferredTargetId, then falling back to the active tab, and finally defaulting to the last tab in the list. If the chosen targetId differs from the cached sessionTargetId, the runtime creates a fresh CDP session via Target.attachToTarget.

Where does ego-browser store session state?

All session identifiers, timestamps, and inflight promises reside in a mutable singleton exported from src/state.ts. This shared state object includes sessionId, sessionTargetId, sessionAt, and sessionInflight fields that persist across function calls but remain scoped to the browser runtime instance.

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 →