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 neededbrowserCdp()— 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:
- The request fails with a
SESSION_LOSTregex match - The call was not explicitly targeted at a specific session (auto-detection mode)
invalidateSession()clears the stale cacheensureSession()creates a fresh attachment- 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
Navigation with Transparent Session Handling
import { goto } from "./driver/nav.js";
// Session created/reused automatically; no manual management needed
await goto("https://example.com");
Execution flow: goto → browserCdp("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.tswith a 2-second TTL caching strategy that favors freshness over longevity ensureSession()returns valid session IDs transparently, creating new attachments only when stale or missingbrowserCdp()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
gotoandswitchTabremain 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →