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

> Discover how ego-browser in ego-lite manages CDP sessions with automatic re-attachment via TTL caching and retry logic. Learn to maintain stable connections even after disconnections.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: internals
- Published: 2026-08-06

---

**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](https://github.com/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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:

```typescript
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`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L5-L11), 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:

```typescript
// 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:

```typescript
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`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L94-L103), 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:

```typescript
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`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L52-L64) |
| `Target.targetDestroyed` | Same cleanup + removes from `pageEnabledSessions` | [`browser-runtime.ts:59-64`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L52-L64) |

## State Management Across the Runtime

All session-related state lives in **[`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts)** as a mutable singleton:

```typescript
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`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L22-L30).

## Practical Examples

### Navigation with Transparent Session Handling

```typescript
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

```typescript
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

```typescript
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`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts)** | Core CDP wrapper, session lifecycle, automatic retry logic |
| **[`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts)** | Mutable singleton storing session identifiers and timestamps |
| **[`src/driver/nav.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/nav.ts)** | High-level navigation helpers (`goto`, `switchTab`) |
| **[`src/cdp-eval.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/cdp-eval.ts)** | CDP command helper with automatic session injection |

## Summary

- **ego-browser** centralizes CDP session management in [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L94-L103).

### 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`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/driver/nav.ts#L54-L60).

### Can multiple concurrent calls create duplicate sessions?

No. The `state.sessionInflight` field in [`state.ts`](https://github.com/citrolabs/ego-lite/blob/main/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.