mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-28 04:24:51 +02:00
[eric] browser: a delete task can no longer report a send it never made
This commit is contained in:
@@ -2135,7 +2135,20 @@ async def run_browser_agent(
|
||||
done_success = delivery_verified
|
||||
p_aux_c, p_aux_m = await p_get_aux_client()
|
||||
p_pay = composer_committed_payload or ""
|
||||
if delivery_verified:
|
||||
if p_task_is_removal:
|
||||
# A DELETE task must never exit through the send confirmation. It
|
||||
# did: asked to delete three reddit posts, the run replied "Done,
|
||||
# that's sent." and removed nothing, and an independent re-read
|
||||
# found all three still there. The reply was shaped by the branch it
|
||||
# fell through, not by anything that happened on the page, which is
|
||||
# the exact false-success shape the receipt work exists to kill.
|
||||
done_success = False
|
||||
done_message = (
|
||||
"I could not confirm anything was deleted. Nothing here proves "
|
||||
"the item was removed, so please check the page yourself.")
|
||||
logger.info(f"[browser-agent {session_id}] removal task hit the "
|
||||
f"post-send backstop; refusing to claim a send")
|
||||
elif delivery_verified:
|
||||
p_nice = await compose_send_confirmation(p_aux_c, p_aux_m, task, p_pay)
|
||||
done_message = p_nice or (
|
||||
f'Done, I sent "{p_pay}" for you.' if p_pay else "Done, that's sent.")
|
||||
@@ -2609,7 +2622,15 @@ async def run_browser_agent(
|
||||
)
|
||||
|
||||
# Send completion: a SUCCESSFUL click on a send-class control (Send/ Submit/Post, opener-excluded so 'Message' never trips it) means the message went out, the composer clears instantly. We do NOT depend on the thread-text confirm here: the sent text often renders late, split across nodes, or scrolled off, so the text-probe is unreliable, which is exactly what left the model stalling to "double-check". A clean send click is proof enough; drive to the OUTCOME.
|
||||
if task_is_send and not send_confirmed and "error" not in result and tu.name in P_CONFIRM_TOOLS:
|
||||
# NOT on a removal task. `task_is_send` is true for one too (the classifier keys on
|
||||
# the verb), and the receipt below cannot tell composing from SEARCHING: asked to
|
||||
# delete a post, the model types its title into the site's search box, which sets
|
||||
# composer_committed_payload, and the next navigation leaves that box empty. Fill
|
||||
# committed + box now empty is exactly the two-sided receipt, so a delete that
|
||||
# removed nothing reported "Done, that's sent." Measured live on reddit: three
|
||||
# posts still there afterwards, all three claimed gone.
|
||||
if (task_is_send and not p_task_is_removal and not send_confirmed
|
||||
and "error" not in result and tu.name in P_CONFIRM_TOOLS):
|
||||
p_cn = result.get("clickedName") or ""
|
||||
p_cr = result.get("clickedRole") or ""
|
||||
p_send_click = browser_batch_replay.is_send_completed(
|
||||
@@ -2723,7 +2744,12 @@ async def run_browser_agent(
|
||||
if p_fill_text and payload_in_textbox(p_auto_state or "", p_fill_text):
|
||||
composer_committed_payload = p_fill_text
|
||||
# B: the model just TYPED the message into a composer on a send task, so finish the send in CODE (find Send, click, verify the composer cleared) instead of it burning ~3-4 turns on a Send button whose index goes stale after the fill. Uses what the MODEL typed, so un-quoted phrasings ("say hi" -> "hi") work; fails safe, an unverified click never claims delivery and send_confirmed blocks a resend.
|
||||
if (task_is_send and not send_confirmed and tu.name in P_CONFIRM_TOOLS
|
||||
# Same removal exclusion as the receipt above, and here it is the sharper edge:
|
||||
# this path CLICKS Send. On a delete task the text the model just typed is a
|
||||
# search query, so without the guard we would search for a post and then post
|
||||
# the search.
|
||||
if (task_is_send and not p_task_is_removal and not send_confirmed
|
||||
and tu.name in P_CONFIRM_TOOLS
|
||||
and browser_send_script.autosend_enabled()):
|
||||
p_cs = await browser_send_script.complete_send(
|
||||
composer_committed_payload, p_auto_state or "", browser_id, tab_id,
|
||||
|
||||
@@ -38,7 +38,10 @@ def test_the_stall_backstop_branches_on_evidence():
|
||||
final sentence without the model ever getting to speak; it must therefore read the evidence
|
||||
flag, not the resend guard."""
|
||||
idx = P_SRC.index("done_success = delivery_verified")
|
||||
window = P_SRC[idx:idx + 900]
|
||||
# Bounded by the next sibling branch, not by a character count. The count was 900, and adding an
|
||||
# `if p_task_is_removal:` branch (a delete must not borrow the send wording) pushed the honest
|
||||
# line past it, failing a test whose subject had not changed.
|
||||
window = P_SRC[idx:P_SRC.index("if not wrapup_nudged", idx)]
|
||||
assert "compose_unverified_send" in window, \
|
||||
"the unverified branch must compose an honest line, not fall straight to a template"
|
||||
assert "unverified_send_note" in window, \
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""A delete task must never exit through the send machinery.
|
||||
|
||||
Measured live 2026-08-02. Asked to delete three reddit posts, the run replied "Done, that's sent."
|
||||
and removed nothing: an independent re-read found all three still on the profile. Three separate
|
||||
paths let it happen, and all three share one root cause.
|
||||
|
||||
`task_is_send` is TRUE for a removal task. That is deliberate and documented (`is_removal_task`
|
||||
exists precisely because the send classifier keys on the verb), but every consumer of `task_is_send`
|
||||
then has to remember to exclude removals, and three of them did not:
|
||||
|
||||
1. the two-sided receipt, which cannot tell composing from SEARCHING. To find a post you want to
|
||||
delete, the model types its title into the site's search box. That sets
|
||||
composer_committed_payload. Navigating to the result leaves the box empty. "Fill committed AND
|
||||
box now empty" is the receipt, so a delete that deleted nothing scored a verified send.
|
||||
2. the autosend path, the sharper edge: it CLICKS Send on that same typed text, so a delete task
|
||||
could search for a post and then post the search.
|
||||
3. the post-send stall backstop, which phrased the ending as a send confirmation.
|
||||
|
||||
These tests are about the WIRING, not the regexes: they assert that a removal task is excluded from
|
||||
each of the three, so a future consumer of `task_is_send` that forgets the exclusion fails here.
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import re
|
||||
|
||||
from backend.apps.agents.browser import browser_agent as BA
|
||||
from backend.apps.agents.browser.browser_loop import is_removal_task
|
||||
|
||||
|
||||
P_SRC = inspect.getsource(BA)
|
||||
|
||||
|
||||
def p_guard_near(needle: str, window: int = 6) -> str:
|
||||
"""The few lines above a landmark, where its guard lives."""
|
||||
i = P_SRC.find(needle)
|
||||
assert i != -1, f"landmark moved: {needle!r}"
|
||||
return "\n".join(P_SRC[:i].splitlines()[-window:]) + "\n" + needle
|
||||
|
||||
|
||||
def test_a_delete_task_is_still_classified_as_a_send():
|
||||
"""The premise the other tests rest on. If this ever flips, the exclusions below become dead
|
||||
code and their protection quietly disappears, so it is asserted rather than assumed."""
|
||||
task = 'Go to reddit.com and delete my post titled "canary9428eec8"'
|
||||
assert is_removal_task(task), "a delete task must read as a removal"
|
||||
assert re.search(r"\b(post|submit|publish|send)\b", task, re.I), (
|
||||
"and it also carries a publish verb, which is why task_is_send is true for it")
|
||||
|
||||
|
||||
def test_the_two_sided_receipt_excludes_removals():
|
||||
"""The path that scored the false success: typing a title into search, then navigating away."""
|
||||
guard = p_guard_near('if (task_is_send and not p_task_is_removal and not send_confirmed\n'
|
||||
' and "error" not in result')
|
||||
assert "not p_task_is_removal" in guard
|
||||
|
||||
|
||||
def test_the_autosend_click_path_excludes_removals():
|
||||
"""The dangerous one: this path clicks Send on whatever the model just typed."""
|
||||
guard = p_guard_near("and browser_send_script.autosend_enabled()):")
|
||||
assert "not p_task_is_removal" in guard
|
||||
|
||||
|
||||
def test_the_post_send_backstop_refuses_to_claim_a_send_on_a_removal():
|
||||
"""Defence in depth. Even if a removal reaches the post-send ending, it must not borrow the
|
||||
send wording, and it must not report success."""
|
||||
i = P_SRC.find("if p_task_is_removal:")
|
||||
assert i != -1, "the removal branch in the post-send backstop is gone"
|
||||
branch = P_SRC[i:i + 900]
|
||||
assert "done_success = False" in branch, "a removal that cannot be confirmed is not a success"
|
||||
assert "could not confirm anything was deleted" in branch
|
||||
# The send wording must be ASSIGNED only under the non-removal branch. Checked on the assignment
|
||||
# rather than the raw text, because the comment above it quotes the bad reply on purpose.
|
||||
removal_code = [ln for ln in branch.split("elif")[0].splitlines()
|
||||
if "done_message" in ln and not ln.strip().startswith("#")]
|
||||
assert removal_code and all("sent" not in ln for ln in removal_code), removal_code
|
||||
Reference in New Issue
Block a user