Skip to content

fix(browser): recover stale CDP sessions#112

Open
MagMueller wants to merge 2 commits into
mainfrom
session-recovery
Open

fix(browser): recover stale CDP sessions#112
MagMueller wants to merge 2 commits into
mainfrom
session-recovery

Conversation

@MagMueller

@MagMueller MagMueller commented Jul 23, 2026

Copy link
Copy Markdown

Summary

  • keep the current CDP socket active until a replacement WebSocket has actually opened, and scope pending calls to the socket that owns them
  • on Chrome’s explicit Session with given id not found rejection, perform one serialized reattach to a usable page (or create about:blank) and retry once
  • restore Page, DOM, Runtime, and Network subscriptions on the replacement session so event-driven code keeps working
  • deliberately do not replay commands after an ambiguous socket drop, where Chrome may already have applied the action
  • cover concurrent stale-session failures, empty/internal target lists, dropped sockets, failed replacement connections, late closes from replaced sockets, and domain restoration

This ports the narrow, deterministic session-recovery behavior from the public browser-harness daemon while keeping transport-drop recovery conservative.

Validation

  • bun test in packages/bcode-browser: 18 pass, 8 environment-gated skips
  • bun typecheck in packages/bcode-browser
  • root filtered typecheck: 13/13 packages pass
  • bunx oxlint packages/bcode-browser/test/cdp-recovery.test.ts: clean
  • recovery test file repeated 10 times without failure

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/bcode-browser/src/cdp/session.ts">

<violation number="1" location="packages/bcode-browser/src/cdp/session.ts:291">
P2: Recovery restores only four hard-coded domains, so callers that enabled `Debugger`, `Fetch`, or another generated domain lose their subscription after reattach; it also enables unrequested domains. In particular, unrequested `Runtime.enable` immediately emits existing-context events, which can resolve an in-flight `waitFor` callback before its intended event; record successful `*.enable` calls and their params, then replay only those.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

?? (await this.domains.Target.createTarget({ url: 'about:blank' })).targetId;
const sessionId = await this.use(targetId);
await Promise.allSettled(
['Page', 'DOM', 'Runtime', 'Network'].map(

@cubic-dev-ai cubic-dev-ai Bot Jul 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Recovery restores only four hard-coded domains, so callers that enabled Debugger, Fetch, or another generated domain lose their subscription after reattach; it also enables unrequested domains. In particular, unrequested Runtime.enable immediately emits existing-context events, which can resolve an in-flight waitFor callback before its intended event; record successful *.enable calls and their params, then replay only those.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/bcode-browser/src/cdp/session.ts, line 291:

<comment>Recovery restores only four hard-coded domains, so callers that enabled `Debugger`, `Fetch`, or another generated domain lose their subscription after reattach; it also enables unrequested domains. In particular, unrequested `Runtime.enable` immediately emits existing-context events, which can resolve an in-flight `waitFor` callback before its intended event; record successful `*.enable` calls and their params, then replay only those.</comment>

<file context>
@@ -286,7 +286,12 @@ export class Session implements Transport {
-    await this.use(targetId);
+    const sessionId = await this.use(targetId);
+    await Promise.allSettled(
+      ['Page', 'DOM', 'Runtime', 'Network'].map(
+        domain => this.send(`${domain}.enable`, {}, sessionId),
+      ),
</file context>
Fix with cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/bcode-browser/src/cdp/session.ts">

<violation number="1" location="packages/bcode-browser/src/cdp/session.ts:236">
P1: Page-domain calls issued during reattachment are sent without `sessionId` and fail rather than recovering. Keep the stale ID until the serialized reattach replaces it, so overlapping calls receive Chrome’s explicit stale-session rejection and join the same recovery.</violation>

<violation number="2" location="packages/bcode-browser/src/cdp/session.ts:418">
P3: Recovery opens an extra blank tab whenever the only usable page is already `about:blank`. Treat existing blank targets as usable; the fallback should create a tab only when no attachable page exists.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment on lines +236 to +237
if (this.activeSessionId === sentSessionId) this.activeSessionId = undefined;
if (!this.activeSessionId) await this.reattachFirstPage();

@cubic-dev-ai cubic-dev-ai Bot Jul 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Page-domain calls issued during reattachment are sent without sessionId and fail rather than recovering. Keep the stale ID until the serialized reattach replaces it, so overlapping calls receive Chrome’s explicit stale-session rejection and join the same recovery.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/bcode-browser/src/cdp/session.ts, line 236:

<comment>Page-domain calls issued during reattachment are sent without `sessionId` and fail rather than recovering. Keep the stale ID until the serialized reattach replaces it, so overlapping calls receive Chrome’s explicit stale-session rejection and join the same recovery.</comment>

<file context>
@@ -206,17 +222,36 @@ export class Session implements Transport {
+      // Chrome explicitly rejected the command before executing it, so this is
+      // safe to retry once. Socket drops are deliberately not retried: Chrome
+      // may have applied a click or submission before the response was lost.
+      if (this.activeSessionId === sentSessionId) this.activeSessionId = undefined;
+      if (!this.activeSessionId) await this.reattachFirstPage();
+      if (!this.activeSessionId) throw error;
</file context>
Suggested change
if (this.activeSessionId === sentSessionId) this.activeSessionId = undefined;
if (!this.activeSessionId) await this.reattachFirstPage();
if (this.activeSessionId === sentSessionId) await this.reattachFirstPage();
if (!this.activeSessionId || this.activeSessionId === sentSessionId) throw error;
Fix with cubic

&& !target.url.startsWith('chrome-untrusted://')
&& !target.url.startsWith('devtools://')
&& !target.url.startsWith('chrome-extension://')
&& !target.url.startsWith('about:');

@cubic-dev-ai cubic-dev-ai Bot Jul 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Recovery opens an extra blank tab whenever the only usable page is already about:blank. Treat existing blank targets as usable; the fallback should create a tab only when no attachable page exists.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/bcode-browser/src/cdp/session.ts, line 413:

<comment>Recovery opens an extra blank tab whenever the only usable page is already `about:blank`. Treat existing blank targets as usable; the fallback should create a tab only when no attachable page exists.</comment>

<file context>
@@ -329,6 +404,15 @@ export async function listPageTargets(session: Session): Promise<PageTarget[]> {
+    && !target.url.startsWith('chrome-untrusted://')
+    && !target.url.startsWith('devtools://')
+    && !target.url.startsWith('chrome-extension://')
+    && !target.url.startsWith('about:');
+}
+
</file context>
Fix with cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant