<!-- This is an auto-generated description by cubic. -->
## Summary by cubic
Removed in-repo telemetry and delegated CLI event reporting to
`browser_harness` for unified, simpler telemetry. No user-facing
behavior changes; events report via Harness when available.
- **Refactors**
- Removed PostHog sender and `CLITelemetryEvent`; cleaned up exports and
dead code in `telemetry/*`.
- Added `_capture_via_harness` that calls
`browser_harness.telemetry.capture_cli_event` if present, and
`_set_harness_client_env` to set `BH_CLIENT`/`BH_CLIENT_VERSION`.
- Added `_delegated_to_harness` to prevent double-reporting; only
capture when not delegated.
- Simplified CLI telemetry context by dropping agent/model detection,
task parsing, and mode tracking; kept only command name.
- **Migration**
- If you imported `browser_use.telemetry.CLITelemetryEvent` or
`capture_detached`, switch to
`browser_harness.telemetry.capture_cli_event`.
<sup>Written for commit d1af39626e.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5165?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. -->
<!-- This is an auto-generated description by cubic. -->
## Summary by cubic
Renamed all non-URL mentions of "browser-harness" to "browser-use" in
generated skill docs to prevent broken links and simplify the logic.
- **Refactors**
- Replaced multiple targeted string replacements with a single regex
that skips URL path segments (uses `re.sub(r'(?<!/)browser-harness',
'browser-use', body)`).
<sup>Written for commit 53b1316076.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5161?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. -->
<!-- This is an auto-generated description by cubic. -->
## Summary by cubic
Fixes CLI slowdowns by sending telemetry in a detached helper with a 5s
network timeout, and adds better error context via the stderr tail. Also
improves agent detection and sends a truncated task for easier
debugging.
- **Bug Fixes**
- Telemetry is sent via a detached helper to avoid blocking the CLI;
respects `ANONYMIZED_TELEMETRY` and uses a 5s send timeout.
- Error events include the last 500 chars of stderr as context when
available.
- **New Features**
- Broader agent client detection via env markers (e.g., Cursor, Copilot,
Claude Code, Gemini CLI, OpenClaw, Hermes, CodeX, Amp).
- Sends a truncated task (up to 20k chars) instead of fully redacting;
device ID is unified, persisted, and falls back to a machine
fingerprint.
<sup>Written for commit 2d01ea316f.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5150?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. -->
## Problem
`is_element_visible_according_to_all_parents` mutated
`node.snapshot_node.bounds` **in place** while walking the frame chain
(`dom/service.py:295-296, 333-334`):
1. Every checked node's bounds were permanently shifted by frame offsets
and scroll, corrupting coordinates shared with `absolute_position` math,
paint-order filtering, and iframe hidden-element counting. The check
wasn't even idempotent — calling it twice on the same node could return
different results.
2. A frame node appears in **its own** frame chain
(`_construct_enhanced_node` appends it before computing its visibility),
so an iframe's bounds got offset by themselves — coordinates doubled. An
iframe at y=1200 on a 900px viewport (threshold 1000) became y=2400 →
classified invisible → `should_process_iframe=False` → its entire
content subtree silently dropped from extraction. A likely contributor
to "elements missing from the DOM" reports and one of the reasons
cross-origin iframe support has been flaky.
## Fix
Work on a copy of the bounds; skip `frame is node` in the chain walk. No
behavioral change for correctly-classified elements.
## Verification
New unit tests (no browser needed) that fail on the previous code:
- bounds object unchanged after a visibility check
- the check is idempotent
- an iframe checked against a frame chain containing itself is not
double-transformed (y=1200 stays visible)
Plus existing DOM serializer browser tests passing; pyright and ruff
clean.
Part of a series of small fixes from an internal review (#5133, #5146,
#5147, #5148).
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes a visibility bug that mutated shared bounds and double-offset
iframes. We now copy bounds and skip self in the frame chain, restoring
correct visibility and preventing dropped iframe content.
- **Bug Fixes**
- Copy `snapshot_node.bounds` before transforms to avoid in-place
mutations.
- Skip `frame is node` when walking the frame chain to prevent double
transforms.
- Add tests for non-mutation and iframe visibility; strengthen the
idempotency test to catch drift at threshold.
<sup>Written for commit 94ea8f60c8.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5151?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. -->
is_element_visible_according_to_all_parents mutated snapshot_node.bounds in
place while walking the frame chain, permanently shifting every checked
node's coordinates by frame offsets and scroll — corrupting values shared
with absolute_position math, paint-order filtering, and any later visibility
check (the function was not even idempotent).
Worse, a frame node appears in its own frame chain (_construct_enhanced_node
appends it before computing visibility), so an iframe's bounds were offset
by themselves — coordinates doubled — wrongly classifying iframes past
half the viewport threshold as invisible and silently dropping their entire
content subtree from extraction.
Work on a copy of the bounds and skip self in the frame chain.
## Problem
When structured output hits the completion-token cap, the JSON arrives
cut mid-string. Nothing checked `finish_reason`/`stop_reason` anywhere
(`grep stop_reason browser_use/agent/service.py` → no hits), so users
saw either:
- an opaque `Unterminated string starting at...` / pydantic validation
error, or
- **worse**: a valid-but-chopped answer, if truncation landed between
JSON fields — silent partial answers.
With `ChatOpenAI`'s default `max_completion_tokens=4096`, this regularly
hits long `done(text=...)` and `extract` outputs. The real cause was
never surfaced.
## Fix
Each provider checks its truncation signal before parsing structured
output and raises a clear error naming the cause and the knob:
- **OpenAI** (`finish_reason == 'length'`): raise
`ModelProviderError('Model output was truncated at
max_completion_tokens=N; ... increase max_completion_tokens or request
shorter output')`
- **Anthropic** (`stop_reason == 'max_tokens'`): same, for the tool_use
input path; plus an `except ModelProviderError: raise` guard so the new
error isn't re-wrapped by the generic catch-all into a status-502 blob
- **Google** (`MAX_TOKENS` finish reason): shared
`_raise_if_output_truncated()` helper applied to both the structured
path and the fallback text path
Status code **400** is deliberate: it's not in any provider's retryable
set (Google retries 429/5xx), and retrying an identical request would
truncate identically.
## Verification
- Regression test drives the **real OpenAI SDK** against a local
httpserver returning `finish_reason='length'` with JSON cut mid-string —
asserts the error names the truncation and the parameter, not a parse
failure (fails on previous code with the misleading validation error). A
companion test confirms normal `finish_reason='stop'` responses still
parse.
- 9 LLM tests passing; pyright and ruff clean.
Anthropic/Google paths are verified by type-checker and code-trace;
their SDK transports don't point at a local httpserver as easily —
follow-up welcome if there's an established fake for them.
Part of a series of small fixes from an internal review (#5133, #5146,
#5147).
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Detect and surface LLM output truncation instead of misleading JSON
parse errors, and allow switching to a fallback LLM when truncation
occurs. Also handles reasoning models that return `content=null` with a
length-based finish reason.
- **Bug Fixes**
- OpenAI: If `finish_reason == 'length'`, raise
`ModelOutputTruncatedError` (400) before the missing-content guard;
reference `max_completion_tokens` or say "the model's output token
limit" when unset.
- Anthropic: If `stop_reason == 'max_tokens'`, raise
`ModelOutputTruncatedError`; add `except ModelProviderError: raise` so
the clear error isn’t re-wrapped.
- Google: Detect `MAX_TOKENS` in both structured and fallback paths via
`_raise_if_output_truncated`; raise (400) referencing
`max_output_tokens` or the generic phrase when unset.
- Agent: Introduced `ModelOutputTruncatedError` (400). Provider retry
loops skip it; the agent treats it as fallback-switchable so a
higher-cap model can succeed.
- Tests: Cover truncation vs normal parse, unset-cap message,
`content=None` case, and that truncation triggers a fallback LLM switch.
- **Refactors**
- Trimmed comments to essentials.
<sup>Written for commit 248f98f208.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5148?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. -->
<!-- This is an auto-generated description by cubic. -->
## Summary by cubic
Adds a legacy command shim to the `browser-use` CLI that detects pre-3.0
subcommands/flags and prints a short migration guide with examples. It
exits with code 2 and tags the run as `legacy:<cmd>` instead of running
the harness.
- **New Features**
- Detects common pre-3.0 commands and flags and maps them to clear
hints.
- Prints a structured migration message to stderr with stdin piping
examples, core helpers, and doc links.
- Exits with status 2 to signal misuse; avoids starting the browser
harness.
- Updates command context/dispatch to label events as `legacy:<cmd>` for
easier tracking.
<sup>Written for commit b65642f810.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5149?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. -->
OpenAI reasoning models can spend the entire max_completion_tokens budget on
hidden reasoning, returning finish_reason='length' with content=null. The
truncation check ran after the missing-content guard, so that case raised
the generic 'Failed to parse structured output' (500) instead of
ModelOutputTruncatedError — no truncation signal, no fallback switch. Check
finish_reason first; it does not depend on content.
The 400 status chosen for truncation errors (deliberately outside provider
retry lists — an identical retry truncates identically) also fell outside
Agent._try_switch_to_fallback_llm's allowlist, so a configured fallback_llm
could no longer rescue a truncated run. Before this PR the downstream parse
failure was wrapped as a 502 ModelProviderError, which did allow the switch.
Introduce ModelOutputTruncatedError(ModelProviderError, status 400): the
three providers raise it, provider retry loops still skip it, and the
agent's fallback check treats it as switchable explicitly — a fallback with
a different output cap can succeed where the primary truncated.
max_completion_tokens (OpenAI) and max_output_tokens (Google) are optional;
when set to None the truncation message printed 'truncated at
max_output_tokens=None'. Fall back to "the model's output token limit" —
a MAX_TOKENS/length finish reason means some server-side cap fired even
with no client-side cap configured. Anthropic's max_tokens is non-optional
and unaffected.
Structured output cut off at the completion-token cap was never detected:
OpenAI's finish_reason='length', Anthropic's stop_reason='max_tokens', and
Gemini's MAX_TOKENS finish reason all produced JSON cut mid-string, which
surfaced as an opaque parse error ('Unterminated string starting at...') —
or worse, a valid-but-chopped prefix. The actual cause (output token cap)
was never mentioned, and with defaults like max_completion_tokens=4096 this
regularly hits long done()/extract outputs.
Each provider now checks the finish/stop reason before parsing structured
output and raises a clear ModelProviderError ('Model output was truncated
at max_*_tokens=N; increase it or request shorter output'). Status code 400
is used deliberately: it is not in any retry list, and retrying the same
request would truncate identically.
Also adds an 'except ModelProviderError: raise' guard in the Anthropic
handler so the new error is not re-wrapped by the generic catch-all.
## Problem
`Registry.execute_action`'s catch-all `except Exception` also caught
`BrowserError`, flattening it into a generic `RuntimeError('Error
executing action ...')`. That destroyed the structured
`short_term_memory` / `long_term_memory` the error carries specifically
to steer the LLM's next action — e.g. the list of available dropdown
options when the agent clicks a `<select>`.
The `except BrowserError` branch in `Tools.act` (which formats those
memories into an `ActionResult` via `handle_browser_error`) was dead
code for any action that lets a `BrowserError` propagate through
`execute_action` — `upload_file`, `dropdown_options` via
`event_result(raise_if_any=True)`, extraction handler errors. The LLM
got an opaque error string instead of actionable context, burning extra
steps rediscovering what the error already knew.
## Fix
One `except BrowserError: raise` ahead of the generic handlers in
`execute_action`, so `handle_browser_error` in `Tools.act` is the single
formatting point again.
## Verification
- New regression test: a registered action raising
`BrowserError(short_term_memory=..., long_term_memory=...)` must arrive
in the `ActionResult` with `error` = long-term memory and
`extracted_content` = short-term memory. Fails on the previous code with
the flattened generic string.
- 17 tools/registry tests passing; pyright and ruff clean.
Part of a series of small fixes from an internal review (same batch as
#5133, #5146).
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Preserves structured `BrowserError` memory through
`Registry.execute_action` while keeping plain errors recoverable, so
agents get context without breaking callers. `Tools.act` stays the
single formatting point for structured errors.
- **Bug Fixes**
- Re-raise `BrowserError` only when `long_term_memory` is present;
otherwise, flatten to `RuntimeError` to preserve previous recoverable
behavior.
- Map `long_term_memory` -> `ActionResult.error` and `short_term_memory`
-> `ActionResult.extracted_content` for structured errors.
- Add tests for both structured and plain `BrowserError` paths to lock
in behavior.
<sup>Written for commit 782535c34f.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5147?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. -->
An unconditional re-raise sent BrowserErrors without long_term_memory (e.g.
upload_file's failure paths) into Tools.act's handle_browser_error, which
re-raises exactly those — escaping act() as an exception where callers
previously got a recoverable ActionResult(error=...).
Guard the bypass on long_term_memory being present (the exact condition
handle_browser_error formats without re-raising; short_term_memory alone
would still re-raise), and flatten plain BrowserErrors to RuntimeError as
before. Regression test covers the plain-BrowserError path through
tools.act.
Registry.execute_action's catch-all handler flattened BrowserError into a
generic 'Error executing action ...' RuntimeError, destroying the structured
short_term_memory/long_term_memory the error carries to steer the LLM's next
action (e.g. the list of available dropdown options when clicking a select).
The 'except BrowserError' branch in Tools.act that formats those memories
into an ActionResult was dead code for any action that let a BrowserError
propagate (upload_file, dropdown_options via event_result, extraction
handlers).
Re-raise BrowserError before the generic handlers so handle_browser_error
becomes the single formatting point again.
## Problem
Two content-destruction bugs in `extract_clean_markdown` caused
extraction results to silently lose content — a direct source of agents
reporting 'not found on page' for content that was there:
1. **All percent-encodings stripped**: `re.sub(r'%[0-9A-Fa-f]{2}', '',
content)` ran over the entire markdown output, corrupting every URL
containing `%20`/`%2F`/`%26`... — precisely when `extract_links=True`
was requested.
2. **Long link lines dropped**: the JSON-blob heuristic removed any line
over 100 chars starting with `{` **or `[`** — which matches long
markdown links `[text](very-long-url)`, clickable images
`[](href)`, and citation-style lines. Items simply vanished
from extracted lists.
## Fix
- Delete the `%XX` regex entirely (markdownify does not emit stray
percent-encodings; URLs must survive intact).
- Only drop long `{`/`[`-prefixed lines that actually parse as JSON
(`json.loads`), so SPA state blobs are still filtered while markdown
links survive. The three upstream regexes for common blob shapes are
unchanged.
- Extract the HTML→markdown conversion into a pure
`convert_html_to_markdown()` helper so this stage is unit-testable
without a browser.
Trade-off: JS object literals (unquoted keys) over 100 chars are now
kept since they aren't valid JSON — deliberately erring toward never
destroying real content.
## Verification
- New regression tests: long markdown link lines and clickable-image
lines survive; valid JSON array blobs are still dropped; percent-encoded
URLs survive the full HTML→markdown pipeline. The link-line test fails
on the previous code.
- 37 markdown extractor/chunking tests passing; pyright and ruff clean.
Part of a series of small fixes from an internal review (same batch as
#5133).
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes content loss in markdown extraction by preserving percent-encoded
URLs and keeping long markdown link/image lines. Prevents broken links
and “not found on page” misses during link extraction.
- **Bug Fixes**
- Removed `%XX` stripping from the output so `%20`, `%2F`, `%26`, etc.
in URLs are preserved.
- Only drop long `{`/`[` lines if they parse as JSON; long `[text](url)`
and `[](href)` lines now remain.
- **Refactors**
- Extracted HTML→markdown into `convert_html_to_markdown()` for easier
testing and reuse.
<sup>Written for commit ab08dea62c.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5146?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. -->
Two content-destruction bugs in extract_clean_markdown:
- A cleanup regex stripped every %XX sequence from the converted markdown,
corrupting all percent-encoded URLs (%20, %2F, ...) — precisely when
extract_links=True was requested.
- The JSON-blob line filter dropped any line over 100 chars starting with
'{' OR '[' — silently deleting long markdown links [text](long-url),
clickable images, and citation-style lines.
Delete the %XX regex, and only drop long lines that actually parse as JSON
(json.loads) so SPA state blobs are still filtered while markdown links
survive. Also extract the HTML->markdown conversion into a pure
convert_html_to_markdown() helper so this stage is unit-testable.
## Problem
Every navigation could burn its full readiness timeout (3s same-domain /
8s cross-domain) and then proceed on a page in an unknown load state —
DOM snapshots and screenshots taken on half-loaded pages, plus 3-8s of
dead time per navigation.
**Root cause:** `_navigate_and_wait` polls a lifecycle-event deque that
was fed by a per-session closure registered via
`cdp_client.register.Page.lifecycleEvent(...)`. cdp-use's event registry
is **single-slot per CDP method** (`self._handlers[method] = callback`),
so every new target attach **replaced** the previous tab's handler.
Existing tabs' deques froze with only pre-navigation events, which the
loaderId filter then (correctly) rejected — guaranteed timeout.
This is deterministic with 2+ tabs, and nondeterministic even single-tab
because each target gets attached twice (manual `attachToTarget` +
browser-level `setAutoAttach`), creating two session objects with two
competing registrations while `_get_session_for_target` picks one
arbitrarily.
## Fix
- Lifecycle events are now stored **per target_id in SessionManager**,
fed by **one global** `Page.lifecycleEvent` handler registered once in
`start_monitoring()` and routed by `session_id` — same pattern as the
existing Target attach/detach handlers. Buffers are freed on target
removal.
- `_navigate_and_wait` reads the per-target buffer and returns a timeout
status string instead of swallowing readiness timeouts;
`on_NavigateToUrlEvent` surfaces it via
`NavigationCompleteEvent.loading_status`, so downstream consumers can
distinguish 'loaded' from 'gave up waiting'.
- loaderId-less lifecycle events that predate the current navigation are
skipped (defense against stale entries).
- Dropped unused `CDPSession._lifecycle_lock`.
## Verification
Deterministic regression test
(`tests/ci/browser/test_navigation_readiness.py`): navigate tab A after
opening tab B — **before: exactly 3.01s (burned timeout), after:
<0.5s**. Also covers: fast-page navigation completes without burning the
timeout, readiness timeout is reported via the return status (page with
a stalled `<img>` holding back `load`), and successful navigation
returns no timeout status.
- 4 new tests + 20 existing navigation/session/cross-origin tests
passing
- pyright and ruff clean
## Notes
- The loaderId filter itself was verified correct against live Chrome
(new-navigation events match `Page.navigate`'s returned loaderId within
ms) and is kept as-is.
- Known remaining issue (separate PR): HAR recording registers
`Page.lifecycleEvent` too and will clobber this global handler when
`record_har_path` is set — same single-slot registry problem, needs a
proper fan-out multiplexer.
- The double-attach (two CDP sessions per pre-existing target) is also a
separate follow-up; it's harmless to this wait loop now.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes navigation readiness by storing lifecycle events per target and
routing them through one global `Page.lifecycleEvent` handler. Also
short-circuits same-document navigations so fragment/History API changes
don’t burn the timeout.
- **Bug Fixes**
- Store lifecycle events per `target_id` in `SessionManager`, fed by a
single global `Page.lifecycleEvent` from `cdp-use`; clear buffers on
target removal.
- `_navigate_and_wait` reads the per-target buffer and returns a timeout
status string; surfaced via `NavigationCompleteEvent.loading_status`.
- Ignore stale signals: drop events with mismatched `loaderId`, and skip
loaderId-less events older than the navigation start.
- Treat same-document navigations (no `loaderId`) as complete
immediately.
- Regression tests: fast navigations finish without burning fallback
timeouts; navigating tab A remains fast after opening tab B; readiness
timeouts are reported; same-document navigation completes immediately.
<sup>Written for commit b4b6868232.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5133?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. -->
Page.navigate omits loaderId for same-document navigations (#fragment,
History API), and Chrome emits no new load/DOMContentLoaded lifecycle events
for them — the navigation is already committed when Page.navigate returns.
The stale-event timestamp guard would otherwise reject all buffered events
and burn the full readiness timeout.
Short-circuit when loaderId is absent, and simplify the stale-event guard
(the no-navigation-id case can no longer reach it). Regression test drains
the previous load's trailing networkIdle first so a stale event can't
accidentally satisfy the wait.
Navigation waits polled a per-session event deque whose feeding handler was
registered per-session on cdp-use's single-slot event registry. Any later
target attach replaced the handler, freezing existing tabs' deques with only
pre-navigation events, so every navigation on those tabs burned the full
readiness timeout (3s same-domain / 8s cross-domain) and then proceeded on a
page in unknown load state.
- Store lifecycle events per target_id in SessionManager, fed by ONE global
Page.lifecycleEvent handler registered in start_monitoring() and routed by
session_id; buffers are freed on target removal
- _navigate_and_wait reads the per-target buffer and now returns a timeout
status string instead of swallowing readiness timeouts;
on_NavigateToUrlEvent surfaces it via NavigationCompleteEvent.loading_status
- Skip loaderId-less lifecycle events that predate the current navigation
- Drop unused CDPSession._lifecycle_lock
Deterministic regression test: navigating tab A after opening tab B took
exactly the 3s fallback timeout before this fix, <0.5s after.