mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-07 10:17:43 +02:00
[eric] browser: fix the 2 warm-marriage replay bugs = (1) 'Send a message to X' opener no longer trips the replay send-boundary (it opens the composer, reversible), (2) marriage replays NAV+opener only and hands composer->send to the send-script (which polls the lazy overlay), instead of replaying a composer-textbox click that races the render; 4 tests
This commit is contained in:
@@ -1059,6 +1059,11 @@ async def run_browser_agent(
|
||||
logger.info(f"[browser-skills] skill on {host} not replayed: {why}; running the full agent so the send is confirmed")
|
||||
return None
|
||||
prefix = steps[:unsafe_i]
|
||||
# Marriage mode: the send-script owns the composer (it polls for the lazy overlay + fills + sends). Replaying a recorded composer-textbox click races that render and misses (v903/v906), so truncate the prefix to NAV + opener only and let the script take it from the navigated page. Keep >=1 step or there's nothing to replay.
|
||||
if os.environ.get("OSW_REPLAY_SENDTAIL", "0") == "1":
|
||||
p_nav_prefix = [s for s in prefix if not browser_skills.step_touches_composer(s)]
|
||||
if p_nav_prefix:
|
||||
prefix = p_nav_prefix
|
||||
logger.info(
|
||||
f"[browser-skills] PREFIX replay: {len(prefix)}/{len(steps)} steps on {host}, "
|
||||
f"live agent confirms the tail ({why})"
|
||||
|
||||
@@ -111,17 +111,24 @@ P_LIVE_IRREVERSIBLE_RE = re.compile(
|
||||
r"confirm|apply|accept|decline|delete|remove|unsend|withdraw|endorse)\b",
|
||||
re.I,
|
||||
)
|
||||
# Composer OPENERS phrased with a send-word: LinkedIn's profile button is literally
|
||||
# named "Send a message to <person>", which opens the compose box (reversible), not
|
||||
# a real Send. A true Send control is short and exact ("Send", "Send now"); these
|
||||
# describe opening a conversation, so they must NOT trip the irreversible boundary.
|
||||
P_SEND_OPENER_RE = re.compile(r"send (a |an |the )?(message|note|inmail|dm) to\b", re.I)
|
||||
|
||||
|
||||
def is_replay_boundary(step: dict) -> bool:
|
||||
"""The genuinely irreversible step where a learned skill's mechanical replay
|
||||
must STOP and hand to the live agent. Same as is_send_step EXCEPT a composer
|
||||
OPENER ('Message'/'DM' click) is reversible and NOT a boundary: the prefix can
|
||||
mechanically open the composer, and only the real Send (and composer typing)
|
||||
crosses to the live model. Uses the same opener-excluded wordlist the live
|
||||
send-guard already trusts, so a recorded Send still stops the prefix."""
|
||||
OPENER ('Message'/'DM' click, incl. 'Send a message to X') is reversible and NOT
|
||||
a boundary: the prefix can mechanically open the composer, and only the real Send
|
||||
(and composer typing) crosses to the live model."""
|
||||
action = step.get("action")
|
||||
if action == "click" and P_LIVE_IRREVERSIBLE_RE.search(str(step.get("name") or "")):
|
||||
name = str(step.get("name") or "")
|
||||
if action == "click" and P_SEND_OPENER_RE.search(name):
|
||||
return False # opener phrasing, not a real send
|
||||
if action == "click" and P_LIVE_IRREVERSIBLE_RE.search(name):
|
||||
return True
|
||||
if action == "type" and P_COMPOSE_SEL_RE.search(str(step.get("selector") or "")):
|
||||
return True
|
||||
|
||||
@@ -330,6 +330,23 @@ def replay_settle_target(step: dict) -> str | None:
|
||||
return name if 0 < len(name) <= 60 else None
|
||||
|
||||
|
||||
P_COMPOSER_STEP_RE = re.compile(r"write a message|compose|message body|comment|reply|tweet|post text|type here|editor", re.I)
|
||||
|
||||
|
||||
def step_touches_composer(step: dict) -> bool:
|
||||
"""True if this step interacts with the compose box itself (focusing/typing),
|
||||
as opposed to navigation or the opener click. The send-script owns the composer
|
||||
(it polls for the lazy overlay), so the marriage replays only the nav+opener and
|
||||
hands the composer->send tail to the script."""
|
||||
tool = step.get("tool", "")
|
||||
p = step.get("params", {}) or {}
|
||||
if tool == "BrowserType":
|
||||
return True
|
||||
if tool in ("BrowserClickByName", "BrowserClick"):
|
||||
return bool(P_COMPOSER_STEP_RE.search(str(p.get("name") or p.get("selector") or "")))
|
||||
return False
|
||||
|
||||
|
||||
def first_unsafe_step(steps: list[dict]) -> tuple[int, str]:
|
||||
"""Index of the first GENUINELY irreversible step (click Send/Submit/Pay, type
|
||||
into a composer), -1 if none. This is the prefix-replay/batch boundary, so a
|
||||
|
||||
@@ -717,3 +717,24 @@ def test_replay_settle_target_for_click_by_name():
|
||||
{"tool": "BrowserClickByName", "params": {"name": "x" * 80}}) is None
|
||||
assert sk.replay_settle_target(
|
||||
{"tool": "BrowserClickByName", "params": {"name": ""}}) is None
|
||||
|
||||
|
||||
def test_send_opener_is_not_a_replay_boundary():
|
||||
# The LinkedIn profile opener is literally "Send a message to Tyler Chen":
|
||||
# it OPENS the composer (reversible), so it must NOT trip the send boundary
|
||||
# (v902 bug: it did, killing the prefix replay).
|
||||
from backend.apps.agents.browser import browser_batch_replay as BR
|
||||
assert BR.is_replay_boundary({"action": "click", "name": "Send a message to Tyler Chen"}) is False
|
||||
assert BR.is_replay_boundary({"action": "click", "name": "Send a note to Ada"}) is False
|
||||
# a REAL send still stops the prefix
|
||||
assert BR.is_replay_boundary({"action": "click", "name": "Send"}) is True
|
||||
assert BR.is_replay_boundary({"action": "click", "name": "Send now"}) is True
|
||||
|
||||
|
||||
def test_step_touches_composer():
|
||||
from backend.apps.agents.browser import browser_skills as SK
|
||||
assert SK.step_touches_composer({"tool": "BrowserType", "params": {"text": "hi"}}) is True
|
||||
assert SK.step_touches_composer({"tool": "BrowserClickByName", "params": {"name": "Write a message…"}}) is True
|
||||
# nav + opener are NOT composer steps (they stay in the marriage prefix)
|
||||
assert SK.step_touches_composer({"tool": "BrowserNavigate", "params": {"url": "https://x"}}) is False
|
||||
assert SK.step_touches_composer({"tool": "BrowserClickByName", "params": {"name": "Send a message to Tyler Chen"}}) is False
|
||||
|
||||
Reference in New Issue
Block a user