From c19f685833ef28a5e8ffc544206d0223a640765f Mon Sep 17 00:00:00 2001 From: miguel Date: Sat, 19 Sep 2026 11:01:08 -0700 Subject: [PATCH 1/3] perf(extension): overlap Jev intent with DOM settle and observe's snapshot --- packages/extension/services/actService.ts | 19 +++++- packages/extension/services/jevAct/README.md | 6 ++ packages/extension/services/jevAct/observe.ts | 10 ++- .../extension/services/jevAct/pipeline.ts | 8 +++ .../extension/tests/jevActPipeline.test.ts | 57 ++++++++++++++++++ .../extensionassets/stagehand-extension.zip | Bin 484173 -> 479417 bytes 6 files changed, 95 insertions(+), 5 deletions(-) diff --git a/packages/extension/services/actService.ts b/packages/extension/services/actService.ts index fdbf22cd8..a1e2b3a20 100644 --- a/packages/extension/services/actService.ts +++ b/packages/extension/services/actService.ts @@ -157,7 +157,14 @@ export async function act({ }, } : undefined; - await waitForDomNetworkQuiet(page.mainFrame(), logger, domSettleTimeoutMs); + // With Jev on, the intent request (which needs no page) runs while the DOM + // settles; everything that reads or touches the page still waits for it. + // performance.now(): tests script Date.now() for inference timing. + const actStartedAt = performance.now(); + const settled = waitForDomNetworkQuiet(page.mainFrame(), logger, domSettleTimeoutMs); + const overlapSettle = jevAct !== undefined && jevAct.enabled !== false; + if (overlapSettle) settled.catch(() => {}); + else await settled; ensureTimeRemaining(); let actPath: "llm" | "jev" | "jev+arg-llm" | "jev+llm" | "jev-tool" | "jev-tool+arg-llm" = "llm"; let usedArgumentLlm = false; @@ -175,9 +182,11 @@ export async function act({ bypass: cacheService.shouldBypassCacheForLocatorScope(options), context: cache, logger, - onHit: (value) => replayCachedActions(value, instruction, variables, context), + onHit: async (value) => { + await settled; + return await replayCachedActions(value, instruction, variables, context); + }, execute: async () => { - // performance.now(): tests script Date.now() for inference timing. const startedAt = performance.now(); const result = await runActPipeline(); // Whatever Jev already did changed the page, whether or not the act @@ -195,6 +204,8 @@ export async function act({ path: actPath, success: result.data.success, durationMs: Math.round(performance.now() - startedAt), + // From the start of act(), DOM settle included: what the caller waits for. + totalMs: Math.round(performance.now() - actStartedAt), llmInputTokens: result.metadata.usage.inputTokens, llmOutputTokens: result.metadata.usage.outputTokens, llmMs: result.metadata.usage.inferenceTimeMs, @@ -229,6 +240,7 @@ export async function act({ snapshotOptions, ensureTimeRemaining, openPageCount, + settled, ...(webmcp ? { webmcp } : {}), extractText: async (text) => { const response = await inference.actTextArgument({ @@ -285,6 +297,7 @@ export async function act({ } } + await settled; const { combinedTree, combinedXpathMap } = await page.captureSnapshot(snapshotOptions); const actPrompt = buildActPrompt( diff --git a/packages/extension/services/jevAct/README.md b/packages/extension/services/jevAct/README.md index 08f504a27..8fc20979b 100644 --- a/packages/extension/services/jevAct/README.md +++ b/packages/extension/services/jevAct/README.md @@ -24,6 +24,12 @@ deliberately not a field of the public create config. Evals build that variable | Checks | code | Fill read-back, native `