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 - Deduplication –
sessionInflightprevents 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:
- Queries available tabs via
ego.listTabs() - Locates the matching
targetIdin the returned array - 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.sessionIdstate.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 selectionsetPreferredTarget()– API for explicit tab pinning, used by task-space drivers insrc/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →