# How the CDP Transport and Session Caching Work in browser-runtime.ts

> Explore the CDP transport in browser-runtime.ts. Learn how it combines JSON-RPC with session caching and TTL for efficient Chrome DevTools Protocol communication and recovery.

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

---

**The [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts) module implements a low-level Chrome DevTools Protocol (CDP) transport that combines JSON-RPC request/response handling with automatic session caching, using a time-to-live (TTL) mechanism and transparent retry logic for session loss recovery.**

In the `citrolabs/ego-lite` repository, the browser runtime serves as the bridge between agent scripts and the embedded browser. Understanding how the CDP transport and session caching work in [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts) is essential for debugging automation flows or extending the harness with custom CDP commands.

## The Three-Stage CDP Transport Flow

The transport mechanism operates through a coordinated dance of message dispatch, promise resolution, and event routing. The architecture separates concerns between raw message transmission and high-level session management.

### Sending Requests via rawCdp()

When agent code initiates a CDP call, the `rawCdp()` function constructs the wire format. Located at lines 38–76 in [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts), this method assembles a JSON payload containing four critical fields:

- A unique numeric `id` generated for correlation
- The CDP `method` string (e.g., `"Page.getViewport"`)
- Optional `params` for the command arguments
- An optional `sessionId` for target-scoped execution

The function registers a deferred promise in an internal `pending` Map, keyed by the request ID. It then initiates a `RESPONSE_TIMEOUT_MS` timer to prevent indefinite hanging. Finally, the payload passes to `globalThis.ego.sendCDPMessage`, which interfaces with the embedded browser's native message channel.

```typescript
// Conceptual flow inside rawCdp()
const payload = {
  id: generateMessageId(),
  method: "Runtime.evaluate",
  params: { expression: "window.location.href" },
  sessionId: currentSessionId
};

pending.set(payload.id, { resolve, reject, timer });
globalThis.ego.sendCDPMessage(JSON.stringify(payload));

```

### Receiving Responses via handleMessage()

Incoming messages arrive asynchronously through `handleMessage()` (lines 32–107 in the same file). This parser distinguishes between command responses and spontaneous events.

For messages containing an `id` property, the function retrieves the matching entry from the `pending` Map. If the payload includes an `error` field, it rejects the promise with a wrapped `EgoError`. Otherwise, it resolves with the `result` object and clears the timeout timer.

Messages lacking an `id` represent CDP events (such as `Runtime.consoleAPICalled`). These route to subscribers, buffer for late-joining listeners, and may trigger registered waiters for specific event conditions.

## Session Caching and Lifecycle Management

Session management introduces resilience against target detachment while optimizing performance by avoiding redundant attachment operations.

### The ensureSession() Mechanism

