all repos — sz-chrome-remote-cdp @ 226e814d47a5e1acd4d308619921ca1012723b88

pi-browser-harness, patched to drive a Chrome on another host over CDP, with castle_cdp_search web search

fix(cdp): route background-tab events to the tab they belong to

The event loop dropped every event whose sessionId was not the active tab's, so
the per-tab console and network buffers could only ever hold data captured while
their tab was in the foreground. The filter now drops only sessions this instance
never attached, and the dialog, frameNavigated and loadEventFired arms resolve
their tab from the event's sessionId instead of assuming the active tab — without
that, a background tab's dialog would have been recorded against the foreground
one. Tabs are also registered before the domain-enable round trips, so events
arriving during attach are no longer dropped as unknown.

Report: docs/superpowers/reports/2026-07-26-bug-audit.md#f4

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
amankumarsingh77 amankumarsingh7702@gmail.com
Tue, 28 Jul 2026 19:44:17 +0530
commit

226e814d47a5e1acd4d308619921ca1012723b88

parent

ce30547af39f692bef777591bdb329a7fa00e86e

2 files changed, 104 insertions(+), 4 deletions(-)

jump to
M src/cdp/session.ts → src/cdp/session.ts

@@ -90,11 +90,11 @@ 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 && ev.sessionId !== sessionId) continue; + if (ev.sessionId && !sessionIdToTargetId.has(ev.sessionId)) continue; } switch (ev.method) { case "Page.javascriptDialogOpening": { - const tab = targetId ? tabs.get(targetId) : undefined; + const tab = resolveTab(ev.sessionId); if (!tab) break; const decoded = decodeEvent(ev.method, ev.params); tab.dialog = decoded.success

@@ -114,7 +114,7 @@ // This prevents fast dismiss flows from dropping a dialog the agent

// was about to read. (Fix for spec §7 predictability bug #2.) case "Page.frameNavigated": case "Page.loadEventFired": { - const tab = targetId ? tabs.get(targetId) : undefined; + const tab = resolveTab(ev.sessionId); if (tab) tab.pageInfoDirty = true; break; }

@@ -333,7 +333,6 @@ if (!attached.success) return attached;

const attachedSessionId = attached.data; sessionId = attachedSessionId; targetId = pickTargetId; - await enableDomains(attachedSessionId); tabs.set(pickTargetId, { sessionId: attachedSessionId, targetId: pickTargetId,

@@ -345,6 +344,7 @@ refMap: new Map(),

refSig: new Map(), }); sessionIdToTargetId.set(attachedSessionId, pickTargetId); + await enableDomains(attachedSessionId); return ok({ targetId: pickTargetId, sessionId: attachedSessionId }); }, async switchTo(tid) {
A test/cdp/session-events-test.ts

@@ -0,0 +1,100 @@

+import { test, describe } from "node:test"; +import assert from "node:assert/strict"; +import { setImmediate as tick } from "node:timers/promises"; +import { createCdpSession } from "../../src/cdp/session"; +import { createOwnershipRegistry } from "../../src/cdp/ownership"; +import { createStubTransport } from "../domains/fake-client"; +import { ok } from "../../src/util/result"; + +const flush = async (): Promise<void> => { + await tick(); + await tick(); +}; + +const twoTabSession = async () => { + const stub = createStubTransport({ + "Target.setDiscoverTargets": ok({}), + "Target.getTargets": ok({ + targetInfos: [ + { targetId: "t1", type: "page", title: "One", url: "https://one.test/" }, + { targetId: "t2", type: "page", title: "Two", url: "https://two.test/" }, + ], + }), + "Target.attachToTarget": [ok({ sessionId: "s1" }), ok({ sessionId: "s2" }), ok({ sessionId: "s3" })], + "Browser.getWindowForTarget": ok({ windowId: 7 }), + "Target.activateTarget": ok({}), + }); + const ownership = createOwnershipRegistry({ ownedTargetIds: ["t1", "t2"] }); + const session = createCdpSession(stub.transport, ownership); + const attached = await session.attachFirstPage(); + assert.equal(attached.success, true); + await session.switchTo("t2"); + return { stub, session }; +}; + +describe("CdpSession event routing across tabs", () => { + test("a console event from a background tab reaches that tab's buffer", async () => { + const { stub, session } = await twoTabSession(); + stub.emit({ + method: "Runtime.consoleAPICalled", + sessionId: "s1", + params: { type: "error", args: [{ type: "string", value: "boom" }] }, + }); + await flush(); + assert.equal(session.drainConsoleBuffer({}).records.length, 0); + await session.switchTo("t1"); + const drained = session.drainConsoleBuffer({}); + assert.equal(drained.records.length, 1); + assert.equal(drained.records[0]?.text, "boom"); + }); + + test("a network event from a background tab reaches that tab's buffer", async () => { + const { stub, session } = await twoTabSession(); + stub.emit({ + method: "Network.requestWillBeSent", + sessionId: "s1", + params: { requestId: "r1", request: { url: "https://one.test/api", method: "GET" }, type: "XHR" }, + }); + await flush(); + await session.switchTo("t1"); + const drained = session.drainNetworkBuffer({}); + assert.equal(drained.records.length, 1); + assert.equal(drained.records[0]?.url, "https://one.test/api"); + }); + + test("a background tab's dialog is recorded against that tab, not the active one", async () => { + const { stub, session } = await twoTabSession(); + stub.emit({ + method: "Page.javascriptDialogOpening", + sessionId: "s1", + params: { type: "confirm", message: "really?" }, + }); + await flush(); + assert.equal(session.takeDialog(), null); + await session.switchTo("t1"); + assert.equal(session.takeDialog()?.message, "really?"); + }); + + test("a background tab's navigation marks that tab's page info dirty, not the active one", async () => { + const { stub, session } = await twoTabSession(); + session.drainPageInfoInvalidations(); + stub.emit({ method: "Page.loadEventFired", sessionId: "s1", params: {} }); + await flush(); + assert.equal(session.drainPageInfoInvalidations(), false); + await session.switchTo("t1"); + assert.equal(session.drainPageInfoInvalidations(), true); + }); + + test("an event from a session this instance never attached is ignored", async () => { + const { stub, session } = await twoTabSession(); + stub.emit({ + method: "Runtime.consoleAPICalled", + sessionId: "unknown-session", + params: { type: "error", args: [{ type: "string", value: "stranger" }] }, + }); + await flush(); + assert.equal(session.drainConsoleBuffer({}).records.length, 0); + await session.switchTo("t1"); + assert.equal(session.drainConsoleBuffer({}).records.length, 0); + }); +});