Gemini 3+ models may throw errors if a default value is set for
parameters like temperature, top-p, etc.
The 3-series models use 1.0 as the default temperature, so this
should be a backwards-compatible change.
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.
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.
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.
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.
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.