Skip to content

[feat]: add timeout support to locator resolvers - #3023

Open
seanmcguire12 wants to merge 4 commits into
add-locator-operation-contextfrom
share-operation-context-in-locator-helpers
Open

seanmcguire12 wants to merge 4 commits into
add-locator-operation-contextfrom
share-operation-context-in-locator-helpers

Conversation

@seanmcguire12

@seanmcguire12 seanmcguire12 commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

why

resolving a the node/object ID that a locator points to can involve crossing iframes, waiting for browser helpers, & looking up the element. these steps need to share the caller's deadline so each frame or retry cannot restart the timeout.

this pr connects that deadline to node/object ID resolution. the next PR in this stack will connect locator actions & reads to these helpers. public parameters for setting timeouts will come in a later PR.

note:

this PR also renames LocatorOperation to Progress for clarity

what changed

  • renamed locatorOperation.ts to progress.ts, LocatorOperation to Progress, & runLocatorOperation() to runWithProgress() because it will be used by callers beyond locators.
  • passed the same optional progress through frame traversal, element lookup, & browser helper readiness. callers without progress keep their existing behavior for now.
  • made readiness waits, retries, & browser commands use the remaining time. readiness can exceed the old fixed limits when the caller allows it. passing in 0 disables the deadline.
  • preserved timeout errors through retries, fallback setup, & cleanup. closed connection or session errors stop resolution, while frame movement & temporary browser context loss can still recover before expiry.
  • released temporary browser references after timeout, including late results. callers waiting for shared helper setup have independent deadlines.

test plan

added tests in locatorResolution.test.ts using controlled time & browser responses to verify:

  • multiple frame hops share one budget, & polling stops when it expires.
  • readiness can finish beyond the old limits with a longer or disabled timeout.
  • short readiness attempts fit the remaining time & detect a frame's new session.
  • stalled commands time out; late responses cannot resume resolution & temporary references are released. listeners & timers are removed after expiry.
  • one caller timing out does not cancel shared helper setup for another caller.
  • errors or cleanup finishing after the deadline produce a timeout, even before the timeout timer fires. ordinary lookup failures retain their existing behavior before expiry.
  • closed connections & sessions surface their errors. recovery from a changed session or missing browser context works only while the call is still active.

Summary by cubic

Adds a shared deadline to locator resolution so iframe hops, readiness waits, and element lookup all share the caller's timeout instead of each restarting it. Also fixes locator failures when entering still-loading iframes and correctly resolves XPaths that end at an iframe element.

What changed

  • Renames LocatorOperation to Progress and runLocatorOperation() to runWithProgress(); callers beyond locators now use it.
  • Threads an optional Progress through frame traversal and element lookup; callers without one keep current behavior, and 0 disables the deadline.
  • Readiness waits and browser commands use remaining time; timeout errors survive retries, fallback setup, and cleanup.
  • Releases temporary browser references after expiry, including late results and earlier element matches replaced by a later lookup.
  • Closed connection or session errors stop resolution immediately; recovered contexts and frame restarts are honored only while the call is active.

Bug fixes

  • Waits for the locator world when a deep XPath crosses a loading iframe instead of failing on a missing execution context.
  • Keeps a trailing iframe[n] in the parent frame as the target element instead of descending into the child document.

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

Review in cubic

@changeset-bot

changeset-bot Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: d82c7f7

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.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

@seanmcguire12
seanmcguire12 changed the base branch from main to add-locator-operation-context September 23, 2026 20:06

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

All reported issues were addressed across 17 files

Architecture diagram
sequenceDiagram
    participant Client as Stagehand Client SDK
    participant Locator as Locator (user-facing)
    participant Progress as Progress (deadline context)
    participant DeepLoc as DeepLocatorDelegate
    participant FrameLoc as FrameLocator
    participant Frame as Frame
    participant SelResolver as FrameSelectorResolver
    participant ExecCtx as ExecutionContextRegistry
    participant CDPSession as CDPSession (browser)
    participant Page as Page (frame registry)

    Note over Client,Page: Locator Resolution with Shared Deadline

    Client->>Locator: locator(selector).resolveNode(progress)
    Note over Locator,Progress: NEW: Optional Progress context shared across all steps
    
    Locator->>Progress: throwIfStopped()
    alt No progress provided
        Locator->>CDPSession: Runtime.enable + DOM.enable (no deadline)
    else Progress provided
        Locator->>Progress: run("enabling runtime", ...)
        Progress-->>Locator: step completed within budget
        Locator->>Progress: run("enabling DOM", ...)
        Progress-->>Locator: step completed within budget
    end

    Locator->>SelResolver: resolveAtIndex(query, index, progress)
    SelResolver->>Progress: throwIfStopped()
    SelResolver->>ExecCtx: waitForLocatorWorld(session, frameId, 1000, progress)
    
    Note over ExecCtx,CDPSession: Readiness wait with shared deadline
    loop While locator world not ready
        ExecCtx->>ExecCtx: Check cached extension/fallback world
        alt World not cached
            ExecCtx->>CDPSession: Runtime.enable (bounded by progress)
            ExecCtx->>CDPSession: Runtime.evaluate (inspect helpers)
            Note over ExecCtx: Each probe bounded by remaining budget
            alt Timeout expired
                ExecCtx->>Progress: throwIfStopped() -> TimeoutError
            end
        end
    end

    SelResolver->>SelResolver: resolveElements(helper, value, limit, progress)
    loop For each match index
        SelResolver->>Progress: run("evaluating selector", ...)
        SelResolver->>CDPSession: Runtime.evaluate (resolve nth selector)
        CDPSession-->>SelResolver: objectId
        SelResolver->>CDPSession: DOM.requestNode (resolve nodeId)
        alt Multiple matches resolved
            SelResolver->>SelResolver: Release non-selected nodes via cleanup()
        end
    end

    Note over DeepLoc,FrameLoc: Deep XPath / Frame traversal with shared deadline
    
    Client->>DeepLoc: deepLocatorThroughIframes(page, root, xpath, progress)
    DeepLoc->>DeepLoc: planDeepXPathTarget(xpath)
    Note over DeepLoc: CHANGED: Trailing iframe in XPath stays in parent frame
    alt XPath contains iframe hops
        DeepLoc->>FrameLoc: frameLocator(iframeSelector).resolveFrame(progress)
        FrameLoc->>Progress: throwIfStopped()
        FrameLoc->>FrameLoc: Resolve parent frame (recursive with progress)
        FrameLoc->>Frame: locator(iframeSelector).resolveNode(progress)
        Frame->>CDPSession: DOM.describeNode (bounded by progress)
        FrameLoc->>Page: listDirectChildFrameIdsFromRegistry(progress)
        loop For each child frame
            FrameLoc->>CDPSession: DOM.getFrameOwner (bounded by progress)
            alt Match found
                FrameLoc->>ExecCtx: waitForLocatorWorld(childSession, childFrameId, 200, progress, retry=false)
                Note over FrameLoc,ExecCtx: Re-checks session ownership between attempts
                alt OOPIF ownership changed
                    FrameLoc->>ExecCtx: Retry with new session
                end
            end
        end
        alt Deadline expired
            FrameLoc->>Progress: TimeoutError
            Progress->>FrameLoc: cleanup() releases iframe objectId
        end
    end

    Note over Locator,Progress: Edge: late results / cleanup after timeout
    alt Timeout fires while command pending
        Progress->>Progress: Abort with TimeoutError
        CDPSession-->>SelResolver: Late result arrives
        SelResolver->>Progress: cleanup() releases late objectId
        Note over SelResolver: Resolution does not resume post-timeout
    end

    Locator-->>Client: Resolved node {objectId, nodeId} or TimeoutError
Loading

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

Re-trigger cubic

Comment thread packages/extension/understudy/frameLocator.ts
@seanmcguire12
seanmcguire12 force-pushed the share-operation-context-in-locator-helpers branch from 8366982 to 8a7192c Compare September 23, 2026 21:11
@seanmcguire12
seanmcguire12 added this pull request to stack #3025 September 23, 2026 21:12
@seanmcguire12
seanmcguire12 force-pushed the share-operation-context-in-locator-helpers branch from 8a7192c to d82c7f7 Compare September 25, 2026 22:06

This branch has not been deployed

No deployments
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