From b9cf6b9e3f8837b4fc1a0fa8c0bfa11c393d22d3 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Tue, 7 Jul 2026 02:18:13 -0700 Subject: [PATCH] [eric] browser: send-script decline logging + prestage settle detects overlay opens via interactives diff --- backend/apps/agents/browser/browser_prestage.py | 11 ++++++----- backend/apps/agents/browser/browser_send_script.py | 4 ++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/backend/apps/agents/browser/browser_prestage.py b/backend/apps/agents/browser/browser_prestage.py index 89ac7487..b1281eeb 100644 --- a/backend/apps/agents/browser/browser_prestage.py +++ b/backend/apps/agents/browser/browser_prestage.py @@ -149,13 +149,14 @@ async def run_prestage( seen_steps: set[tuple[str, str]] = set() staged_complete = False - async def settle(pre_url: str, pre_text: str) -> bool: - # A click returns before the page swaps; perceiving too early reads the OLD page and the aux re-issues the same click (observed 4x loop). Wait for the page to actually change, capped. False = the action verifiably did NOT take. + async def settle(pre_url: str, pre_text: str, pre_li: str) -> bool: + # A click returns before the page swaps; perceiving too early reads the OLD page and the aux re-issues the same click (observed 4x loop). Wait for the page to actually change, capped. False = the action verifiably did NOT take. An overlay (message composer) changes the INTERACTIVES but not the URL and often not the first 400 chars of text, so the element list counts as change too. t_s = time.monotonic() while time.monotonic() - t_s < 3.0: await asyncio.sleep(0.35) li2, gt2, u2 = await perceive() - if (u2 and u2 != pre_url) or (gt2 and gt2[:400] != pre_text[:400]): + if ((u2 and u2 != pre_url) or (gt2 and gt2[:400] != pre_text[:400]) + or (li2 and pre_li and li2 != pre_li)): return True return False while steps < MAX_STEPS and (time.monotonic() - t0) < TOTAL_TIMEOUT_S: @@ -193,7 +194,7 @@ async def run_prestage( logger.info(f"[browser-prestage] step {steps + 1}: nav {arg} ok={ok}") if not ok: break - if not await settle(current_url, gt_text): + if not await settle(current_url, gt_text, li_text): logger.info(f"[browser-prestage] nav {arg} did not settle; stopping unstaged") break done_desc.append(f"navigated to {arg}") @@ -213,7 +214,7 @@ async def run_prestage( logger.info(f"[browser-prestage] step {steps + 1}: click [{idx}] {entry[:60]!r} ok={ok}") if not ok: break - if not await settle(current_url, gt_text): + if not await settle(current_url, gt_text, li_text): # The click ran but the page never changed (occluded element, overlay, stale index). Recording it would make the handoff note LIE ("navigation done") and send the main loop on a walkabout; observed live as 27-turn/112s regressions. logger.info(f"[browser-prestage] click [{idx}] did not settle; stopping unstaged") break diff --git a/backend/apps/agents/browser/browser_send_script.py b/backend/apps/agents/browser/browser_send_script.py index c54b48ad..a19f70a4 100644 --- a/backend/apps/agents/browser/browser_send_script.py +++ b/backend/apps/agents/browser/browser_send_script.py @@ -80,6 +80,7 @@ async def run_send_script( t0 = time.monotonic() payload = quoted_payload(task) if not payload: + logger.info("[browser-sendscript] decline: no unambiguous quoted payload") return None log: list[dict] = [] @@ -96,7 +97,9 @@ async def run_send_script( # Reversible-opener hop: prestage often stops on the profile with the "Message" opener visible (its settle raced the overlay). Opening a composer is the allowed opener class; the irreversible bar is unchanged. opener = opener_index_in_state(state_text) if not opener: + logger.info("[browser-sendscript] decline: no composer and no single exact-named opener in staged state") return None + logger.info(f"[browser-sendscript] firing via opener {opener[1]!r} [{opener[0]}]") r_open = await execute_tool("BrowserClickIndex", {"index": opener[0]}, browser_id, tab_id) if not (isinstance(r_open, dict) and "error" not in r_open): return None @@ -112,6 +115,7 @@ async def run_send_script( logger.info("[browser-sendscript] opener clicked but no composer appeared; handing to model") return None # No Send-button precondition: composer sites (LinkedIn) lazy-render Send only AFTER text commits, so it's resolved post-fill; never appearing = clean pre-click abort. + logger.info(f"[browser-sendscript] fill target {composer[1]!r} [{composer[0]}]") # 1. fill (focused by node, the composer overlay path coordinate clicks miss) r_fill = await execute_tool("BrowserClickIndex", {"index": composer[0], "text": payload}, browser_id, tab_id)