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

The 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 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, 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.

// 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) implements a TTL-based cache backed by the global state object defined in 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 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:

// 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);
}
// Example 2 – Browser-level commands (no session required)
async function enumerateTargets() {
  const resp = await browserCdp("Target.getTargets");
  return resp.result.targetInfos;
}
// 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →