From c8dfac0e5d5d11c5550451d6f455b867b32eedd2 Mon Sep 17 00:00:00 2001 From: lucas-felicity Date: Tue, 26 Aug 2025 22:26:53 -0400 Subject: [PATCH] Don't rely on module-level state for Playwright selector engine registration (#1022) # why This is to fix some undesired behavior for a common dev workflow with `next dev`. Introduced in https://github.com/browserbase/stagehand/issues/954. There is now module-level state in `lib/StagehandPage.ts` (the `stagehandSelectorRegistered` boolean) used to guard against multiple calls to `selectors.register` (a Playwright function which sets module-level state). This used in the function `ensureStagehandSelectorEngine`. This guard exists because calling `selectors.register` with the same string more than once will cause an error. The problem is that `next dev` repeatedly reloads the `stagehand` module whenever we first start up our dev server or make changes, but without always reloading the underlying `playwright` module.
So, we get lots of errors like this. ```zsh [2025-08-22 12:58:10] web:dev: Error in Inngest task { [2025-08-22 12:58:10] web:dev: error: { [2025-08-22 12:58:10] web:dev: error: 'NonRetriableError', [2025-08-22 12:58:10] web:dev: message: "Hey! We're sorry you ran into an error. \n" + [2025-08-22 12:58:10] web:dev: 'Stagehand version: 2.4.3 \n' + [2025-08-22 12:58:10] web:dev: 'If you need help, please open a Github issue or reach out to us on Slack: https://stagehand.dev/slack\n' + [2025-08-22 12:58:10] web:dev: '\n' + [2025-08-22 12:58:10] web:dev: 'Full error:\n' + [2025-08-22 12:58:10] web:dev: 'selectors.register: "stagehand" selector engine has been already registered', [2025-08-22 12:58:10] web:dev: name: 'Error', [2025-08-22 12:58:10] web:dev: stack: 'StagehandDefaultError: \n' + [2025-08-22 12:58:10] web:dev: "Hey! We're sorry you ran into an error. \n" + [2025-08-22 12:58:10] web:dev: 'Stagehand version: 2.4.3 \n' + [2025-08-22 12:58:10] web:dev: 'If you need help, please open a Github issue or reach out to us on Slack: https://stagehand.dev/slack\n' + [2025-08-22 12:58:10] web:dev: '\n' + [2025-08-22 12:58:10] web:dev: 'Full error:\n' + [2025-08-22 12:58:10] web:dev: 'selectors.register: "stagehand" selector engine has been already registered\n' + [2025-08-22 12:58:10] web:dev: ' at _StagehandPage.eval (webpack-internal:///(rsc)/../../node_modules/.pnpm/@browserbasehq+stagehand@2.4.3_deepmerge@4.3.1_dotenv@16.6.1_react@18.3.1_zod@3.25.67/node_modules/@browserbasehq/stagehand/dist/index.js:4077:15)\n' + [2025-08-22 12:58:10] web:dev: ' at Generator.throw ()\n' + [2025-08-22 12:58:10] web:dev: ' at rejected (webpack-internal:///(rsc)/../../node_modules/.pnpm/@browserbasehq+stagehand@2.4.3_deepmerge@4.3.1_dotenv@16.6.1_react@18.3.1_zod@3.25.67/node_modules/@browserbasehq/stagehand/dist/index.js:73:29)' [2025-08-22 12:58:10] web:dev: }, ```
**TL;DR The `stagehand` module state guard, to guard the Playwright module state, becomes out of sync with Playwright.** This is not really `stagehand`'s "fault". It appears to be `next`-specific behavior combined with some logic to get around funky module-level `playwright` state. But it is causing a lot of friction on our team; I think module-level state is risky in general for this reason. # what changed My proposed fix is to wrap this `selectors.`register call in a specific `try`/`catch` that looks for, and ignores, the specific error `/selector engine has been already registered/` in `packages/playwright-core/src/client/selectors.ts` instead of using the `stagehandSelectorRegistered` boolean. # test plan Existing evals. And this works locally as expected when I build our system against this version, but without the error, no matter how many times the module is reloaded. --- .changeset/neat-walls-walk.md | 5 +++++ lib/StagehandPage.ts | 23 +++++++++++++++-------- 2 files changed, 20 insertions(+), 8 deletions(-) create mode 100644 .changeset/neat-walls-walk.md diff --git a/.changeset/neat-walls-walk.md b/.changeset/neat-walls-walk.md new file mode 100644 index 000000000..bd88aa32e --- /dev/null +++ b/.changeset/neat-walls-walk.md @@ -0,0 +1,5 @@ +--- +"@browserbasehq/stagehand-lib": patch +--- + +Fixed small issue with module-level state guard for the Playwright selectors.register call diff --git a/lib/StagehandPage.ts b/lib/StagehandPage.ts index e4fd45dfb..b34a2f302 100644 --- a/lib/StagehandPage.ts +++ b/lib/StagehandPage.ts @@ -38,9 +38,6 @@ async function getCurrentRootFrameId(session: CDPSession): Promise { return frameTree.frame.id; } -/** ensure we register the custom selector only once per process */ -let stagehandSelectorRegistered = false; - export class StagehandPage { private stagehand: Stagehand; private rawPage: PlaywrightPage; @@ -197,10 +194,7 @@ ${scriptContent} \ /** Register the custom selector engine that pierces open/closed shadow roots. */ private async ensureStagehandSelectorEngine(): Promise { - if (stagehandSelectorRegistered) return; - stagehandSelectorRegistered = true; - - await selectors.register("stagehand", () => { + const registerFn = () => { type Backdoor = { getClosedRoot?: (host: Element) => ShadowRoot | undefined; }; @@ -299,7 +293,20 @@ ${scriptContent} \ return out; }, }; - }); + }; + + try { + await selectors.register("stagehand", registerFn); + } catch (err) { + if ( + err instanceof Error && + err.message.match(/selector engine has been already registered/) + ) { + // ignore + } else { + throw err; + } + } } /**