fix(extension): retry console capture when a domain enable fails

`enableConsoleDomains` marked the tab in `consoleDomainsAttemptedTabs`
before issuing `Runtime.enable`/`Log.enable` and swallowed any errors,
so a transient failure (e.g. a restricted page during attach)
permanently disabled console capture for that tab: every later
`ensureConsoleCapture` was a no-op and the agent silently got an empty
console buffer with no indication that capture was broken.

This was inconsistent with `enableNetworkDomain`, which records the tab
only after `Network.enable` succeeds (and is explicitly tested as
"retries and surfaces failures"). Bring `enableConsoleDomains` in line:
mark the tab only after both domains enable successfully, so a later
`ensureConsoleCapture` can recover from a transient failure. Renamed
the flag `consoleDomainsAttemptedTabs` -> `consoleDomainsEnabledTabs`
to match `networkDomainsEnabledTabs` and reflect the new semantics.

Attach remains best-effort (it still succeeds when a domain can't be
enabled). Adds a regression test asserting `ensureConsoleCapture`
retries after a failed enable; it fails (no retry) on the previous code.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Qiaochu Hu
2026-07-22 20:12:51 +08:00
co-authored by Claude
parent ff74db48c8
commit 2dad162a87
2 changed files with 39 additions and 8 deletions
@@ -132,11 +132,33 @@ describe("ChromiumCdp", () => {
expect(cdp.isAttached(12)).toBe(true);
expect(api.sendCommand).toHaveBeenCalledWith({ tabId: 12 }, "Runtime.enable", {});
expect(api.sendCommand).toHaveBeenCalledWith({ tabId: 12 }, "Log.enable", {});
await cdp.ensureConsoleCapture(12);
const enableCalls = (api.sendCommand as ReturnType<typeof vi.fn>).mock.calls.filter(
});
it("retries console capture after a domain enable fails during attach", async () => {
const { api } = fakeApi();
(api.sendCommand as ReturnType<typeof vi.fn>).mockImplementation(
async (_target, method: string) => {
if (method === "Log.enable") throw new Error("restricted");
return {};
},
);
const cdp = new ChromiumCdp(api);
// Attach is best-effort: Log.enable fails but attach still succeeds,
// leaving the tab unmarked so capture can be retried.
await cdp.ensureAttached(12);
const afterAttach = (api.sendCommand as ReturnType<typeof vi.fn>).mock.calls.filter(
([, method]) => method === "Runtime.enable" || method === "Log.enable",
);
expect(enableCalls).toHaveLength(2);
expect(afterAttach).toHaveLength(2);
// Before the fix the "attempted" flag was set before success, so this
// was a no-op and the tab silently returned no console output forever.
// Now it retries both domains.
await cdp.ensureConsoleCapture(12);
const afterRetry = (api.sendCommand as ReturnType<typeof vi.fn>).mock.calls.filter(
([, method]) => method === "Runtime.enable" || method === "Log.enable",
);
expect(afterRetry).toHaveLength(4);
});
it("retries and surfaces Network.enable failures for explicit capture", async () => {
@@ -127,7 +127,7 @@ export class ChromiumCdp {
private readonly dialogSequences = new Map<number, number>();
private readonly consoleBuffers = new Map<number, ConsoleEntry[]>();
private readonly consoleSequences = new Map<number, number>();
private readonly consoleDomainsAttemptedTabs = new Set<number>();
private readonly consoleDomainsEnabledTabs = new Set<number>();
private readonly networkBuffers = new Map<number, NetworkEntry[]>();
private readonly networkSequences = new Map<number, number>();
private readonly networkDomainsEnabledTabs = new Set<number>();
@@ -313,7 +313,7 @@ export class ChromiumCdp {
this.dialogSequences.clear();
this.consoleBuffers.clear();
this.consoleSequences.clear();
this.consoleDomainsAttemptedTabs.clear();
this.consoleDomainsEnabledTabs.clear();
this.networkBuffers.clear();
this.networkSequences.clear();
this.networkDomainsEnabledTabs.clear();
@@ -334,15 +334,24 @@ export class ChromiumCdp {
}
private async enableConsoleDomains(tabId: number): Promise<void> {
if (this.consoleDomainsAttemptedTabs.has(tabId)) return;
this.consoleDomainsAttemptedTabs.add(tabId);
if (this.consoleDomainsEnabledTabs.has(tabId)) return;
// Mark the tab only after both domains enable successfully — a
// transient failure (e.g. restricted page during attach) must leave
// the tab unmarked so a later `ensureConsoleCapture` can retry,
// instead of silently returning no console output forever. Mirrors
// `enableNetworkDomain`, which records the tab after success only.
let failed = false;
for (const method of ["Runtime.enable", "Log.enable"]) {
try {
await this.api.sendCommand({ tabId }, method, {});
} catch (err) {
failed = true;
console.debug("[bsk cdp] console domain enable failed", { tabId, method, err });
}
}
if (!failed) {
this.consoleDomainsEnabledTabs.add(tabId);
}
}
private async enableNetworkDomain(tabId: number): Promise<void> {
@@ -445,7 +454,7 @@ export class ChromiumCdp {
private clearConsoleState(tabId: number): void {
this.consoleBuffers.delete(tabId);
this.consoleSequences.delete(tabId);
this.consoleDomainsAttemptedTabs.delete(tabId);
this.consoleDomainsEnabledTabs.delete(tabId);
}
private bindNetworkHandler(): void {