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
statesingleton instate.tscoordinates 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →