From bd24d0bf6c016c8fceb27896bb7a46db2d284123 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Tue, 4 Aug 2026 16:11:51 -0700 Subject: [PATCH] [eric] browser: a comment task must not click Create-new-post --- .../apps/agents/browser/browser_send_parse.py | 32 +++++++++++++++++-- .../agents/browser/browser_send_script.py | 2 +- backend/tests/test_browser_opener_match.py | 25 +++++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/backend/apps/agents/browser/browser_send_parse.py b/backend/apps/agents/browser/browser_send_parse.py index 6d4f9fd1..6c7db5ff 100644 --- a/backend/apps/agents/browser/browser_send_parse.py +++ b/backend/apps/agents/browser/browser_send_parse.py @@ -9,6 +9,7 @@ imports from here, never the reverse. """ import re +from typing import Optional # Double quotes are unambiguous. Single quotes only delimit when the opener is at a word boundary (start/space/colon), so an in-word apostrophe like "chen's" is never mistaken for a payload quote, that mispairing was silently corrupting the canonical "text him '...'" errand. P_QUOTED_DQ_RE = re.compile(r'"([^"]{4,300})"') @@ -249,21 +250,48 @@ def quoted_payload(task: str) -> str: return sq.pop() if len(sq) == 1 else "" -def opener_index_in_state(state_text: str): +def opener_index_in_state(state_text: str, task: Optional[str] = None): """(index, name) of the single composer OPENER, or None. An exact name, or a verb+noun compose phrase anywhere in a longer label. The second half is what reaches the openers whose label is a whole sentence, and it keeps the exactness the first half was buying: an upsell ('Send InMail') has the wrong noun and a count ('526 comments') has - no verb. Still a SINGLETON, so two candidates stay the model's problem, not a coin flip.""" + no verb. Still a SINGLETON, so two candidates stay the model's problem, not a coin flip. + + Pass `task` to drop openers that contradict it, which is what keeps a comment task off the + 'New post' upload button. Optional so existing read-only callers are unaffected.""" hits = [(int(m.group(1)), m.group(2)) for m in P_OPENER_ROW_RE.finditer(state_text or "")] if not hits: hits = [(int(m.group(1)), m.group(2)) for m in P_CONTROL_ROW_RE.finditer(state_text or "") if P_OPENER_PHRASE_RE.search(m.group(2) or "")] + if task is not None: + hits = [h for h in hits if not opener_contradicts_task(task, h[1])] return hits[0] if len(hits) == 1 else None +# "New post", "Create", "Compose" open a BLANK thing. Fine when the task is to write something new; +# wrong when the task is to respond to something that already exists. +P_CREATE_OPENER_RE = re.compile(r"\b(new|create|compose|start a)\b", re.I) + + +def opener_contradicts_task(task: str, opener_name: str) -> bool: + """True when this opener would take us somewhere the task did not ask to go. + + Measured on instagram 2026-08-04: a "write a comment on the first post" task matched the opener + `New post Create`, which is the UPLOAD flow, so the run left the feed for a file picker and + never saw a post. Nothing downstream could catch it, because by then the only evidence left was + a page with no composer on it. + + Only the create-vs-respond direction, because that is the one with a wrong destination. A + respond-shaped opener on a create task is left alone: some sites really do route a new post + through a control labelled "Write". + """ + t, name = task or "", opener_name or "" + responding = bool(P_COMMENT_INTENT_RE.search(t)) + return responding and bool(P_CREATE_OPENER_RE.search(name)) + + def composer_index_in_state(state_text: str): """(index, name) of the single compose-shaped textbox, or None. Two candidates = ambiguous = model's problem.""" diff --git a/backend/apps/agents/browser/browser_send_script.py b/backend/apps/agents/browser/browser_send_script.py index e0921d53..33fd814e 100644 --- a/backend/apps/agents/browser/browser_send_script.py +++ b/backend/apps/agents/browser/browser_send_script.py @@ -293,7 +293,7 @@ async def run_send_script( p_struct_selector: str = "" if not composer: # 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 = browser_send_parse.opener_index_in_state(state_text) + opener = browser_send_parse.opener_index_in_state(state_text, task_sans_brief) if opener and browser_send_parse.surface_mismatch(task_sans_brief, opener[1]): # The same wrong-surface rule the composer already enforces, applied one step earlier. # Measured on linkedin.com with "start a post": the only opener listed was 'Comment', so diff --git a/backend/tests/test_browser_opener_match.py b/backend/tests/test_browser_opener_match.py index b9375aa7..c0250658 100644 --- a/backend/tests/test_browser_opener_match.py +++ b/backend/tests/test_browser_opener_match.py @@ -57,3 +57,28 @@ def test_the_exact_rule_is_preferred_over_the_phrase_rule(): hit = sp.opener_index_in_state( '[3]