# How ego-browser's CDP Session Management Handles Tab Switching and Reconnection

> Discover how ego-browser's CDP session management seamlessly handles tab switching and reconnection with a self-healing runtime. Learn more about its automated tab tracking and recovery.

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

---

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

```typescript
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** – `sessionInflight` prevents 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`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts):

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

1. Queries available tabs via `ego.listTabs()`
2. Locates the matching `targetId` in the returned array
3. Attaches a fresh session via `Target.attachToTarget`

Higher-level drivers in [`src/driver/nav.ts`](https://github.com/citrolabs/ego-lite/blob/main/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:

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

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

```typescript
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`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts)) clears all cached state:

- `state.sessionId`
- `state.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`](https://github.com/citrolabs/ego-lite/blob/main/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 selection
- **`setPreferredTarget()`** – API for explicit tab pinning, used by task-space drivers in [`src/driver/nav.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/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.