[eric] browser: stop the caller starving the send-script, which silently killed every write

This commit is contained in:
ciregenz
2026-07-28 14:42:35 -07:00
parent 49e935e3bd
commit 95b0e00d51
3 changed files with 41 additions and 2 deletions
+4 -2
View File
@@ -1690,9 +1690,11 @@ async def run_browser_agent(
task, browser_id, tab_id, preloaded_perception,
execute_browser_tool, send_submit_index_in_state, payload_in_textbox,
payload_source=user_prompt, current_url=current_url,
), timeout=30.0)
), timeout=browser_send_script.WORST_CASE_BUDGET_S)
except Exception as p_se:
logger.info(f"[browser-sendscript] outer skip ({p_se})")
# Name the class: a bare TimeoutError stringifies to nothing, so this used to log
# "outer skip ()" and a starved send looked identical to a page we chose not to touch.
logger.info(f"[browser-sendscript] outer skip ({type(p_se).__name__}: {p_se})")
p_script = None
if isinstance(p_script, dict):
action_log.extend(p_script["log"])
@@ -27,6 +27,18 @@ logger = logging.getLogger(__name__)
ToolRunner = Callable[[str, dict, str, str], Awaitable[dict]]
# The worst case this routine can legitimately take, so the CALLER cannot starve it. Roughly: the
# composer poll (3 backoff waits plus three 6s interactive lists), one structural finder call (which
# carries its own 30s command timeout), then fill-and-commit and submit-and-receipt.
#
# This constant exists because the caller's timeout and this routine's real cost silently drifted
# apart: raising find_composer's command timeout from 15s to 30s made a single finder call able to
# eat the caller's entire 30s budget, so the whole script was killed mid-send and EVERY write fell
# back to the slow model loop. It failed invisibly, because asyncio.TimeoutError stringifies to
# nothing and the log read "outer skip ()". Measured live on LinkedIn: a 190.9s write that never
# posted. Import this instead of writing a number at the call site.
WORST_CASE_BUDGET_S = 75.0
def script_enabled() -> bool:
return os.environ.get("OSW_SEND_SCRIPT", "0") != "0"
@@ -53,6 +53,31 @@ def test_the_two_facts_are_not_the_same_variable():
"the run's success must be a function of evidence, not of the model's Done argument"
def test_the_caller_cannot_starve_the_send_script():
"""INVARIANT, and the third time this exact trap has bitten (find_composer, import_session, now
this). The send-script's cost and its caller's timeout drifted apart when find_composer went
15s -> 30s: one finder call could eat the caller's whole 30s budget, so the script was killed
mid-send and EVERY write silently fell back to the slow model loop. It was invisible because
asyncio.TimeoutError stringifies to nothing, so the log read "outer skip ()".
Measured live on LinkedIn: a 190.9s write that never posted."""
from backend.apps.agents.browser import browser_send_script as ss
from backend.apps.agents.core.ws_manager import BROWSER_CMD_TIMEOUTS
assert "timeout=browser_send_script.WORST_CASE_BUDGET_S" in P_SRC, (
"the caller hardcodes its own timeout again; it must import the script's stated worst case "
"so the two cannot drift")
# A single finder call must not be able to consume the whole budget.
assert ss.WORST_CASE_BUDGET_S > BROWSER_CMD_TIMEOUTS.get("find_composer", 15.0) * 2, \
"the budget must leave room for fill and submit after the finder, not just the finder"
def test_a_starved_send_is_logged_by_exception_class():
"""A bare TimeoutError has an empty message, so a starved send read exactly like a page we
deliberately declined to touch. The class name is what makes those two distinguishable."""
assert "type(p_se).__name__" in P_SRC
def test_an_unverified_send_gets_an_honest_line_that_does_not_claim_delivery():
note = dc.unverified_send_note("https://x.com/home", "hello from my automation")
low = note.lower()