fix(browser): recover from CDP DOM-node serialization crash in browser_console (#35385)

browser_console(expression="document.body") returned the cryptic CDP error
"Object reference chain is too long" instead of a usable result.

With returnByValue=true, Chrome deep-serializes the eval result; for a live
DOM Node/NodeList/Window that serialization overruns CDP's recursion guard
and fails the whole call with a protocol-level error (not a JS exception),
which _browser_eval surfaced raw.

- browser_supervisor.evaluate_runtime: on that specific error, retry once
  with returnByValue=false so Chrome returns the node's description string —
  the same graceful path already used for document.querySelector() results.
- browser_tool._browser_eval (CLI subprocess fallback): the subprocess can't
  retry, so convert the reference-chain error into actionable guidance
  (extract a primitive / use JSON.stringify) instead of leaking it raw.

No expression rewriting — normal evals (1+41 -> 42) are untouched.
This commit is contained in:
Teknium
2026-05-30 07:31:25 -07:00
committed by GitHub
parent 42bbd221e8
commit 92ad7cc62c
3 changed files with 156 additions and 8 deletions
+26 -8
View File
@@ -496,12 +496,12 @@ class CDPSupervisor:
if not session_id:
return {"ok": False, "error": "supervisor has no attached page session"}
async def _do_eval() -> Dict[str, Any]:
async def _do_eval(by_value: bool) -> Dict[str, Any]:
return await self._cdp(
"Runtime.evaluate",
{
"expression": expression,
"returnByValue": return_by_value,
"returnByValue": by_value,
"awaitPromise": await_promise,
# userGesture matters for things like clipboard / fullscreen
# APIs that require a user-activation context.
@@ -511,14 +511,32 @@ class CDPSupervisor:
timeout=timeout,
)
try:
from agent.async_utils import safe_schedule_threadsafe
fut = safe_schedule_threadsafe(_do_eval(), loop)
from agent.async_utils import safe_schedule_threadsafe
def _run_eval(by_value: bool) -> Dict[str, Any]:
fut = safe_schedule_threadsafe(_do_eval(by_value), loop)
if fut is None:
return {"ok": False, "error": "Browser supervisor loop unavailable"}
response = fut.result(timeout=timeout + 1)
raise RuntimeError("Browser supervisor loop unavailable")
return fut.result(timeout=timeout + 1)
try:
response = _run_eval(return_by_value)
except Exception as exc:
return {"ok": False, "error": f"{type(exc).__name__}: {exc}"}
# ``returnByValue=True`` asks Chrome to deep-serialize the result.
# For live DOM nodes / NodeLists / Window that serialization can
# blow past CDP's recursion guard and fail the whole call with
# ``Object reference chain is too long`` (a protocol-level error,
# not a JS exception). Retry once with ``returnByValue=False`` so
# Chrome returns the object's description string instead — the same
# graceful degradation path used for ``document.querySelector(...)``
# results — rather than crashing the eval.
if return_by_value and "reference chain is too long" in str(exc).lower():
try:
response = _run_eval(False)
except Exception as exc2:
return {"ok": False, "error": f"{type(exc2).__name__}: {exc2}"}
else:
return {"ok": False, "error": f"{type(exc).__name__}: {exc}"}
# Runtime.evaluate response shape:
# {"id": N, "result": {"result": {"type": "...", "value": ..., ...},