fix(browser): derive batch navigation outcome from command rows

A batch whose open succeeded but whose later command failed was recorded
as a failed navigation, so a following observation was wrongly labelled
stale. Use the per-command rows; when the outcome cannot be determined,
treat the page as unknown instead of claiming either result.
This commit is contained in:
Alexandre Teixeira
2026-10-01 21:03:00 +01:00
parent 576abb012d
commit 255bff1f76
5 changed files with 102 additions and 6 deletions
+38 -3
View File
@@ -2757,6 +2757,33 @@ class PrivateBrowserTool:
header = "\n".join(part for part in (title, url) if part)
return {"ok": True, "url": url, "text": f"{header}\n\n{text}".strip()}
@staticmethod
def _batch_navigation_outcome(output: str, command_ok: bool) -> tuple[str, str]:
"""Outcome of a batch's last navigation: ``ok``, ``failed`` or ``unknown``.
A later command failing does not undo a navigation that succeeded,
so the per-command rows decide, not the batch exit status.
"""
try:
rows = json.loads(output)
except (ValueError, TypeError):
rows = None
if isinstance(rows, list):
for row in reversed(rows):
command = row.get("command") if isinstance(row, dict) else None
if not (
isinstance(command, list)
and command
and str(command[0]).lower() in {"open", "goto", "navigate"}
):
continue
if row.get("success") is True:
result = row.get("result") if isinstance(row.get("result"), dict) else {}
return "ok", str(result.get("url") or "")
return "failed", ""
return ("ok", "") if command_ok else ("unknown", "")
@staticmethod
def _navigated_url(output: str) -> str:
"""Final URL reported by ``open`` (after redirects), when present."""
@@ -3028,12 +3055,20 @@ class PrivateBrowserTool:
"untrusted_content": True,
}
if navigation_url:
if command_ok:
outcome, final_url = "ok" if command_ok else "failed", ""
if action == "batch":
outcome, final_url = self._batch_navigation_outcome(out, command_ok)
if outcome == "ok":
browser.navigated(
(read_page or {}).get("url") or self._navigated_url(out) or navigation_url
final_url
or (read_page or {}).get("url")
or self._navigated_url(out)
or navigation_url
)
else:
elif outcome == "failed":
browser.navigation_failed(navigation_url)
else:
browser.navigation_unknown(navigation_url)
if read_page is not None:
if not command_ok:
return {
+18
View File
@@ -300,6 +300,7 @@ class BrowserSession:
navigation_generation: int = 0
page_url: str = ""
failed_navigation_url: str = ""
navigation_outcome_unknown: bool = False
_lock: asyncio.Lock | None = field(default=None, repr=False)
_lock_loop: Any = field(default=None, repr=False)
@@ -321,22 +322,39 @@ class BrowserSession:
self.navigation_generation += 1
self.page_url = url
self.failed_navigation_url = ""
self.navigation_outcome_unknown = False
self.state = "ready"
def navigation_failed(self, url: str) -> None:
self.failed_navigation_url = url
self.navigation_outcome_unknown = False
self.state = "navigation_failed"
def navigation_unknown(self, url: str) -> None:
"""A navigation was attempted but whether it happened is unknown."""
self.page_url = ""
self.failed_navigation_url = url
self.navigation_outcome_unknown = True
self.state = "navigation_unknown"
def discarded(self, state: str) -> None:
"""The browser and its page are gone; nothing earlier is observable."""
self.page_url = ""
self.failed_navigation_url = ""
self.navigation_outcome_unknown = False
self.state = state
def stale_observation_note(self) -> str:
if not self.failed_navigation_url:
return ""
if self.navigation_outcome_unknown:
return (
f"Browser lifecycle: the outcome of the most recent navigation to "
f"{self.failed_navigation_url} is unknown. This observation may not "
f"show {self.failed_navigation_url}."
)
shown = self.page_url or "an earlier page"
return (
f"Browser lifecycle: the most recent navigation to {self.failed_navigation_url} "