[eric] skills: name the send-script's clicks so a completed write can be recorded at all

This commit is contained in:
ciregenz
2026-08-05 07:47:01 -07:00
parent 950755af07
commit 438a96eb42
2 changed files with 80 additions and 2 deletions
@@ -129,7 +129,8 @@ async def complete_send(
await execute_tool("BrowserClickIndex", {"index": p_idx, "text": payload},
browser_id, tab_id)
log.append({"tool": "BrowserClickIndex", "input": {"index": p_idx, "text": payload},
"ok": True, "result_summary": f"filled required field {p_name!r}"[:200],
"ok": True, "clicked_role": "textbox", "clicked_name": p_name,
"result_summary": f"filled required field {p_name!r}"[:200],
"elapsed_ms": 0})
r_ev2 = await execute_tool(
"BrowserEvaluate",
@@ -374,7 +375,12 @@ async def run_send_script(
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
# clicked_name/clicked_role are the fields the skill distiller reads. Putting the
# element's name only into result_summary prose is why that layer recorded nothing in 95
# consecutive gate passes: distill_steps hits a click it cannot name, truncates there,
# and the navigation-only remainder is then correctly refused by its own guard.
log.append({"tool": "BrowserClickIndex", "input": {"index": opener[0]}, "ok": True,
"clicked_role": "button", "clicked_name": opener[1],
"result_summary": f"script opened composer via {opener[1]!r}"[:200], "elapsed_ms": 0})
# Wait for the surface to STOP MOVING, not for a number of seconds. Fixed budgets kept
# being wrong in both directions: 1.8s missed gmail and linkedin entirely, 5.3s still
@@ -464,8 +470,13 @@ async def run_send_script(
# 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)
fill_ok = isinstance(r_fill, dict) and "error" not in r_fill
# Named for the distiller, same as the opener. The payload does NOT ride along: a
# BrowserClickIndex distills to a BrowserClickByName carrying role and name only, so a
# replay re-focuses this box and the send script fills it with the CURRENT text. Baking the
# payload in here is how a replay would re-post last week's message.
log.append({"tool": "BrowserClickIndex", "input": {"index": composer[0], "text": payload},
"ok": fill_ok, "result_summary": f"script fill into {composer[1]!r}"[:200], "elapsed_ms": 0})
"ok": fill_ok, "clicked_role": "textbox", "clicked_name": composer[1],
"result_summary": f"script fill into {composer[1]!r}"[:200], "elapsed_ms": 0})
if not fill_ok and browser_submit_click.is_stale_index_error(r_fill):
# The opener click opens a modal that keeps re-rendering after we listed it, so the
# composer node we resolved is already detached by the time the fill lands. Measured
@@ -499,6 +510,7 @@ async def run_send_script(
fill_ok = isinstance(r_fill, dict) and "error" not in r_fill
log.append({"tool": "BrowserClickIndex",
"input": {"index": composer[0], "text": payload}, "ok": fill_ok,
"clicked_role": "textbox", "clicked_name": composer[1],
"result_summary": f"script fill retry into {composer[1]!r}"[:200],
"elapsed_ms": 0})
if not fill_ok:
@@ -0,0 +1,66 @@
"""The send script's own action_log must be able to become a skill.
Measured across 2026-08 sweeps: 95 runs passed the skill record gate (`honest=True
informational=False removal=False unconfirmed_send=False`) and all 95 came back
`NOT recorded (host empty or no robust steps)`. Nothing was wrong with the gate. The send script
appended its opener and composer clicks with the element name in `result_summary` prose only, and
`distill_steps` reads `clicked_name`. An unnameable click truncates the distillation, the truncation
branch then drops the typing steps, and what is left is navigation-only, which `productive_count`
correctly refuses. Zero recordings, forever, from four missing dict keys.
The class of bug is "one side writes prose, the other side reads a field", so the test is written
against the SHAPE the send script appends rather than against any one call site.
"""
from backend.apps.agents.browser import browser_skills as bs
def p_sendscript_log() -> list:
"""The action_log a fast-path write leaves behind: navigate, open the composer, fill it.
Mirrors browser_send_script.py's own appends. `send click` is deliberately included and
deliberately not distillable: its tool name matches no branch, which is what keeps a replay from
ever re-firing a send.
"""
return [
{"tool": "BrowserNavigate", "input": {"url": "https://www.linkedin.com/feed/"}, "ok": True},
{"tool": "BrowserClickIndex", "input": {"index": 12}, "ok": True,
"clicked_role": "button", "clicked_name": "Start a post",
"result_summary": "script opened composer via 'Start a post'"},
{"tool": "BrowserClickIndex", "input": {"index": 31, "text": "hello there"}, "ok": True,
"clicked_role": "textbox", "clicked_name": "Text editor for creating content",
"result_summary": "script fill into 'Text editor for creating content'"},
{"tool": "send click", "input": {"via": "index"}, "ok": True,
"clicked_role": "button", "clicked_name": "Post"},
]
def test_a_fast_path_write_distills_into_a_replayable_prefix():
"""The regression: this returned [] on every single fast-path write."""
steps = bs.distill_steps(p_sendscript_log())
assert steps, "a completed fast-path write must be recordable"
assert [s["tool"] for s in steps] == [
"BrowserNavigate", "BrowserClickByName", "BrowserClickByName"]
def test_the_payload_never_rides_into_the_recorded_skill():
"""A skill that carries last week's text re-posts last week's text. The composer click is
recorded as role+name, and the send script fills it fresh on every replay."""
blob = repr(bs.distill_steps(p_sendscript_log()))
assert "hello there" not in blob
def test_the_send_click_is_never_recorded():
"""The one step a replay must never perform mechanically."""
steps = bs.distill_steps(p_sendscript_log())
names = [(s.get("params") or {}).get("name") for s in steps]
assert "Post" not in names
def test_an_unnameable_click_still_truncates():
"""The guard that made the old behaviour correct-but-useless has to stay correct: a click we
cannot name is a step we cannot replay, and keeping it would replay the wrong element."""
log = p_sendscript_log()
del log[1]["clicked_name"]
steps = bs.distill_steps(log)
assert all(s["tool"] == "BrowserNavigate" for s in steps) or steps == []