The `ensureSession()` function (lines 7–45 in [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts)) implements a TTL-based cache backed by the global `state` object defined in [`state.ts`](https://github.com/citrolabs/ego-lite/blob/main/state.ts). Before issuing commands requiring a page context, the function validates the cached `state.sessionId`.

The validation checks two conditions:

1. **Existence**: Whether `state.sessionId` holds a non-null value
2. **Freshness**: Whether `Date.now() - state.sessionAt` exceeds `SESSION_TTL_MS` (set to 2 seconds)

If either check fails, the function initiates a new attachment sequence by listing tabs via `Target.getTargets`, selecting the active tab (or preferred target), and invoking `Target.attachToTarget`. The resulting session ID updates `state.sessionId` while `state.sessionAt` receives the current timestamp.

To prevent race conditions during concurrent attachment attempts, inflight promises store temporarily in `state.sessionInflight`, allowing parallel callers to await the same attachment operation rather than spawning duplicate requests.

### Automatic Session Injection in browserCdp()

The `browserCdp()` wrapper (lines 85–104) analyzes method names to determine scope. Browser-level methods (such as `Target.getTargets`) execute without a session context. For page-level methods, when the caller omits an explicit `sessionId`, the wrapper automatically invokes `ensureSession()` and injects the cached ID into the request payload via an `explicit` flag check.

This abstraction allows agent scripts to remain agnostic to session lifecycle details while ensuring every page-scoped command executes within a valid target context.

### Session Loss Recovery

Network volatility or target destruction can invalidate cached sessions. The `invalidateSession()` function clears `state.sessionId` and removes entries from `pageEnabledSessions` when detection occurs.

Detection happens through two pathways:

1. **Explicit error parsing**: When `browserCdp()` receives a response error matching the `SESSION_LOST` pattern, it triggers invalidation and automatic retry
2. **Event listeners**: `handleMessage()` watches for `Target.detachedFromTarget` and `Target.targetDestroyed` events, calling `invalidateSession()` proactively

The retry mechanism wraps the original request, fetches a fresh session via `ensureSession()`, and retransmits transparently to the caller.

## State Management and TTL

The session cache relies on [`state.ts`](https://github.com/citrolabs/ego-lite/blob/main/state.ts) for shared mutable storage. Key fields include:

- `state.sessionId`: The active CDP session identifier string
- `state.sessionAt`: Unix timestamp marking the last attachment time
- `state.sessionInflight`: Promise reference for active attachment operations

The 2-second TTL (`SESSION_TTL_MS`) balances performance against stability. Short durations detect stale sessions quickly, while the async `sessionInflight` deduplication prevents thundering-herd scenarios during high-frequency command bursts.

## Practical Usage Examples

The following patterns demonstrate correct usage of the CDP transport layer:

```typescript
// Example 1 – Automatic session injection
import { browserCdp } from "./browser-runtime.js";

async function getViewport() {
  // Session is injected automatically; no sessionId required
  const resp = await browserCdp("Page.getViewport");
  console.log("Viewport:", resp.result);
}

```

```typescript
// Example 2 – Browser-level commands (no session required)
async function enumerateTargets() {
  const resp = await browserCdp("Target.getTargets");
  return resp.result.targetInfos;
}

```

```typescript
// Example 3 – Transparent session recovery
async function robustClick(selector: string) {
  try {
    // If session invalidates mid-call, retry happens automatically
    await browserCdp("Runtime.evaluate", {
      expression: `document.querySelector("${selector}").click()`
    });
  } catch (e) {
    console.error("Permanent failure after retry:", e);
  }
}

```

## Summary

- **`rawCdp()`** handles JSON-RPC serialization, promise registration, and native message dispatch with timeout protection.
- **`handleMessage()`** demultiplexes responses by ID, routes events to subscribers, and manages error translation to `EgoError` instances.
- **`ensureSession()`** validates cached sessions against a 2-second TTL, deduplicates concurrent attachment attempts via `sessionInflight`, and executes `Target.attachToTarget` when necessary.
- **`browserCdp()`** automatically injects session IDs for page-scoped methods and implements transparent retry logic for `SESSION_LOST` errors.
- **Invalidation** occurs on explicit error detection or lifecycle events (`Target.detachedFromTarget`), clearing state to force re-attachment on subsequent calls.

## Frequently Asked Questions

### How does the session cache prevent duplicate attachment requests?

The `ensureSession()` function checks `state.sessionInflight` before initiating attachment. If a promise exists, concurrent callers await the same promise rather than spawning duplicate `Target.attachToTarget` requests. Once resolved, all callers receive the same session ID.

### What triggers automatic session invalidation?

Two conditions invalidate the cached session: receiving a CDP error message matching the `SESSION_LOST` pattern during `browserCdp()` execution, or processing `Target.detachedFromTarget`/`Target.targetDestroyed` events in `handleMessage()`. Both pathways invoke `invalidateSession()` to clear `state.sessionId`.

### Why is the session TTL set to 2 seconds?

The `SESSION_TTL_MS` constant (2 seconds) provides a balance between performance and consistency. It allows rapid reuse of valid sessions for command bursts while ensuring stale identifiers are discarded quickly if the underlying target context becomes unstable.

### Can I use rawCdp() without automatic session management?

Yes. The `rawCdp()` function accepts an explicit `sessionId` parameter. When provided, `browserCdp()` passes this directly without calling `ensureSession()`, allowing manual control over session scoping for advanced use cases requiring specific target attachments.