Skip to content

Restore active-page waiting in V4 - #2430

Closed
monadoid wants to merge 1 commit into
v4-spikefrom
restore-active-page-waiting
Closed

Restore active-page waiting in V4#2430
monadoid wants to merge 1 commit into
v4-spikefrom
restore-active-page-waiting

Conversation

@monadoid

@monadoid monadoid commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Adds in the missing awaitActivePage - we only had await activePage() which functioned differently.

V4 could read the active page before a newly opened tab had registered. This restores V3’s popup-aware active-page waiting across the protocol, server, and TypeScript/Python SDKs, and removes fixed RPC response deadlines.

Configuration tab_handling multi_tab
V3 control 3/3 3/3
V4 baseline 0/3 0/3
V4 with popup-aware awaitActivePage() and no fixed RPC response deadlines 3/3 3/3

Summary by cubic

Restores V3-style, popup-aware active-page waiting in V4 and adds context.await_active_page across the protocol and TS/Python SDKs. Also removes fixed RPC response deadlines to avoid premature failures while popups register.

  • New Features

    • New JSON‑RPC method context.await_active_page with optional timeout (ms); returns PageRef.
    • TS: BrowserContext.awaitActivePage(timeoutMs?); Python: BrowserContext.await_active_page(timeout=None).
    • Runtime tracks Page.windowOpen/Target.targetCreated to wait for newly opened tabs before resolving.
  • Bug Fixes

    • Prevents reading the wrong active page during popup creation; falls back to the prior page on timeout and throws PageNotFoundError if none is available.
    • Removes fixed per‑request RPC timeouts in SDK and worker clients; requests stay pending until the transport responds or closes.
    • Validated on Browserbase with google/gemini-2.5-flash: V4 now 3/3 for tab_handling and multi_tab under concurrency.

Written for commit e28edcf. Summary will update on new commits.

Review in cubic

@changeset-bot

changeset-bot Bot commented Jul 26, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: e28edcf

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@monadoid
monadoid marked this pull request as ready for review July 26, 2026 09:57

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

5 issues found across 27 files

Confidence score: 2/5

  • In packages/server/understudy/context.ts, popup detection can race with concurrent target creation and bind to the wrong tab, so callers may automate an unrelated page instead of the popup; tie the popup signal to Target.targetCreated.info.targetId and wait for that target’s Page entry before returning.
  • In packages/server/understudy/context.ts (activePage() path), a popup that arrives during the Chrome await can be missed, causing the previous page to be returned immediately and skipping intended popup handling; re-check popup recency after the await so in-flight events still enter the registration wait.
  • In packages/sdk-python/src/stagehand/browser_context.py, await_active_page() with the documented default (timeout=None) fails client-side before RPC, which breaks the default call path for Python users; avoid marking timeout as explicitly set when it is None so the request can be sent.
  • Coverage is thin for the protocol/RPC behavior changes in packages/sdk-python/src/stagehand/rpc_client.py and packages/protocol/schemas.ts, so regressions in indefinite waits, cancellation cleanup, and timeout validation may slip through; add focused tests for delayed response/cancel flows and omitted/valid/invalid timeout payloads.
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/sdk-python/src/stagehand/rpc_client.py">

<violation number="1" location="packages/sdk-python/src/stagehand/rpc_client.py:147">
P3: The removed RPC deadline has no regression coverage for its new indefinite-wait behavior. Add focused tests for a delayed response and cancellation so pending-request cleanup remains guaranteed.

