How ego-browser Manages CDP Sessions and Re-attaches After Disconnection

ego-browser in the ego-lite runtime maintains a single lightweight CDP session per active tab with automatic re-attachment through TTL-based caching and transparent retry logic when sessions are lost.

The ego-browser package (part of citrolabs/ego-lite) provides a resilient wrapper around Chrome DevTools Protocol (CDP) for browser automation. Unlike raw CDP clients that require manual session management, ego-browser handles session lifecycle, expiration, and automatic reconnection transparently. This article examines how the runtime maintains CDP stability across navigation events, tab switches, and transient connection failures.

CDP Session Architecture Overview

ego-browser communicates with Chrome through the global ego.sendCDPMessage API. All session logic is centralized in src/browser-runtime.ts, which exposes two primary interfaces for other modules:

  • ensureSession() — returns a valid session ID, creating one if needed
  • browserCdp() — executes CDP commands with automatic session injection and retry

Higher-level helpers like goto and switchTab (in src/driver/nav.ts) never manage sessions directly. Instead, they rely on these core primitives to handle the complexity.

Session Lifecycle and TTL-Based Reuse

Creating and Reusing Sessions

The ensureSession() function implements a time-to-live (TTL) caching strategy with a short 2000ms window:

import { ensureSession } from "./browser-runtime.js";

const sessionId = await ensureSession(); // reuses if < 2s old, else re-attaches

Per the source in browser-runtime.ts:7-9, the function checks state.sessionId and state.sessionAt:

  • If cached and within TTL → returns immediately
  • If expired or missing → triggers fresh attachment

Attaching to a Target

When a new session is required, the runtime executes Target.attachToTarget via the low-level rawCdp helper:

// From browser-runtime.ts lines 27-35
const attachResult = await rawCdp("Target.attachToTarget", {
  targetId,
  flatten: true,
});
state.sessionId = attachResult.sessionId;
state.sessionTargetId = targetId;
state.sessionAt = Date.now();

After attachment, Page.enable is sent once per session (tracked in pageEnabledSessions set) to receive page-level events like dialogs and screencasts.

Why the 2-Second TTL Is Deliberately Short

The SESSION_TTL_MS = 2000 constant reflects a key design decision: frequent refresh is cheaper than stale detection. Because the underlying ego bridge can re-attach to the same target with negligible overhead, the short TTL ensures that any navigation, network hiccup, or tab change quickly triggers a fresh, valid session rather than attempting to use a potentially broken one.

Automatic Reconnection on Session Loss

The browserCdp() wrapper provides transparent resilience. Every CDP call flows through this function, which detects session-loss errors and automatically retries:

import { browserCdp } from "./browser-runtime.js";

// This call will re-attach and retry if session was lost
const result = await browserCdp("Runtime.evaluate", {
  expression: "document.title"
});

Per browser-runtime.ts:94-103, the retry logic triggers when:

  1. The request fails with a SESSION_LOST regex match
  2. The call was not explicitly targeted at a specific session (auto-detection mode)
  3. invalidateSession() clears the stale cache
  4. ensureSession() creates a fresh attachment
  5. The original CDP method is retried once

This guarantees that agents continue operating even after page reloads, DevTools disconnections, or target destruction.

Session Invalidation and Cleanup

Sessions are proactively invalidated in two scenarios. First, explicit navigation changes like tab switches call invalidateSession() to force a fresh attach:

import { switchTab } from "./driver/nav.js";

// Per nav.ts lines 54-60: invalidates old session, sets preferred target
await switchTab("target-abc123");

Second, CDP events automatically clear state:

Event Action Source
Target.detachedFromTarget Clears sessionId, sessionTargetId, pending dialogs browser-runtime.ts:52-58
Target.targetDestroyed Same cleanup + removes from pageEnabledSessions browser-runtime.ts:59-64

State Management Across the Runtime

All session-related state lives in src/state.ts as a mutable singleton:

export const state = {
  sessionId: null as string | null,         // Current CDP session ID
  sessionTargetId: null as string | null,   // Attached target ID
  sessionAt: 0,                             // Timestamp of last attach
  sessionInflight: null as Promise<string> | null, // Deduplicates concurrent attaches
  preferredTargetId: null as string | null, // Next attach target (from switchTab)
  // ... additional fields
};

The sessionInflight field prevents thundering-herd scenarios where multiple concurrent calls race to create a session—only one Target.attachToTarget executes, and all waiters receive the same result.

Error Handling and Task Lifecycle

If ego.sendCDPMessage itself fails (e.g., the user's task becomes inactive), the runtime's handleSendError rejects all pending requests with a unified EgoError. This prevents orphaned promises from hanging indefinitely, as implemented in browser-runtime.ts:22-30.

Practical Examples

import { goto } from "./driver/nav.js";

// Session created/reused automatically; no manual management needed
await goto("https://example.com");

Execution flow: gotobrowserCdp("Page.navigate")ensureSession()rawCdp with session auto-injected.

Force Reconnection After Target Change

import { invalidateSession, setPreferredTarget } from "./browser-runtime.js";

invalidateSession();
setPreferredTarget("target-xyz");
// Next CDP call will attach to the new target

Low-Level CDP with Guaranteed Delivery

import { browserCdp } from "./browser-runtime.js";

const { result } = await browserCdp("DOM.getDocument", { depth: 1 });
console.log(result.root.nodeId);

Even if the underlying session disappears mid-flight, this call succeeds after automatic re-attachment.

Key Implementation Files

File Purpose
src/browser-runtime.ts Core CDP wrapper, session lifecycle, automatic retry logic
src/state.ts Mutable singleton storing session identifiers and timestamps
src/driver/nav.ts High-level navigation helpers (goto, switchTab)
src/cdp-eval.ts CDP command helper with automatic session injection

Summary

  • ego-browser centralizes CDP session management in browser-runtime.ts with a 2-second TTL caching strategy that favors freshness over longevity
  • ensureSession() returns valid session IDs transparently, creating new attachments only when stale or missing
  • browserCdp() wraps all CDP calls with automatic retry on session loss, detecting failures via regex and re-attaching before retrying
  • Explicit invalidation via invalidateSession() and CDP events (detachedFromTarget, targetDestroyed) ensure clean state transitions
  • Higher-level helpers like goto and switchTab remain stateless, delegating all session complexity to the runtime layer
  • This architecture guarantees resilient browser automation across navigation, tab switches, and transient connection failures without agent-level intervention

Frequently Asked Questions

How does ego-browser detect when a CDP session is lost?

ego-browser detects session loss through regex matching on error messages in the browserCdp() wrapper. When a CDP call fails with an error matching the SESSION_LOST pattern and the call used automatic session detection (no explicit session ID provided), the runtime triggers invalidateSession() followed by ensureSession() and retries the original command once. This is implemented in browser-runtime.ts:94-103.

Why is the session TTL only 2 seconds instead of longer?

The SESSION_TTL_MS = 2000 value is deliberately conservative. Since the underlying ego bridge can re-attach to targets with negligible overhead, frequent refresh is safer than risking stale sessions. The short TTL ensures that navigation events, network hiccups, or tab switches quickly trigger fresh attachments rather than attempting operations on potentially invalid sessions.

What happens when I call switchTab in ego-browser?

switchTab explicitly invalidates the current session via invalidateSession(), sets a preferredTargetId in state, and returns. The next CDP call will detect the missing session, create a fresh attachment to the preferred target, and execute normally. This indirection prevents race conditions where the old session might receive commands intended for the new tab, as shown in nav.ts:54-60.

Can multiple concurrent calls create duplicate sessions?

No. The state.sessionInflight field in state.ts deduplicates concurrent attachment attempts. If ensureSession() is called while another attachment is in progress, all callers await the same in-flight promise and receive the identical session ID. This prevents the thundering-herd problem where multiple racing requests would otherwise create redundant CDP sessions.

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 →