Skip to content

feat(sdk-ts): add configurable RPC call controls (#2925) - #3000

Open
seanmcguire12 wants to merge 1 commit into
mainfrom
evals/pr1m8-replit-configurable-rpc-timeouts
Open

seanmcguire12 wants to merge 1 commit into
mainfrom
evals/pr1m8-replit-configurable-rpc-timeouts

Conversation

@seanmcguire12

@seanmcguire12 seanmcguire12 commented Sep 21, 2026

Copy link
Copy Markdown
Member

this PR is a mirror of the original #2925

why

Long-running Stagehand RPCs need two forms of caller control:

  • A bounded response wait for environments that cannot wait indefinitely.
  • A way to stop waiting when the current caller or notebook cell is cancelled.

Cancellation must not implicitly close Stagehand or the underlying browser. A later operation should still be able to use the existing session.

what changed

  • Add TypeScript-only rpcTimeouts to Stagehand.create():
    • defaultMs sets a response deadline for ordinary RPC calls.
    • methods overrides the default for specific Stagehand RPC methods.
  • Timeout values must be positive integers and are capped at 2_147_473_647 ms.
  • Configured values take precedence over Stagehand’s existing computed response deadlines.
  • Add TypeScript-only getCallOptions() to Stagehand.create():
    • Invoked once immediately before each ordinary outbound RPC call.
    • May return { signal?: AbortSignal }.
  • Its signal is combined with an existing call signal and the response-timeout signal.
    • A pre-aborted signal fails before the RPC is sent.
  • Keep stagehand.init and stagehand.callback_batch outside both hooks:
    • Initialization retains its existing lifecycle deadline.
  • experimentalBatch() retains its existing whole-batch timeout and cancellation behavior.
  • Re-throw transport send errors after rejecting their pending RPC request.
  • Export schemas and types, document the options, and add a minor Changeset.

test plan

  • pnpm --filter @browserbasehq/stagehand typecheck
  • pnpm --filter @browserbasehq/stagehand build
  • Production build passed publint.
  • pnpm --filter @browserbasehq/stagehand test:unit
    • 20 test files passed
    • 290 tests passed
  • Published-package consumer coverage verifies the new schemas and types.

Focused coverage verifies:

  • configured per-method timeout overrides the configured default;
  • existing fallback timeout behavior remains unchanged without configuration;
  • initialization and callback batches ignore the new hooks;
  • getCallOptions() is called once per ordinary RPC;
  • pre-aborted signals do not send an RPC;
  • aborting one ordinary RPC leaves the client usable for a later RPC;
  • transport send failures propagate to the caller.
---

Summary by cubic

Adds configurable response wait deadlines and per-call abort signals to ordinary Stagehand RPC calls, so callers can bound how long they wait and cancel a single call without closing the session.

  • Stagehand.create() accepts two new TypeScript-only options: rpcTimeouts (defaultMs with per-method overrides) and getCallOptions() returning an optional AbortSignal.
  • Configured timeouts take precedence over the SDK's existing computed deadlines; values must be positive integers capped at 2_147_473_647 ms.
  • getCallOptions() is invoked once before each ordinary RPC call, and a pre-aborted signal fails before the call is sent.
  • stagehand.init and experimentalBatch() ignore both hooks and keep their existing lifecycle timeout and cancellation behavior.
  • Aborting a single call leaves the Stagehand instance and its browser usable for later calls.
  • Transport send failures now surface to the caller as errors after their pending request is rejected.

Written for commit 807a930. Summary will update on new commits.

Review in cubic

# why

Long-running Stagehand RPCs need two forms of caller control:

- A bounded response wait for environments that cannot wait
indefinitely.
- A way to stop waiting when the current caller or notebook cell is
cancelled.

Cancellation must not implicitly close Stagehand or the underlying
browser. A later operation should still be able to use the existing
session.

# what changed

- Add TypeScript-only `rpcTimeouts` to `Stagehand.create()`:
  - `defaultMs` sets a response deadline for ordinary RPC calls.
  - `methods` overrides the default for specific Stagehand RPC methods.
- Timeout values must be positive integers and are capped at
`2_147_473_647` ms.
- Configured values take precedence over Stagehand’s existing computed
response deadlines.
- Add TypeScript-only `getCallOptions()` to `Stagehand.create()`:
  - Invoked once immediately before each ordinary outbound RPC call.
  - May return `{ signal?: AbortSignal }`.
- Its signal is combined with an existing call signal and the
response-timeout signal.
  - A pre-aborted signal fails before the RPC is sent.
- Keep `stagehand.init` and `stagehand.callback_batch` outside both
hooks:
  - Initialization retains its existing lifecycle deadline.
- `experimentalBatch()` retains its existing whole-batch timeout and
cancellation behavior.
- Re-throw transport send errors after rejecting their pending RPC
request.
- Export schemas and types, document the options, and add a minor
Changeset.

# test plan

- [x] `pnpm --filter @browserbasehq/stagehand typecheck`
- [x] `pnpm --filter @browserbasehq/stagehand build`
- [x] Production build passed `publint`.
- [x] `pnpm --filter @browserbasehq/stagehand test:unit`
  - 20 test files passed
  - 290 tests passed
- [x] Published-package consumer coverage verifies the new schemas and
types.

Focused coverage verifies:

- configured per-method timeout overrides the configured default;
- existing fallback timeout behavior remains unchanged without
configuration;
- initialization and callback batches ignore the new hooks;
- `getCallOptions()` is called once per ordinary RPC;
- pre-aborted signals do not send an RPC;
- aborting one ordinary RPC leaves the client usable for a later RPC;
- transport send failures propagate to the caller.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds configurable response wait deadlines and per-call abort signals to
ordinary Stagehand RPC calls, so callers can bound how long they wait
and cancel a single call without closing the session.

- `Stagehand.create()` accepts two new TypeScript-only options:
`rpcTimeouts` (`defaultMs` with per-method overrides) and
`getCallOptions()` returning an optional `AbortSignal`.
- Configured timeouts take precedence over the SDK's existing computed
deadlines; values must be positive integers capped at `2_147_473_647`
ms.
- `getCallOptions()` is invoked once before each ordinary RPC call, and
a pre-aborted signal fails before the call is sent.
- `stagehand.init` and `experimentalBatch()` ignore both hooks and keep
their existing lifecycle timeout and cancellation behavior.
- Aborting a single call leaves the Stagehand instance and its browser
usable for later calls.
- Transport send failures now surface to the caller as errors after
their pending request is rejected.

<sup>Written for commit 807a930.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2925?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->
@seanmcguire12
seanmcguire12 requested a review from a team as a code owner September 21, 2026 17:26
@mintlify

mintlify Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
stagehand 🟢 Ready View Preview Sep 21, 2026, 5:26 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

@changeset-bot

changeset-bot Bot commented Sep 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 51918a1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
Name Type
@browserbasehq/stagehand Minor
browse Patch
@browserbasehq/stagehand-integrations Patch
@browserbasehq/stagehand-integrations-example-eve-facade Patch
@browserbasehq/stagehand-integrations-example-pi-facade Patch
@browserbasehq/stagehand-integrations-claude-agent-sdk Patch
@browserbasehq/stagehand-integrations-example-claude-code-facade Patch
@browserbasehq/stagehand-integrations-codex-sdk Patch
@browserbasehq/stagehand-integrations-example-codex-facade Patch
@browserbasehq/stagehand-integrations-cursor-sdk Patch
@browserbasehq/stagehand-integrations-deepagents-sdk Patch
@browserbasehq/stagehand-integrations-eve-sdk Patch
@browserbasehq/stagehand-integrations-fx-sdk Patch
@browserbasehq/stagehand-integrations-mastra-sdk Patch
@browserbasehq/stagehand-integrations-example-mastra-facade Patch
@browserbasehq/stagehand-integrations-pi-sdk Patch
@browserbasehq/stagehand-integrations-example-vercel-ai-facade Patch

Not sure what this means? Click here to learn what changesets are.

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

@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.

3 issues found across 8 files

Confidence score: 3/5

  • packages/sdk-ts/src/clientSchemas.ts can silently ignore timeout configuration when an RPC method name is misspelled, making callers believe a timeout is enforced when it is not; validate methods keys against registered method names.
  • packages/sdk-ts/src/rpcClient.ts invokes getCallOptions even when local parameter validation prevents an RPC from being sent, which can trigger unnecessary or misleading side effects; move the hook next to the actual request dispatch.
  • packages/docs/v4/reference/stagehand.mdx currently instructs callers to return a signal instead of an options object, causing the documented usage to be rejected by StagehandCallOptionsSchema; update the example and wording to return { signal: controller.signal }.
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/docs/v4/reference/stagehand.mdx">

<violation number="1" location="packages/docs/v4/reference/stagehand.mdx:85">
P2: Tell callers to return an options object, such as `{ signal: controller.signal }`, rather than the signal itself. Following the current prose produces a value that `StagehandCallOptionsSchema` rejects.</violation>
</file>

<file name="packages/sdk-ts/src/clientSchemas.ts">

<violation number="1" location="packages/sdk-ts/src/clientSchemas.ts:284">
P2: When a caller misspells an RPC method name, this schema accepts the key and the requested timeout is silently ignored. Validate `methods` keys against the registered method names so timeout configuration errors fail during `Stagehand.create()`.</violation>
</file>

<file name="packages/sdk-ts/src/rpcClient.ts">

<violation number="1" location="packages/sdk-ts/src/rpcClient.ts:185">
P2: When local parameter validation fails, `getCallOptions` still runs even though `send` never issues an RPC. Move the hook invocation to immediately before `waitForResponse`/`cdp.send` so it runs only for an actual ordinary outbound call.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client as Client Code
    participant SH as Stagehand Class
    participant RPC as RPCClient
    participant CDP as CDP Transport
    participant Browser as Browser Session

    Note over Client,Browser: Stagehand RPC Call Flow with Configurable Controls

    Client->>SH: Stagehand.create({ rpcTimeouts, getCallOptions })
    SH->>SH: Parse options via StagehandCreateOptionsSchema
    SH->>RPC: Constructor(cdp, { rpcTimeouts, getCallOptions })

    Client->>RPC: send(method, params, options)
    RPC->>RPC: Check if method is init or callback_batch
    
    alt Ordinary RPC call
        RPC->>RPC: callOptions = getCallOptions?.()
        RPC->>RPC: Validate callOptions with StagehandCallOptionsSchema
        RPC->>RPC: Combine signals: options.signal + callOptions.signal
        RPC->>RPC: throwIfAborted(combinedSignal)
        
        Note over RPC: Pre-aborted signal fails here without sending
        
        RPC->>RPC: Determine timeout via rpcResponseTimeoutMs()
        
        alt Configured timeout exists
            RPC->>RPC: Use rpcTimeouts.methods[method] ?? rpcTimeouts.defaultMs
        else No configured timeout
            RPC->>RPC: Use computed deadline (legacy behavior)
        end
        
        RPC->>RPC: Create timeout AbortController if needed
        RPC->>RPC: Combine callSignal + timeoutSignal
        RPC->>CDP: Send JSON-RPC request with signal
        
        alt Request succeeds
            CDP-->>RPC: Response
            RPC-->>Client: Result
        else Signal aborted (timeout or user)
            RPC->>RPC: Reject pending request with abort reason
            Note over RPC: Client remains usable
            RPC-->>Client: Error
        else Transport send fails
            CDP-->>RPC: Send error
            RPC->>RPC: Reject pending request
            RPC->>RPC: Re-throw transport error
            RPC-->>Client: Transport error
        end
        
    else Init or callback_batch
        Note over RPC: Skips getCallOptions and configured timeouts
        RPC->>CDP: Send with existing lifecycle/deadline behavior
    end

    Note over RPC,Browser: Aborting an RPC does not close browser or Stagehand
Loading

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

Re-trigger cubic

</ParamField>

<ParamField path="getCallOptions" type="() => { signal?: AbortSignal } | undefined" optional>
Called immediately before each ordinary RPC call. Return an `AbortSignal` to stop waiting for that call when the signal aborts. Aborting a call does not close the Stagehand instance or its browser.

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: Tell callers to return an options object, such as { signal: controller.signal }, rather than the signal itself. Following the current prose produces a value that StagehandCallOptionsSchema rejects.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/docs/v4/reference/stagehand.mdx, line 85:

<comment>Tell callers to return an options object, such as `{ signal: controller.signal }`, rather than the signal itself. Following the current prose produces a value that `StagehandCallOptionsSchema` rejects.</comment>

<file context>
@@ -77,6 +77,14 @@ const stagehand = await Stagehand.create({ browser });
+</ParamField>
+
+<ParamField path="getCallOptions" type="() => { signal?: AbortSignal } | undefined" optional>
+  Called immediately before each ordinary RPC call. Return an `AbortSignal` to stop waiting for that call when the signal aborts. Aborting a call does not close the Stagehand instance or its browser.
+</ParamField>
+
</file context>
Suggested change
Called immediately before each ordinary RPC call. Return an `AbortSignal` to stop waiting for that call when the signal aborts. Aborting a call does not close the Stagehand instance or its browser.
Called immediately before each ordinary RPC call. Return `{ signal: controller.signal }` (or `undefined`) to stop waiting for that call when the signal aborts. Aborting a call does not close the Stagehand instance or its browser.

export const StagehandRPCTimeoutsSchema = z
.strictObject({
defaultMs: RPCTimeoutMsSchema.optional(),
methods: z.record(z.string(), RPCTimeoutMsSchema).optional(),

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: When a caller misspells an RPC method name, this schema accepts the key and the requested timeout is silently ignored. Validate methods keys against the registered method names so timeout configuration errors fail during Stagehand.create().

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk-ts/src/clientSchemas.ts, line 284:

<comment>When a caller misspells an RPC method name, this schema accepts the key and the requested timeout is silently ignored. Validate `methods` keys against the registered method names so timeout configuration errors fail during `Stagehand.create()`.</comment>

<file context>
@@ -267,8 +268,40 @@ export const StagehandBrowserSchema = z
+export const StagehandRPCTimeoutsSchema = z
+  .strictObject({
+    defaultMs: RPCTimeoutMsSchema.optional(),
+    methods: z.record(z.string(), RPCTimeoutMsSchema).optional(),
+  })
+  .meta({ id: "StagehandRPCTimeouts" });
</file context>

method.name === StagehandMethods.stagehandInit.name ||
method.name === StagehandMethods.stagehandCallbackBatch.name
? undefined
: this.getCallOptions?.();

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: When local parameter validation fails, getCallOptions still runs even though send never issues an RPC. Move the hook invocation to immediately before waitForResponse/cdp.send so it runs only for an actual ordinary outbound call.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk-ts/src/rpcClient.ts, line 185:

<comment>When local parameter validation fails, `getCallOptions` still runs even though `send` never issues an RPC. Move the hook invocation to immediately before `waitForResponse`/`cdp.send` so it runs only for an actual ordinary outbound call.</comment>

<file context>
@@ -163,6 +178,16 @@ export class RPCClient {
+      method.name === StagehandMethods.stagehandInit.name ||
+      method.name === StagehandMethods.stagehandCallbackBatch.name
+        ? undefined
+        : this.getCallOptions?.();
+    const callSignal = combineAbortSignals(
+      options.signal,
</file context>

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.

2 participants