(Based on your team's feedback about unit tests for changed behavior.) [FEEDBACK_USED]</violation>
</file>

<file name="packages/protocol/schemas.ts">

<violation number="1" location="packages/protocol/schemas.ts:1378">
P3: New `context.await_active_page` wire validation has no regression coverage. Add focused parsing tests for omitted/valid timeout and rejected negative, fractional, or extra fields.</violation>
</file>

<file name="packages/server/understudy/context.ts">

<violation number="1" location="packages/server/understudy/context.ts:288">
P1: A popup arriving while `activePage()` is awaiting Chrome can be missed and return the prior page immediately. Evaluate popup recency after the await so events delivered during the lookup enter the registration wait.</violation>

<violation number="2" location="packages/server/understudy/context.ts:297">
P1: Concurrent target creation can return an unrelated newly registered tab instead of the popup. Associate `Target.targetCreated`'s `info.targetId` with the popup signal and wait for that target's `Page` entry before falling back.</violation>
</file>

<file name="packages/sdk-python/src/stagehand/browser_context.py">

<violation number="1" location="packages/sdk-python/src/stagehand/browser_context.py:72">
P2: Calling `await_active_page()` with the documented default currently fails before the RPC is sent: `ContextAwaitActivePageParams(timeout=None)` marks `timeout` as explicitly set, and the Python RPC client rejects the resulting `null` value against the integer-only protocol field. Construct the params model without the field when `timeout` is `None`, so the server can apply its default timeout.</violation>
</file>

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

Re-trigger cubic

while (Date.now() < deadline) {
let newestTargetId: TargetId | undefined;
let newestCreatedAt = -1;
for (const [targetId] of this.pagesByTarget) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Concurrent target creation can return an unrelated newly registered tab instead of the popup. Associate Target.targetCreated's info.targetId with the popup signal and wait for that target's Page entry before falling back.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/server/understudy/context.ts, line 297:

<comment>Concurrent target creation can return an unrelated newly registered tab instead of the popup. Associate `Target.targetCreated`'s `info.targetId` with the popup signal and wait for that target's `Page` entry before falling back.</comment>

<file context>
@@ -271,6 +272,46 @@ export class V3Context {
+    while (Date.now() < deadline) {
+      let newestTargetId: TargetId | undefined;
+      let newestCreatedAt = -1;
+      for (const [targetId] of this.pagesByTarget) {
+        const createdAt = this.createdAtByTarget.get(targetId) ?? 0;
+        if (createdAt > newestCreatedAt) {
</file context>

const timeout = timeoutMs ?? defaultTimeout;
const recentWindowMs = this.env === "BROWSERBASE" ? 1000 : 300;
const now = Date.now();
const hasRecentPopup = now - this._lastPopupSignalAt <= recentWindowMs;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: A popup arriving while activePage() is awaiting Chrome can be missed and return the prior page immediately. Evaluate popup recency after the await so events delivered during the lookup enter the registration wait.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/server/understudy/context.ts, line 288:

<comment>A popup arriving while `activePage()` is awaiting Chrome can be missed and return the prior page immediately. Evaluate popup recency after the await so events delivered during the lookup enter the registration wait.</comment>

<file context>
@@ -271,6 +272,46 @@ export class V3Context {
+    const timeout = timeoutMs ?? defaultTimeout;
+    const recentWindowMs = this.env === "BROWSERBASE" ? 1000 : 300;
+    const now = Date.now();
+    const hasRecentPopup = now - this._lastPopupSignalAt <= recentWindowMs;
+
+    const immediate = await this.activePage();
</file context>

return None if result.root is None else Page(self._rpc_client, result.root)

async def await_active_page(self, timeout: int | None = None) -> Page:
params = ContextAwaitActivePageParams(timeout=timeout)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Calling await_active_page() with the documented default currently fails before the RPC is sent: ContextAwaitActivePageParams(timeout=None) marks timeout as explicitly set, and the Python RPC client rejects the resulting null value against the integer-only protocol field. Construct the params model without the field when timeout is None, so the server can apply its default timeout.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk-python/src/stagehand/browser_context.py, line 72:

<comment>Calling `await_active_page()` with the documented default currently fails before the RPC is sent: `ContextAwaitActivePageParams(timeout=None)` marks `timeout` as explicitly set, and the Python RPC client rejects the resulting `null` value against the integer-only protocol field. Construct the params model without the field when `timeout` is `None`, so the server can apply its default timeout.</comment>

<file context>
@@ -67,6 +68,15 @@ async def active_page(self) -> Page | None:
         return None if result.root is None else Page(self._rpc_client, result.root)
 
+    async def await_active_page(self, timeout: int | None = None) -> Page:
+        params = ContextAwaitActivePageParams(timeout=timeout)
+        page_ref = await self._rpc_client.send(
+            "context.await_active_page",
</file context>
Suggested change
params = ContextAwaitActivePageParams(timeout=timeout)
params = ContextAwaitActivePageParams() if timeout is None else ContextAwaitActivePageParams(timeout=timeout)

except TimeoutError as error:
raise TimeoutError(f"RPC request timed out: {method}") from error
)
result = await response

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: The removed RPC deadline has no regression coverage for its new indefinite-wait behavior. Add focused tests for a delayed response and cancellation so pending-request cleanup remains guaranteed.

(Based on your team's feedback about unit tests for changed behavior.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk-python/src/stagehand/rpc_client.py, line 147:

<comment>The removed RPC deadline has no regression coverage for its new indefinite-wait behavior. Add focused tests for a delayed response and cancellation so pending-request cleanup remains guaranteed.

(Based on your team's feedback about unit tests for changed behavior.) </comment>

<file context>
@@ -142,17 +138,14 @@ async def send(
-        except TimeoutError as error:
-            raise TimeoutError(f"RPC request timed out: {method}") from error
+            )
+            result = await response
+            return cast(ResultT, result)
         finally:
</file context>

.strict()
.meta({ id: "ContextNewPageParams" });

export const ContextAwaitActivePageParamsSchema = z

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: New context.await_active_page wire validation has no regression coverage. Add focused parsing tests for omitted/valid timeout and rejected negative, fractional, or extra fields.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/protocol/schemas.ts, line 1378:

<comment>New `context.await_active_page` wire validation has no regression coverage. Add focused parsing tests for omitted/valid timeout and rejected negative, fractional, or extra fields.</comment>

<file context>
@@ -1375,6 +1375,15 @@ export const ContextNewPageParamsSchema = z
   .strict()
   .meta({ id: "ContextNewPageParams" });
 
+export const ContextAwaitActivePageParamsSchema = z
+  .object({
+    timeout: z.number().int().nonnegative().optional().meta({
</file context>

@monadoid
monadoid marked this pull request as draft July 27, 2026 17:15
@monadoid monadoid closed this Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant