# why
this PR wires the `progress` tracker into `act()`, `extract()`,
`observe()`. before this PR, `act()`, `observe()`, & `extract()` had
their own timeout tracking mechanism, which only checked remaining time
between between major IO steps. a stalled snapshot, model request, or
browser action could keep the call waiting past its timeout. nested work
also did not share the caller's remaining time.
# what changed
- gave each call one shared deadline across snapshots, cache reads &
writes, model requests, & browser work. each step gets only the time
left.
- carried act's deadline through page settling, supplied actions, cached
replay, two-step actions, & self-healing. extract's optional screenshot
& second model request share its deadline too.
- made expiry reject with the caller's timeout error & prevent later
steps. late model or cache responses cannot start another action or
request. browser commands & model requests already sent may still
finish.
- cleaned up settling listeners, temporary element references, & pressed
inputs when interrupted. kept snapshot scope fallback within the same
budget.
- kept the existing defaults: omitted timeouts & `0` remain unlimited.
- removed the unused timeout guard
# test plan
- `service-progress.test.ts` checks expiry during snapshots, cache work,
& model calls for all three services. it verifies that nested work uses
the remaining budget, omitted/zero timeouts stay unlimited, & late
responses cannot resume work. act cases cover cached replay,
self-healing, two-step actions, interrupted fill/drag/key presses, &
settling cleanup. extract cases cover screenshots, image encoding, & its
second model request.
- `serviceTimeouts.test.ts` uses a local browser, a controlled iframe
response, & controlled model responses. it checks that readiness expiry
prevents inference, full-page fallback does not restart the timeout, &
late responses cannot cause clicks or a second extraction request. it
also verifies that act can use the full-page fallback while time remains
& that an expired supplied action stays stopped after its iframe loads.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Gives `act()`, `extract()`, and `observe()` a shared `progress` deadline
so timeouts apply across snapshots, cache reads/writes, model requests,
and browser actions. Before, each service checked remaining time only
between major steps, so stalled work could outlive its timeout and
nested work never shared the caller's budget.
- One deadline now covers all work in a call; nested steps inherit the
caller's remaining time.
- Expiry rejects with the caller's timeout error and blocks any later
steps, though already-sent browser commands and model requests may still
finish.
- Interruption cleans up settling listeners and temporary element
references, releases held keys and a pressed mouse button exactly once
(the mouse at its last dispatched position) when a drag expires mid-way,
and drops held cache keys.
- Omitted or `0` timeouts remain unlimited; the unused `timeoutGuard`
helper is removed.
- Added unit and integration coverage for expiry during each step,
budget inheritance, late responses, and cleanup.
<sup>Written for commit f9753f8ec1755a21febe921f9b3f304f8cc62b94.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/3109?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. -->