aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoramankumarsingh77 <amankumarsingh7702@gmail.com>2026-07-28 20:25:38 +0530
committerAman Kumar <145339995+amankumarsingh77@users.noreply.github.com>2026-08-02 19:12:46 +0530
commit330767d862458386e804bde240a1f0d7835b9b8e (patch)
tree13a5f2086d265959c1bfa10bb88641a6804fd1f5
parent12d7a38ba0f1b1e4e301fce81e22f4b93036d211 (diff)
refactor(cdp): strip comment noise
Removes module banners, section dividers, and comments restating the code. Keeps one-line notes for the window.open/browserContextId constraint, the seed-tab claim, the unverifiable-pin reuse rule, the SeedTargetProvider rationale, the deliberate dialog retention, the discovery-time browser quirks, and the guarded CDP decoder assertion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-rw-r--r--src/cdp/attach.ts1
-rw-r--r--src/cdp/commands.ts12
-rw-r--r--src/cdp/console-buffer.ts34
-rw-r--r--src/cdp/daemon-transport.ts22
-rw-r--r--src/cdp/discovery.ts65
-rw-r--r--src/cdp/event-queue.ts39
-rw-r--r--src/cdp/network-buffer.ts29
-rw-r--r--src/cdp/ownership.ts16
-rw-r--r--src/cdp/session.ts81
-rw-r--r--src/cdp/target-factory.ts69
-rw-r--r--src/cdp/transport.ts16
-rw-r--r--src/cdp/window.ts1
12 files changed, 16 insertions, 369 deletions
diff --git a/src/cdp/attach.ts b/src/cdp/attach.ts
index 9be539d..c1b101b 100644
--- a/src/cdp/attach.ts
+++ b/src/cdp/attach.ts
@@ -2,7 +2,6 @@ import { map, type Result } from "../util/result";
import type { CdpError } from "./errors";
import type { CdpSession } from "./session";
-/** Target.attachToTarget, unwrapped to the new session id. */
export const attachTo = async (
session: CdpSession,
targetId: string,
diff --git a/src/cdp/commands.ts b/src/cdp/commands.ts
index e6d744d..580f8cf 100644
--- a/src/cdp/commands.ts
+++ b/src/cdp/commands.ts
@@ -78,7 +78,6 @@ const AxNode = Type.Object(
export type AxNodeResult = Static<typeof AxNode>;
export const COMMANDS = {
- // --- Target ---
"Target.attachToTarget": cmd(
Type.Object({ targetId: Type.String(), flatten: Type.Optional(Type.Boolean()) }),
Type.Object({ sessionId: Type.String() }),
@@ -112,7 +111,6 @@ export const COMMANDS = {
),
"Target.setDiscoverTargets": cmd(Type.Object({ discover: Type.Boolean() }), Empty),
- // --- Browser ---
"Browser.getWindowForTarget": cmd(
Type.Object({ targetId: Type.Optional(Type.String()) }),
Type.Object({ windowId: Type.Number(), bounds: Type.Optional(Type.Unknown()) }),
@@ -126,7 +124,6 @@ export const COMMANDS = {
Empty,
),
- // --- Page ---
"Page.enable": cmd(Type.Object({}), Empty),
"Page.bringToFront": cmd(Type.Object({}), Empty),
"Page.navigate": cmd(
@@ -163,7 +160,6 @@ export const COMMANDS = {
Empty,
),
- // --- Runtime ---
"Runtime.evaluate": cmd(
Type.Object(
{
@@ -192,7 +188,6 @@ export const COMMANDS = {
),
"Runtime.releaseObject": cmd(Type.Object({ objectId: Type.String() }), Empty),
- // --- DOM ---
"DOM.getDocument": cmd(
Type.Object({ depth: Type.Optional(Type.Number()), pierce: Type.Optional(Type.Boolean()) }),
Type.Object({ root: Type.Object({ nodeId: Type.Number() }, { additionalProperties: true }) }),
@@ -224,13 +219,11 @@ export const COMMANDS = {
Empty,
),
- // --- Input ---
"Input.dispatchMouseEvent": cmd(Type.Object({}, { additionalProperties: true }), Empty),
"Input.dispatchKeyEvent": cmd(Type.Object({}, { additionalProperties: true }), Empty),
"Input.dispatchDragEvent": cmd(Type.Object({}, { additionalProperties: true }), Empty),
"Input.insertText": cmd(Type.Object({ text: Type.String() }), Empty),
- // --- Network / Emulation / Accessibility ---
"Network.getResponseBody": cmd(
Type.Object({ requestId: Type.String() }),
Type.Object({ body: Type.String(), base64Encoded: Type.Optional(Type.Boolean()) }),
@@ -258,9 +251,6 @@ export const decodeResult = <M extends CdpMethod>(
const why = spec.validate.Errors(raw).map((e) => e.message).join("; ");
return err(cdpError("invalid_response", why, method));
}
- // Guarded assertion — see Verified Fact 4. The Check above proves `raw`
- // matches COMMANDS[method].result; TS cannot correlate a generic key to its
- // mapped value, so this carries what the check established. The only
- // assertion in the CDP layer; do not add another.
+ // Guarded assertion: the check above proves `raw` matches COMMANDS[method].result, which TS cannot correlate from a generic key.
return ok(raw as ResultOf<M>);
};
diff --git a/src/cdp/console-buffer.ts b/src/cdp/console-buffer.ts
index f42312f..c38f85e 100644
--- a/src/cdp/console-buffer.ts
+++ b/src/cdp/console-buffer.ts
@@ -1,20 +1,3 @@
-/**
- * In-memory aggregator for CDP Runtime.consoleAPICalled and Log.entryAdded events.
- *
- * Mirrors network-buffer.ts: the single CDP event loop in session.ts hands raw
- * params to ingestConsoleApi / ingestLogEntry; tools later read records via drain().
- *
- * Bounded by capacity (default 500). On overflow the oldest record is evicted FIFO,
- * and the overflow flag is reported once per drain so tools can warn the LLM that
- * data may be missing.
- *
- * Each record carries a monotonic `seq`. Tools expose the last drained seq as
- * `nextCursor`; callers pass it back as `sinceSeq` to read only what's new since
- * the previous drain. This is the cursor pattern that makes "what did this action
- * cause?" answerable in one tool call.
- *
- * Joining/filtering logic stays trivially testable (no transport dependency).
- */
import { decodeEvent } from "./events";
@@ -22,35 +5,26 @@ export type ConsoleLevel = "log" | "info" | "warn" | "error" | "debug";
export type ConsoleRecord = {
seq: number;
- /** Wall-clock ms when the event was ingested, in Date.now() units. */
timestampMs: number;
level: ConsoleLevel;
- /** Joined args (console-api) or the entry text (log-entry). Per-arg truncated. */
text: string;
source: "console-api" | "log-entry";
url?: string;
lineNumber?: number;
- /** Top frames only, joined with newlines. Populated for error/warn when CDP provides one. */
stackTrace?: string;
};
export type ConsoleFilter = {
levels?: ConsoleLevel[];
- /** Substring match by default. Wrap in slashes (`/foo/`) for regex. */
textPattern?: string;
- /** Only records with seq strictly greater than this. */
sinceSeq?: number;
- /** Only records with timestamp at most N ms ago. */
sinceMs?: number;
- /** Cap on returned records (default 50, max 500). */
limit?: number;
};
export type ConsoleDrainResult = {
readonly records: ConsoleRecord[];
- /** Total matches before `limit` was applied. */
readonly total: number;
- /** True if the ring buffer evicted at least one record since the last drain. */
readonly bufferOverflowed: boolean;
};
@@ -61,9 +35,7 @@ export type ConsoleBuffer = {
clear(): void;
};
-/** Per-arg cap to stop a single console.log(hugeBlob) from blowing the buffer. */
const PER_ARG_CAP = 2000;
-/** Max stack frames preserved per record. */
const MAX_STACK_FRAMES = 3;
const compileTextMatcher = (pattern: string): ((text: string) => boolean) => {
@@ -72,7 +44,6 @@ const compileTextMatcher = (pattern: string): ((text: string) => boolean) => {
const re = new RegExp(pattern.slice(1, -1));
return (text) => re.test(text);
} catch {
- // Fall through to substring match if the pattern is not a valid regex.
}
}
return (text) => text.includes(pattern);
@@ -81,7 +52,6 @@ const compileTextMatcher = (pattern: string): ((text: string) => boolean) => {
const truncateArg = (s: string): string =>
s.length > PER_ARG_CAP ? s.slice(0, PER_ARG_CAP) + "…" : s;
-/** Best-effort stringification of one Runtime.RemoteObject. */
const stringifyRemoteObject = (
arg: { type?: string; subtype?: string; value?: unknown; description?: string; unserializableValue?: string },
): string => {
@@ -98,7 +68,6 @@ const stringifyRemoteObject = (
return arg.type ?? "";
};
-/** Map CDP console-API method names to our normalized levels. */
const mapConsoleApiLevel = (type: string | undefined): ConsoleLevel => {
switch (type) {
case "error":
@@ -116,7 +85,6 @@ const mapConsoleApiLevel = (type: string | undefined): ConsoleLevel => {
}
};
-/** Map Log.LogEntry.level values ("verbose","info","warning","error") to our levels. */
const mapLogEntryLevel = (level: string | undefined): ConsoleLevel => {
switch (level) {
case "error":
@@ -145,8 +113,6 @@ const formatStackTrace = (
};
export const createConsoleBuffer = (capacity = 500): ConsoleBuffer => {
- // Insertion-ordered map keyed by seq. Map preserves insertion order in JS,
- // so iteration gives us oldest-first eviction without a separate index.
const records = new Map<number, ConsoleRecord>();
let nextSeq = 1;
let overflowed = false;
diff --git a/src/cdp/daemon-transport.ts b/src/cdp/daemon-transport.ts
index 819e581..101108d 100644
--- a/src/cdp/daemon-transport.ts
+++ b/src/cdp/daemon-transport.ts
@@ -1,15 +1,3 @@
-/**
- * DaemonTransport — CdpTransport backed by a Unix socket to the browser daemon.
- *
- * Satisfies the exact CdpTransport interface so BrowserClient, CdpSession, and
- * all domain tools work without changes. Internally uses newline-delimited JSON
- * over a Unix domain socket instead of a WebSocket to Chrome.
- *
- * References:
- * - CdpTransport interface: src/cdp/transport.ts
- * - net.createConnection IPC: https://nodejs.org/docs/latest-v22.x/api/net.html#netcreateconnection
- * - readline.createInterface: https://nodejs.org/docs/latest-v22.x/api/readline.html#readlinecreateinterfaceoptions
- */
import { createConnection, type Socket } from "node:net";
import { createInterface } from "node:readline";
@@ -26,7 +14,6 @@ import {
serialize,
} from "../daemon/protocol";
-// ── Implementation ─────────────────────────────────────────────────────────────
export const createDaemonTransport = (clientId: string): CdpTransport => {
let socket: Socket | null = null;
@@ -49,10 +36,8 @@ export const createDaemonTransport = (clientId: string): CdpTransport => {
for (const cb of closeListeners) cb();
};
- // ── Connect ────────────────────────────────────────────────────────────
const connect = (_url: string, opts?: { timeoutMs?: number }): Promise<Result<void, CdpError>> => {
- // url is ignored — we always connect to the daemon socket
const timeoutMs = opts?.timeoutMs ?? 10_000;
if (socket && !socket.destroyed && registered) {
@@ -133,17 +118,14 @@ export const createDaemonTransport = (clientId: string): CdpTransport => {
});
sock.on("error", () => {
- // Errors are surfaced via 'close'
});
sock.on("close", () => {
clearTimeout(connectTimer);
cleanup("Daemon socket closed");
- // If connect hasn't settled yet, settle with error
settle(err(cdpError("transport_closed", "Daemon socket closed before registration")));
});
- // Send registration
const regMsg: WireControl = { type: "control", action: "register", clientId };
sock.write(serialize(regMsg) + "\n");
});
@@ -158,7 +140,6 @@ export const createDaemonTransport = (clientId: string): CdpTransport => {
});
};
- // ── Close ──────────────────────────────────────────────────────────────
const close = async (): Promise<void> => {
if (registered && socket && !socket.destroyed) {
@@ -168,7 +149,6 @@ export const createDaemonTransport = (clientId: string): CdpTransport => {
cleanup("close() called");
};
- // ── Request ────────────────────────────────────────────────────────────
const request = (
method: string,
@@ -195,11 +175,9 @@ export const createDaemonTransport = (clientId: string): CdpTransport => {
return sendWithTimeout(pending, id, method, timeoutMs, "Daemon", () => sock.write(serialize(req) + "\n"));
};
- // ── Events ─────────────────────────────────────────────────────────────
const events = (): AsyncIterable<CdpEvent> => queue.iter;
- // ── State ──────────────────────────────────────────────────────────────
const state = (): "open" | "closed" | "connecting" => {
if (!socket) return "closed";
diff --git a/src/cdp/discovery.ts b/src/cdp/discovery.ts
index dc998b5..7b67fe0 100644
--- a/src/cdp/discovery.ts
+++ b/src/cdp/discovery.ts
@@ -20,13 +20,6 @@ const VersionResponse = Type.Object(
const versionValidator = Compile(VersionResponse);
-/**
- * A live CDP endpoint plus, when discovery went through a DevToolsActivePort
- * file, the user-data-dir that file lives in. That directory is the browser's
- * profile registry (`Local State`) — Chromium writes DevToolsActivePort to
- * chrome::DIR_USER_DATA — so it is what profile selection enumerates.
- * Undefined when the endpoint came from BU_CDP_WS or a bare port probe.
- */
export type CdpEndpoint = {
readonly wsUrl: string;
readonly userDataDir?: string;
@@ -63,17 +56,9 @@ const waitForPort = async (port: number): Promise<Result<void, CdpError>> => {
));
};
-// Fast, single-shot TCP check (no retry loop). Used to skip stale
-// DevToolsActivePort files left behind by a browser that has since quit —
-// unlike waitForPort, this returns immediately instead of blocking ~30s.
const isPortLive = async (port: number): Promise<boolean> => (await probePort(port)).success;
-// Ask the live browser for its canonical webSocketDebuggerUrl via
-// http://127.0.0.1:<port>/json/version. This is authoritative: it avoids
-// trusting a path from a stale DevToolsActivePort file that may belong to a
-// different browser which has since exited (e.g. Chrome's stale file pointing
-// at 9222 while Brave actually owns the port now). Returns null if the endpoint
-// is unreachable, non-200, or doesn't expose a WS URL.
+// /json/version is authoritative: a stale DevToolsActivePort file may name a browser that has since exited while another now owns the port.
const queryLiveWsUrl = (port: number, timeoutMs = 1_500): Promise<string | null> =>
new Promise<string | null>((resolve) => {
const req = httpRequest(
@@ -97,10 +82,6 @@ const queryLiveWsUrl = (port: number, timeoutMs = 1_500): Promise<string | null>
req.end();
});
-// Well-known CDP ports to probe when no DevToolsActivePort file is readable
-// (sandboxed harness, restricted filesystem permissions, or an unusual profile
-// location not on the profileDirs list). The default Chromium remote-debugging
-// port is 9222; users can extend via BU_CDP_PORTS="9222,9333".
const fallbackPorts = (): ReadonlyArray<number> => {
const ports = new Set<number>([9222]);
const fromEnv = process.env["BU_CDP_PORTS"];
@@ -117,21 +98,12 @@ type Candidate = {
readonly port: number;
readonly path: string;
readonly mtimeMs: number;
- /** The user-data-dir whose DevToolsActivePort produced this candidate. */
readonly userDataDir: string;
};
-/**
- * Locate a live CDP endpoint, reporting the user-data-dir it belongs to when
- * that is knowable. `discoverWsUrl` is the URL-only view of the same walk.
- */
export const discoverEndpoint = async (): Promise<Result<CdpEndpoint, CdpError>> => {
const dirs = userDataDirCandidates();
- // Phase 1: collect every readable DevToolsActivePort candidate up front,
- // rather than committing to the first one found. A single browser writes one
- // such file, but stale files from quit browsers linger, so we must
- // disambiguate below instead of trusting discovery order.
const candidates: Candidate[] = [];
const readErrors: string[] = [];
for (const base of dirs) {
@@ -143,13 +115,9 @@ export const discoverEndpoint = async (): Promise<Result<CdpEndpoint, CdpError>>
try {
mtimeMs = (await stat(portFile)).mtimeMs;
} catch {
- // mtime is only a tiebreaker; a stat failure just leaves it at 0.
}
} catch (e) {
- // Node fs errors carry .code; see ErrnoException. ENOENT is normal — the
- // dir is on our list but the user hasn't installed that browser. EPERM/
- // EACCES is common under sandboxes; remember it and fall back to network
- // probing rather than failing the whole discovery.
+ // EPERM/EACCES is common under sandboxes; remember it and fall back to network probing rather than failing the whole discovery.
const code = errnoCode(e);
if (code === "ENOENT" || code === undefined) continue;
if (code === "EPERM" || code === "EACCES") {
@@ -163,25 +131,16 @@ export const discoverEndpoint = async (): Promise<Result<CdpEndpoint, CdpError>>
const port = lines[0]?.trim();
const path = lines[1]?.trim();
if (!port || !path) continue;
- // Guard against a truncated/corrupt port file (e.g. a non-numeric first
- // line): an out-of-range port makes net.connect throw ERR_SOCKET_BAD_PORT
- // synchronously, which would escape discoverWsUrl instead of surfacing as
- // a Result error. Skip such entries.
+ // An out-of-range port makes net.connect throw ERR_SOCKET_BAD_PORT synchronously, escaping discoverWsUrl instead of surfacing as a Result error.
const portNum = Number(port);
if (!Number.isInteger(portNum) || portNum <= 0 || portNum >= 65536) continue;
candidates.push({ port: portNum, path, mtimeMs, userDataDir: base });
}
- // Phase 2: no readable candidate — probe well-known ports directly. Covers
- // sandboxed harnesses that can't read profile dirs and non-default install
- // locations. Requires the browser to have been launched with
- // --remote-debugging-port so /json/version is served.
if (candidates.length === 0) {
for (const port of fallbackPorts()) {
if (!(await isPortLive(port))) continue;
const live = await queryLiveWsUrl(port);
- // No DevToolsActivePort file was involved, so the user-data-dir behind
- // this endpoint is unknown — profile selection stays unavailable.
if (live) return ok({ wsUrl: live });
}
const permHint = readErrors.length > 0
@@ -193,9 +152,6 @@ export const discoverEndpoint = async (): Promise<Result<CdpEndpoint, CdpError>>
));
}
- // Phase 3: disambiguate candidates sharing a port (one browser running, and
- // another's file left behind on the default 9222): keep the most-recently-
- // written file — that's the currently-running browser.
const byPort = new Map<number, Candidate>();
for (const c of candidates) {
const prev = byPort.get(c.port);
@@ -203,16 +159,11 @@ export const discoverEndpoint = async (): Promise<Result<CdpEndpoint, CdpError>>
}
const unique = Array.from(byPort.values());
- // Prefer candidates whose port is actually accepting connections; this skips
- // stale files whose browser has quit (avoiding a ~30s waitForPort block).
const liveChecks = await Promise.all(unique.map((c) => isPortLive(c.port)));
const liveCandidates = unique.filter((_, i) => liveChecks[i]);
const ordered = liveCandidates.length > 0 ? liveCandidates : unique;
- // Phase 4: for each candidate, prefer the browser's own canonical WS URL from
- // /json/version. Some browsers disable that HTTP endpoint when remote
- // debugging is toggled only via chrome://inspect (rather than a launch flag),
- // so fall back to the WS path written in DevToolsActivePort.
+ // Some browsers disable the /json/version endpoint when remote debugging is toggled via chrome://inspect rather than a launch flag, hence the DevToolsActivePort fallback.
let lastErr: Result<CdpEndpoint, CdpError> | null = null;
for (const c of ordered) {
const ready = await waitForPort(c.port);
@@ -224,10 +175,6 @@ export const discoverEndpoint = async (): Promise<Result<CdpEndpoint, CdpError>>
return ok({ wsUrl: liveUrl ?? `ws://127.0.0.1:${c.port}${c.path}`, userDataDir: c.userDataDir });
}
- // Every discovered candidate was stale/unreachable. Before giving up, try the
- // well-known fallback ports — a user with a leftover DevToolsActivePort file
- // may still have a live browser reachable via 9222 / BU_CDP_PORTS. (Skip ports
- // already covered by a candidate above so we don't re-probe them.)
for (const port of fallbackPorts()) {
if (byPort.has(port) || !(await isPortLive(port))) continue;
const liveUrl = await queryLiveWsUrl(port);
@@ -237,10 +184,6 @@ export const discoverEndpoint = async (): Promise<Result<CdpEndpoint, CdpError>>
return lastErr ?? err(cdpError("discovery_failed", "no live DevTools endpoint among discovered candidates"));
};
-/**
- * URL-only view of {@link discoverEndpoint}, kept as the stable entry point for
- * callers that don't care which user-data-dir answered.
- */
export const discoverWsUrl = async (): Promise<Result<string, CdpError>> => {
const endpoint = await discoverEndpoint();
return endpoint.success ? ok(endpoint.data.wsUrl) : endpoint;
diff --git a/src/cdp/event-queue.ts b/src/cdp/event-queue.ts
index 87f5551..5e154ea 100644
--- a/src/cdp/event-queue.ts
+++ b/src/cdp/event-queue.ts
@@ -1,12 +1,3 @@
-/**
- * Shared transport primitives for CdpTransport implementations.
- *
- * Both the WebSocket transport (transport.ts, talking to Chrome directly) and
- * the daemon transport (daemon-transport.ts, talking to the daemon over a Unix
- * socket) need an identical single-consumer async event queue plus the same
- * pending-request bookkeeping. This module is the single source of truth for
- * that shared machinery.
- */
import { type Result, err } from "../util/result";
import { type CdpError, cdpError } from "./errors";
@@ -14,14 +5,6 @@ import type { CdpEvent } from "./types";
export type CdpResult = Result<unknown, CdpError>;
-/**
- * A single-consumer async queue of CDP events.
- *
- * `push` enqueues an event (dropped silently after `end`). `end` terminates the
- * stream — any pending and future iterator `next()` calls resolve `done:true`.
- * `iter` is the AsyncIterable consumed by one for-await loop; a second consumer
- * would silently steal events from the first.
- */
export type EventQueue = {
readonly push: (e: CdpEvent) => void;
readonly end: () => void;
@@ -59,18 +42,12 @@ export const makeEventQueue = (): EventQueue => {
};
};
-/** An in-flight CDP request awaiting its response or timeout. */
export type Pending = {
readonly resolve: (v: Result<unknown, CdpError>) => void;
readonly timer: ReturnType<typeof setTimeout>;
readonly method: string;
};
-/**
- * Reject every in-flight request with a transport_closed error and clear the
- * map. Called from each transport's cleanup path when the underlying connection
- * goes away.
- */
export const rejectAllPending = (pending: Map<number, Pending>, reason: string): void => {
for (const [, p] of pending) {
clearTimeout(p.timer);
@@ -79,18 +56,6 @@ export const rejectAllPending = (pending: Map<number, Pending>, reason: string):
pending.clear();
};
-/**
- * Send a CDP request and return a promise that settles when the matching
- * response arrives, the request times out, or the send throws.
- *
- * The transport supplies a `send` callback that puts the already-serialized
- * payload onto its wire (WebSocket frame or Unix-socket line); everything else
- * — registering the pending entry, the timeout timer, and synchronous send
- * failure handling — is identical across transports and lives here.
- *
- * `timeoutLabel` is the transport's prefix for the timeout error message
- * (e.g. "CDP" or "Daemon").
- */
export const sendWithTimeout = (
pending: Map<number, Pending>,
id: number,
@@ -114,10 +79,6 @@ export const sendWithTimeout = (
}
});
-/**
- * Register a close listener, returning an unsubscribe function. Identical
- * across transports, so it lives here.
- */
export const makeOnClose = (closeListeners: Set<() => void>) =>
(cb: () => void): (() => void) => {
closeListeners.add(cb);
diff --git a/src/cdp/network-buffer.ts b/src/cdp/network-buffer.ts
index c26ae73..f6ad032 100644
--- a/src/cdp/network-buffer.ts
+++ b/src/cdp/network-buffer.ts
@@ -1,18 +1,3 @@
-/**
- * In-memory aggregator for CDP Network.* events.
- *
- * The CDP event stream is consumed by a single internal loop in session.ts.
- * That loop hands Network.* event params to this module, which joins them into
- * one record per requestId (request → response → finished/failed). Tools then
- * read records via drain(), filtered to what the caller asked for.
- *
- * Bounded by a fixed capacity (default 500): on overflow the oldest record is
- * evicted FIFO. The overflow flag is reported once per drain so tools can warn
- * the LLM that data may be missing.
- *
- * This module has no transport dependency, so the joining/filtering logic stays
- * trivially testable in isolation.
- */
import { decodeEvent } from "./events";
@@ -24,34 +9,27 @@ export type NetworkRecord = {
statusText?: string;
type: string;
mimeType?: string;
- /** Wall-clock ms when requestWillBeSent fired, in Date.now() units. */
requestStartedMs: number;
durationMs?: number;
requestBodySize: number;
responseBodySize?: number;
failed?: boolean;
errorText?: string;
- /** Populated by the tool layer via Network.getResponseBody, never by the aggregator. */
body?: string | null;
};
export type NetworkFilter = {
- /** Substring match by default. Wrap in slashes (`/foo/`) for regex. */
urlPattern?: string;
methodFilter?: string[];
statusFilter?: { min?: number; max?: number };
resourceTypes?: string[];
- /** Only requests whose start is at most N ms ago. */
sinceMs?: number;
- /** Cap on the returned records. */
limit?: number;
};
export type DrainResult = {
readonly records: NetworkRecord[];
- /** Total matches before `limit` was applied. */
readonly total: number;
- /** True if the ring buffer evicted at least one record since the last drain. */
readonly bufferOverflowed: boolean;
};
@@ -70,15 +48,12 @@ const compileUrlMatcher = (pattern: string): ((url: string) => boolean) => {
const re = new RegExp(pattern.slice(1, -1));
return (url) => re.test(url);
} catch {
- // Fall through to substring match if the pattern is not a valid regex.
}
}
return (url) => url.includes(pattern);
};
export const createNetworkBuffer = (capacity = 500): NetworkBuffer => {
- // Insertion-ordered map keyed by requestId. Map preserves insertion order in JS,
- // so iteration gives us oldest-first eviction without a separate index.
const records = new Map<string, NetworkRecord>();
let overflowed = false;
@@ -99,7 +74,6 @@ export const createNetworkBuffer = (capacity = 500): NetworkBuffer => {
const url = params?.request.url;
const method = params?.request.method;
if (!id || !url || !method) return;
- // Re-fired requestWillBeSent for the same id (e.g. redirect) — refresh in place.
records.delete(id);
evictOldestIfFull();
const postData = params.request.postData;
@@ -166,13 +140,12 @@ export const createNetworkBuffer = (capacity = 500): NetworkBuffer => {
if (cutoff !== undefined && r.requestStartedMs < cutoff) continue;
if (minStatus !== undefined && (r.status === undefined || r.status < minStatus)) continue;
if (maxStatus !== undefined && (r.status === undefined || r.status > maxStatus)) continue;
- // Clone so callers can mutate (e.g. attach body) without polluting the buffer.
matched.push({ ...r });
}
const total = matched.length;
const limit = Math.min(filter.limit ?? 50, 500);
- const limited = matched.slice(-limit); // most recent N
+ const limited = matched.slice(-limit);
const bufferOverflowed = overflowed;
overflowed = false;
diff --git a/src/cdp/ownership.ts b/src/cdp/ownership.ts
index 727b3d8..0a1a810 100644
--- a/src/cdp/ownership.ts
+++ b/src/cdp/ownership.ts
@@ -1,28 +1,12 @@
-/**
- * Tracks which tab targetIds belong to this harness session.
- *
- * The harness attaches to the user's running Chrome, which is also full of
- * the user's own tabs. Without an ownership boundary the agent could attach
- * to (or close) personal tabs by accident. The registry is the single source
- * of truth for "did this session open that tab?".
- *
- * Mutations notify a single change listener so the orchestrator can persist
- * to disk via the existing BrowserState flow without the registry needing to
- * know about the pi storage API.
- */
export type OwnershipRegistry = {
add(targetId: string): void;
remove(targetId: string): void;
has(targetId: string): boolean;
list(): ReadonlyArray<string>;
- /** Replace the full set — used on restoreState to re-hydrate from disk. */
replaceAll(ids: ReadonlyArray<string>): void;
setHarnessWindow(targetId: string | undefined): void;
harnessWindow(): string | undefined;
- /** The real Chrome windowId of the dedicated harness window — the durable
- * identity of "the window this session initialized", surviving seed-tab
- * closure. Undefined until the window is created (or after teardown). */
setHarnessWindowId(windowId: number | undefined): void;
harnessWindowId(): number | undefined;
onChange(cb: () => void): void;
diff --git a/src/cdp/session.ts b/src/cdp/session.ts
index 80fec16..de3fb28 100644
--- a/src/cdp/session.ts
+++ b/src/cdp/session.ts
@@ -10,10 +10,6 @@ import { createConsoleBuffer, type ConsoleDrainResult, type ConsoleFilter } from
import { attachTo } from "./attach";
import { getWindowId } from "./window";
-// Per-tab state. One TabSession exists per known targetId.
-// Dialog, page-info dirty flag, and CDP buffers are per-tab — switching
-// tabs preserves the previous tab's data in its TabSession instead of
-// clearing it.
type TabSession = {
sessionId: string;
targetId: string;
@@ -21,12 +17,7 @@ type TabSession = {
pageInfoDirty: boolean;
networkBuffer: ReturnType<typeof createNetworkBuffer>;
consoleBuffer: ReturnType<typeof createConsoleBuffer>;
- // Stable element refs (e1, e2, …) → CDP backendNodeId, from the latest
- // snapshot of this tab. Replaced wholesale on every snapshot — old refs are
- // stale anyway, so this stays bounded (~50–60 KB worst case).
refMap: Map<string, number>;
- // Per-ref signature ("role|name|value") from the latest snapshot, used as the
- // baseline for the post-mutation auto-diff. Replaced alongside refMap.
refSig: Map<string, string>;
};
@@ -37,11 +28,8 @@ export type CdpSession = {
attachFirstPage(): Promise<Result<{ readonly targetId: string; readonly sessionId: string }, CdpError>>;
switchTo(targetId: string): Promise<Result<void, CdpError>>;
current(): { readonly sessionId: string; readonly targetId: string } | null;
- /** Replace the active tab's ref map and ref signatures (from a fresh snapshot). */
setRefMap(refMap: Map<string, number>, refSig: Map<string, string>): void;
- /** Resolve a ref (e.g. "e12") to its backendNodeId on the active tab, or undefined if unknown/stale. */
resolveRef(ref: string): number | undefined;
- /** The active tab's prior ref signatures — baseline for the post-mutation diff. */
refSignatures(): ReadonlyMap<string, string>;
refMappings(): ReadonlyMap<string, number>;
call<M extends CdpMethod>(method: M, params?: ParamsOf<M>, opts?: { timeoutMs?: number }): Promise<Result<ResultOf<M>, CdpError>>;
@@ -53,15 +41,7 @@ export type CdpSession = {
drainConsoleBuffer(filter: ConsoleFilter): ConsoleDrainResult;
};
-/**
- * Supplies the seed tab when attachFirstPage finds no owned tab to attach to.
- * Injected by the client so that seeding goes through the target factory —
- * which, with a profile pinned, opens the window inside that profile. Without
- * it, attachFirstPage's own `Target.createTarget` would land in whichever
- * profile currently has focus.
- *
- * The provider is responsible for its own ownership bookkeeping.
- */
+// Injected so seeding goes through the target factory; attachFirstPage's own `Target.createTarget` would land in whichever profile has focus.
export type SeedTargetProvider = () => Promise<Result<string, CdpError>>;
export const createCdpSession = (
@@ -72,15 +52,11 @@ export const createCdpSession = (
let sessionId: string | null = null;
let targetId: string | null = null;
- // Per-tab tracking: maps targetId → TabSession and sessionId → targetId for
- // event routing. Lazy: a TabSession is created on the first visit to a tab.
const tabs = new Map<string, TabSession>();
const sessionIdToTargetId = new Map<string, string>();
let activeConsumer: Promise<void> = Promise.resolve();
- // Event → TabSession resolver. Uses sessionId from CDP events to find the
- // correct TabSession, falling back to the current targetId when no sessionId.
const resolveTab = (evSessionId?: string): TabSession | undefined => {
const tid = evSessionId ? sessionIdToTargetId.get(evSessionId) : targetId;
return tid ? tabs.get(tid) : undefined;
@@ -88,8 +64,6 @@ export const createCdpSession = (
const consumeEvents = async (): Promise<void> => {
for await (const ev of transport.events()) {
- // Filter: skip events from sessions we're not currently tracking.
- // Target.targetDestroyed is browser-level (no sessionId) — always process.
if (ev.method !== "Target.targetDestroyed") {
if (ev.sessionId && !sessionIdToTargetId.has(ev.sessionId)) continue;
}
@@ -109,10 +83,7 @@ export const createCdpSession = (
: { type: "alert", message: "" };
break;
}
- // Page.javascriptDialogClosed is intentionally NOT cleared here —
- // the dialog stays in the buffer until takeDialog() is called.
- // This prevents fast dismiss flows from dropping a dialog the agent
- // was about to read. (Fix for spec §7 predictability bug #2.)
+ // Intentionally NOT cleared on javascriptDialogClosed: a fast dismiss would otherwise drop a dialog the agent was about to read.
case "Page.frameNavigated":
case "Page.loadEventFired": {
const tab = resolveTab(ev.sessionId);
@@ -120,11 +91,6 @@ export const createCdpSession = (
break;
}
case "Target.targetCreated": {
- // Adopt tabs opened BY a tab we already own (window.open, target=_blank,
- // popups). Chrome sets openerId to the opener target; if that opener is
- // owned, the child belongs to this session's window and must be
- // controllable + cleaned up. Targets the user opens themselves have no
- // owned opener, so they are never adopted.
if (!ownership) break;
const decoded = decodeEvent(ev.method, ev.params);
if (!decoded.success) break;
@@ -140,7 +106,6 @@ export const createCdpSession = (
if (!decoded.success || !decoded.data.targetId) break;
const destroyed = decoded.data.targetId;
ownership.remove(destroyed);
- // Prune per-tab state for the destroyed target
const tab = tabs.get(destroyed);
if (tab) {
sessionIdToTargetId.delete(tab.sessionId);
@@ -186,10 +151,6 @@ export const createCdpSession = (
const restartConsumer = (): void => {
activeConsumer = activeConsumer.then(() => consumeEvents()).catch((e: unknown) => {
- // The .then() chain calls consumeEvents() which iterates the transport's
- // events() AsyncIterable. The iterable resolves cleanly on close (returns
- // {done:true}); any rejection here is an unexpected bug in the event
- // handler, not a normal termination. Surface it on stderr so it's not lost.
console.warn("[pi-browser-harness] CDP event consumer crashed:", e);
});
};
@@ -200,26 +161,16 @@ export const createCdpSession = (
targetId = null;
tabs.clear();
sessionIdToTargetId.clear();
- // Do NOT clear `dialog` here — same rationale as inside consumeEvents:
- // the agent may have a pending takeDialog() call that should still see it.
+ // Do NOT clear `dialog` here: a pending takeDialog() must still see it.
restartConsumer();
});
- // TODO(perf): the four enable calls are sequential here for predictability.
- // Switching to Promise.all over a single WS pipelines the round-trips and
- // saves ~3× on tab-switch latency. Defer until session.ts has tests.
const enableDomains = async (sid: string): Promise<void> => {
for (const d of ["Page", "DOM", "Runtime", "Network", "Accessibility", "Log"]) {
await transport.request(`${d}.enable`, {}, { sessionId: sid });
}
};
- // The one place `unknown` ends for every CDP call this session makes — its
- // own internal bookkeeping calls (attach/switch) and the public
- // call/callOnTarget/callBrowser all funnel through here. A single
- // transport-then-decode implementation means a future change (tracing,
- // error wrapping, the short-circuit on failure) lands in one place, and a
- // new internal call site can't forget to decode.
const req = async <M extends CdpMethod>(
method: M,
params: ParamsOf<M> | undefined,
@@ -236,14 +187,11 @@ export const createCdpSession = (
const session: CdpSession = {
async attachFirstPage() {
- // Subscribe to Target.* events so we can react to targetDestroyed.
- // Best-effort: failing to enable discovery is not fatal for attach.
await req("Target.setDiscoverTargets", { discover: true }, null);
const targets = await req("Target.getTargets", {}, null);
if (!targets.success) return targets;
const allPages = targets.data.targetInfos.filter((t) => t.type === "page");
- // Reconcile the persisted ownership set against live targets — drop dead IDs.
if (ownership) {
const live = new Set(allPages.map((p) => p.targetId));
const survivors = ownership.list().filter((id) => live.has(id));
@@ -253,22 +201,12 @@ export const createCdpSession = (
const anchorId = survivors[0];
if (anchorId === undefined) {
- // No owned tab survived — the harness window is gone (user closed it,
- // or Chrome restarted and reassigned all ids). The persisted windowId
- // is now meaningless and could even collide with one of the user's
- // windows, so drop it. attachFirstPage will mint a fresh window below.
ownership.setHarnessWindowId(undefined);
} else {
- // Self-heal ownership from the window itself. We anchor on a tab we
- // KNOW we own (a survivor) and re-adopt only its live window-mates —
- // popups/children from last session, tabs reopened in our window.
- // Ownership is thus DERIVED from the real window, never from the bare
- // persisted integer (which Chrome may reassign to a user window on
- // restart — anchoring on a survivor makes that misfire impossible).
const anchorWin = await getWindowId(session, anchorId);
if (anchorWin.success) {
const anchorWindowId = anchorWin.data;
- ownership.setHarnessWindowId(anchorWindowId); // refresh to the live id
+ ownership.setHarnessWindowId(anchorWindowId);
const windows = await Promise.all(
allPages.map((p) =>
p.targetId === anchorId
@@ -286,7 +224,6 @@ export const createCdpSession = (
if (!ownership.harnessWindow()) ownership.setHarnessWindow(anchorId);
}
}
- // Prune per-tab state for pages that no longer exist
const liveTargetIds = new Set(allPages.map((p) => p.targetId));
for (const tid of tabs.keys()) {
if (!liveTargetIds.has(tid)) {
@@ -296,17 +233,12 @@ export const createCdpSession = (
}
}
- // Prefer attaching to a tab this session already owns. Falls back to
- // creating a fresh harness-owned tab in a dedicated window — never
- // grabs the user's foreground tab.
let pickTargetId: string | undefined;
if (ownership) {
const ownedLive = ownership.list().filter((id) => allPages.some((p) => p.targetId === id));
pickTargetId = ownedLive[0];
}
if (!pickTargetId && createSeedTarget) {
- // Delegated seeding (see SeedTargetProvider): the provider creates the
- // window in the right profile and records ownership itself.
const seeded = await createSeedTarget();
if (!seeded.success) return seeded;
pickTargetId = seeded.data;
@@ -322,8 +254,6 @@ export const createCdpSession = (
if (ownership) {
ownership.setHarnessWindow(created.data.targetId);
ownership.add(created.data.targetId);
- // Capture the real Chrome windowId — the durable identity of the
- // window this session initialized (survives seed-tab closure).
const win = await getWindowId(session, created.data.targetId);
if (win.success) ownership.setHarnessWindowId(win.data);
}
@@ -354,7 +284,6 @@ export const createCdpSession = (
const attached = await attachTo(session, tid);
if (!attached.success) return attached;
const attachedSessionId = attached.data;
- // Reuse existing TabSession or create one on first visit
const existing = tabs.get(tid);
const tab: TabSession = existing ?? {
sessionId: attachedSessionId,
@@ -367,7 +296,6 @@ export const createCdpSession = (
refSig: new Map(),
};
if (existing) {
- // Each Target.attachToTarget produces a new sessionId — update it.
sessionIdToTargetId.delete(existing.sessionId);
existing.sessionId = attachedSessionId;
} else {
@@ -375,7 +303,6 @@ export const createCdpSession = (
}
sessionIdToTargetId.set(attachedSessionId, tid);
await enableDomains(attachedSessionId);
- // Update global pointers to point at the new active tab
sessionId = tab.sessionId;
targetId = tid;
return ok(undefined);
diff --git a/src/cdp/target-factory.ts b/src/cdp/target-factory.ts
index 718a8d0..62c46f1 100644
--- a/src/cdp/target-factory.ts
+++ b/src/cdp/target-factory.ts
@@ -1,31 +1,4 @@
-/**
- * The single place harness tabs and windows are created.
- *
- * Every tab the harness opens must land in the pinned profile, so all creation
- * paths (client.newTab, browser_open_urls, isolated tabs for web_search /
- * read_page) funnel through here. A missed call site would silently act in
- * another profile — with that profile's cookies and logins.
- *
- * Two mechanisms, chosen by whether a profile is pinned:
- *
- * unpinned — `Target.createTarget`, exactly as before: `newWindow: true` for
- * a fresh dedicated window, `openerId` for siblings inside it.
- *
- * pinned — the window comes from a command-line launch into the profile
- * (see profile/launch.ts), and further tabs come from
- * `window.open(..., '_blank')` evaluated in an owned page with
- * `userGesture: true`. That is the only CDP-reachable way to place
- * a tab in a non-default browser context: createTarget rejects a
- * foreign browserContextId outright, and `openerId` does NOT
- * inherit the opener's context (verified — it lands in the
- * focus-derived default context, in a different window).
- * window.open keeps both the context and the window, recursively.
- *
- * Correlation: window.open returns no target id, so each spawn carries a unique
- * `about:blank#pi-<uuid>` token and the new target is found by that token.
- * Set-difference polling would be wrong here — isolated tabs run concurrently
- * (browser_read_page is registered unserialized).
- */
+// `window.open` with userGesture is the only CDP-reachable way into a non-default browser context: createTarget rejects a foreign browserContextId and `openerId` does not inherit the opener's context.
import { randomUUID } from "node:crypto";
import type { BrowserClient } from "../client";
@@ -34,17 +7,11 @@ import { attachTo } from "./attach";
import { type CdpError, cdpError } from "./errors";
import { getWindowId } from "./window";
-/** How long to wait for a window.open-spawned target to appear. */
const SPAWN_DEADLINE_MS = 8_000;
const SPAWN_POLL_MS = 100;
export type HarnessWindow = {
- /** Target id of the window's seed tab. */
readonly targetId: string;
- /**
- * True when this call created the window. The seed tab is then a blank tab
- * the caller may use directly, instead of opening a second one.
- */
readonly freshlyCreated: boolean;
};
@@ -61,17 +28,12 @@ const listPageTargets = async (client: BrowserClient): Promise<Result<ReadonlyAr
return ok(r.data.targetInfos.filter((t) => t.type === "page"));
};
-/** A CDP session id for driving `targetId`, reusing the active one when possible. */
const sessionForTarget = async (client: BrowserClient, targetId: string): Promise<Result<string, CdpError>> => {
const current = client.current();
if (current?.targetId === targetId) return ok(current.sessionId);
return attachTo(client.session(), targetId);
};
-/**
- * Spawn a tab from `openerTargetId` via window.open, so it inherits the
- * opener's profile and window, and return its target id.
- */
const spawnTabViaOpener = async (
client: BrowserClient,
openerTargetId: string,
@@ -111,29 +73,13 @@ const spawnTabViaOpener = async (
return err(cdpError("timeout", `the tab opened in the pinned profile did not report back within ${SPAWN_DEADLINE_MS}ms`));
};
-/**
- * In-flight window creation, per client. Isolated tabs run concurrently
- * (browser_read_page is registered unserialized), so two of them starting at
- * once would otherwise each find "no harness window" and create one — two blank
- * windows before, and two launched profile windows now. Callers share the first
- * call's result instead.
- */
const inFlight = new WeakMap<BrowserClient, Promise<Result<HarnessWindow, CdpError>>>();
-/**
- * The harness window for this session, creating it when absent.
- *
- * Reuses a live owned tab when one exists — including after the user closed the
- * original seed — so reconnects don't scatter windows.
- */
export const ensureHarnessWindow = async (client: BrowserClient): Promise<Result<HarnessWindow, CdpError>> => {
const pending = inFlight.get(client);
if (pending) {
const shared = await pending;
- // `freshlyCreated` is a claim on the seed tab, and only the caller that
- // started the creation may hold it. Everyone waiting on the same window
- // must open a tab of their own beside it, or two callers would drive the
- // very same tab.
+ // `freshlyCreated` is a claim on the seed tab that only the caller which started the creation may hold, or two callers would drive the same tab.
return shared.success ? ok({ targetId: shared.data.targetId, freshlyCreated: false }) : shared;
}
const attempt = ensureHarnessWindowUncoordinated(client).finally(() => inFlight.delete(client));
@@ -150,11 +96,7 @@ const ensureHarnessWindowUncoordinated = async (
const ownership = client.ownership();
const pinnedCtx = client.profileContextId();
- // A tab may only be reused when we can prove which profile it is in. With a
- // pin set but no context resolved yet — a fresh connection, or a browser that
- // restarted — the recorded tabs cannot be verified, so we seed a new window
- // rather than risk adopting one from the wrong profile. (Sessions close their
- // owned tabs on shutdown, so this is the normal, no-cost case.)
+ // With a pin set but no context resolved yet, a recorded tab's profile cannot be verified, so seed a new window rather than risk adopting one from the wrong profile.
const canReuse = client.profilePin() === null || pinnedCtx !== undefined;
const inPinnedProfile = (targetId: string): boolean => {
if (!pinnedCtx) return true;
@@ -181,7 +123,6 @@ const ensureHarnessWindowUncoordinated = async (
return ok({ targetId: seeded.data, freshlyCreated: true });
};
-/** Unpinned path: a fresh dedicated window, exactly as the harness always did. */
const createDedicatedWindow = async (client: BrowserClient): Promise<Result<string, CdpError>> => {
const created = await client.session().callBrowser("Target.createTarget", { url: "about:blank", newWindow: true });
if (!created.success) return created;
@@ -194,10 +135,6 @@ const createDedicatedWindow = async (client: BrowserClient): Promise<Result<stri
return ok(targetId);
};
-/**
- * Open a harness-owned tab beside `openerTargetId`, in the same window and —
- * when a profile is pinned — the same profile. The tab is registered as owned.
- */
export const openHarnessTab = async (
client: BrowserClient,
openerTargetId: string,
diff --git a/src/cdp/transport.ts b/src/cdp/transport.ts
index f3bcd16..33b9412 100644
--- a/src/cdp/transport.ts
+++ b/src/cdp/transport.ts
@@ -15,8 +15,6 @@ export type CdpTransport = {
params: Record<string, unknown>,
opts?: { sessionId?: string | null; timeoutMs?: number },
): Promise<Result<unknown, CdpError>>;
- // Returns the current event queue's iterator. After close()+reconnect, callers
- // must re-call events() — the old iterator will have received done:true.
events(): AsyncIterable<CdpEvent>;
state(): "open" | "closed" | "connecting";
onClose(cb: () => void): () => void;
@@ -107,16 +105,13 @@ export const createCdpTransport = (): CdpTransport => {
clearTimeout(timer);
ws = null;
cleanup("WebSocket closed");
- // If onopen already settled the connect promise, this settle is a no-op
- // (guarded by `settled`). cleanup() above still runs unconditionally so
- // pending requests are rejected and the event queue is rotated.
settle(err(cdpError("transport_closed", "CDP WebSocket closed")));
};
});
},
close(): Promise<void> {
if (ws) {
- try { ws.close(1000, "Shutdown"); } catch { /* best effort */ }
+ try { ws.close(1000, "Shutdown"); } catch {}
ws = null;
}
cleanup("close() called");
@@ -128,8 +123,7 @@ export const createCdpTransport = (): CdpTransport => {
if (!ws || ws.readyState !== WebSocket.OPEN) {
return Promise.resolve(err(cdpError("transport_closed", "Browser not connected. Is Chrome running?", method)));
}
- // Bind to a local const so the non-null is guaranteed even across microtasks
- // between the readyState check and the send() call.
+ // Bind to a local const so the non-null holds across microtasks between the readyState check and send().
const sock = ws;
const id = nextId++;
const payload: Record<string, unknown> = { id, method, params };
@@ -138,11 +132,7 @@ export const createCdpTransport = (): CdpTransport => {
return sendWithTimeout(pending, id, method, timeoutMs, "CDP", () => sock.send(json));
},
events(): AsyncIterable<CdpEvent> {
- // Single-consumer: each connection's event stream may be iterated by
- // exactly one for-await loop. Calling events() multiple times returns
- // the same iterable; the second consumer will silently steal events
- // from the first. After a reconnect, the previous iterator is ended
- // and callers must re-call events() to receive new events.
+ // Single-consumer: a second for-await loop over this iterable silently steals events from the first.
return queue.iter;
},
state(): "open" | "closed" | "connecting" {
diff --git a/src/cdp/window.ts b/src/cdp/window.ts
index f16838c..b06f1ac 100644
--- a/src/cdp/window.ts
+++ b/src/cdp/window.ts
@@ -2,7 +2,6 @@ import { map, type Result } from "../util/result";
import type { CdpError } from "./errors";
import type { CdpSession } from "./session";
-/** Browser.getWindowForTarget, unwrapped to the windowId. */
export const getWindowId = async (
session: CdpSession,
targetId: string,