How the Ego Lite CDP Transport Layer Handles Session Management and Auto Re-Attach

The Ego Lite CDP transport layer caches sessions for 2 seconds, automatically creates new sessions on demand, and retries failed commands with fresh sessions when the underlying Chrome DevTools Protocol connection is lost.

The Chrome DevTools Protocol (CDP) transport layer in citrolabs/ego-lite provides browser automation agents with a resilient, self-healing connection to browser targets. This article examines how src/browser-runtime.ts implements session management and auto re-attach mechanisms that shield calling code from connection volatility.

Session Lifecycle and Caching Strategy

The runtime maintains session state through a time-bound caching strategy to minimize unnecessary re-attachments.

Session TTL and Cache Validation

Every session remains valid for 2 seconds (SESSION_TTL_MS = 2000 at line 5). The ensureSession() function checks two conditions before reusing a cached session:

  • state.sessionId exists
  • state.sessionAt timestamp is within the TTL window
// Simplified logic from lines 7-10 of browser-runtime.ts
if (state.sessionId && Date.now() - state.sessionAt < SESSION_TTL_MS) {
  return state.sessionId;  // Reuse fresh session
}

This short TTL balances connection stability against ** tab switch responsiveness** — agents benefit from connection reuse during rapid command sequences while automatically picking up navigation or tab changes.

Deduplicating Concurrent Session Creation

When multiple commands execute simultaneously without a valid session, ensureSession() prevents redundant Target.attachToTarget calls. The state.sessionInflight promise (lines 11-13) ensures all concurrent callers await a single session creation operation:

// Parallel callers share the same pending promise
if (state.sessionInflight) {
  return state.sessionInflight;  // Wait for in-progress creation
}
state.sessionInflight = createSession();  // Single creation attempt

Attaching to Browser Targets

When the cache misses, ensureSession() executes a full attachment flow (lines 14-36):

  1. List available tabs via browserEgo().listTabs()
  2. Select the preferred active tab from the returned array
  3. Compare against cached target — skip re-attachment if unchanged
  4. Call Target.attachToTarget for new or changed targets
  5. Store session ID and timestamp, clear sessionInflight

Enabling Page Events Per Session

After successful attachment, enablePageEvents(sessionId) (lines 38-45) ensures the runtime receives page-level CDP events. A pageEnabledSessions Set tracks which sessions have already been initialized, preventing duplicate Page.enable commands:

// From the source: enablePageEvents guards against duplicate enables
if (!pageEnabledSessions.has(sessionId)) {
  await rawCdp('Page.enable', {}, sessionId);
  pageEnabledSessions.add(sessionId);
}

Automatic Re-Attach on Session Loss

The browserCdp() wrapper function implements transparent recovery from session failures without requiring caller intervention.

Detecting Session-Lost Conditions

The runtime matches error messages against the SESSION_LOST regex pattern (lines 9-10) to identify when the browser has detached or destroyed the target. Common triggers include:

  • Target crashed or was closed
  • Navigation to a different origin (in some Chrome configurations)
  • Browser process restart

Retry Logic Implementation

When browserCdp() encounters a session-lost error and no explicit session ID was provided, it automatically recovers (lines 95-102):

// Simplified from browser-runtime.ts lines 95-102
async function browserCdp(method, params, explicitSessionId) {
  try {
    return await rawCdp(method, params, explicitSessionId ?? state.sessionId);
  } catch (err) {
    if (!explicitSessionId && SESSION_LOST.test(err.message)) {
      invalidateSession();           // Clear stale state
      await ensureSession();         // Fresh attachment
      return browserCdp(method, params);  // Retry once
    }
    throw err;  // Propagate non-recoverable errors
  }
}

Critical distinction: Explicit session IDs bypass auto-retry. This allows callers who manage their own sessions to handle failures according to their own logic.

Explicit Session Invalidation

The runtime exposes invalidateSession() for cases requiring immediate session refresh, such as:

  • Known navigation events
  • Target switching
  • Recovering from extended operation timeouts
// Force immediate session invalidation
await invalidateSession();                   // Clears state.sessionId
const fresh = await ensureSession();         // Guaranteed new attachment
await browserCdp('Page.reload', {}, fresh);   // Execute on fresh session

Reactive Invalidation via CDP Events

The runtime also listens for browser-initiated session termination. When Target.detachedFromTarget or Target.targetDestroyed events arrive for the active target, invalidateSession() executes automatically (lines 52-66):

  • Clears state.sessionId and state.sessionAt
  • Removes session from pageEnabledSessions
  • Clears pendingDialogs buffer

This proactive cleanup prevents the 2-second TTL window from masking target destruction events.

Practical Usage Patterns

Basic Automatic Mode

For most automation scenarios, rely on the default behavior — no session management required:

// Runtime handles session creation and recovery automatically
const result = await browserCdp('Runtime.evaluate', {
  expression: 'document.title'
});
// Returns: { result: { type: 'string', value: 'Page Title' } }

Explicit Session Control

When managing multiple targets or implementing custom retry logic, obtain and pass session IDs explicitly:

// Acquire current session for reuse across multiple calls
const session = await ensureSession();

// All calls use the same session; no auto-retry on failure
await browserCdp('DOM.enable', {}, session);
await browserCdp('DOM.querySelector', { nodeId: 1, selector: 'h1' }, session);

// Caller handles session loss explicitly

Post-Navigation Recovery

After navigation that invalidates the target, force session refresh:

// Navigation likely destroyed previous target
await browserCdp('Page.navigate', { url: 'https://example.com' });
await invalidateSession();  // Clear potentially stale session

// Subsequent calls attach to new target automatically
const metrics = await browserCdp('Performance.getMetrics');

Architecture: Key Files and Responsibilities

File Role in Session Management
src/browser-runtime.ts Core implementation: ensureSession(), invalidateSession(), browserCdp(), event listeners, and state mutations
src/cdp-eval.ts Public API surface exposing browserCdp to agent scripts
src/state.ts Mutable state container: sessionId, sessionAt, sessionInflight, pageEnabledSessions
src/helpers.ts Context binding exposing session helpers to evaluated agent code

Summary

  • Session TTL: 2-second cache window minimizes re-attachment overhead while responding to tab changes
  • Concurrent deduplication: sessionInflight promise prevents redundant session creation under load
  • Automatic recovery: browserCdp() detects session-loss errors and retries once with fresh attachment — unless caller supplies explicit session ID
  • Event-driven cleanup: CDP Target.* events trigger immediate invalidation, keeping state synchronized with browser reality
  • Explicit control: ensureSession() and invalidateSession() APIs support advanced use cases requiring manual session management

Frequently Asked Questions

How long does Ego Lite cache a CDP session?

The cache duration is 2 seconds (SESSION_TTL_MS = 2000). After this window expires, ensureSession() creates a new attachment even if the previous session ID remains technically valid in the browser.

Can I disable automatic re-attach behavior?

Yes. Pass an explicit sessionId to browserCdp() — this bypasses the retry logic entirely. All session management becomes your responsibility, including detection and recovery from SESSION_LOST errors.

What happens when multiple commands run without a cached session?

Concurrent callers automatically share a single session creation operation through the sessionInflight promise mechanism. Only one Target.attachToTarget call executes; all waiting callers receive the same session ID.

When should I call invalidateSession() manually?

Call it after operations known to destroy or replace the target — particularly cross-origin navigation or tab closure/replacement. Also use it when implementing custom recovery logic that must bypass the 2-second TTL window.

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 →