From f46b0b7bcbc7bb92f9b3fda76ae8c45e4e37f233 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sun, 3 May 2026 15:42:09 +0200 Subject: [PATCH] chore: harden verify_success fallbacks to Fail-Fast and apply aggressive GOAP penalty --- GramAddict/core/goap.py | 6 +++++- GramAddict/core/perception/action_memory.py | 14 ++++++++++++-- tests/e2e/test_goap_loop_prevention.py | 4 ++-- tests/e2e/test_system_llm_perception.py | 11 +++++------ 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/GramAddict/core/goap.py b/GramAddict/core/goap.py index 54a3590..4edadb7 100644 --- a/GramAddict/core/goap.py +++ b/GramAddict/core/goap.py @@ -496,8 +496,12 @@ class GoalExecutor: return False else: # action_success is None (INCONCLUSIVE) - # We decay the memory so it unlearns if it repeatedly fails to produce a definitive success. + logger.warning(f"⚠️ [GOAP Execute] Applying AGGRESSIVE PENALTY for inconclusive action '{action}'.") engine.decay_click(action) + # Double penalty to burn ambiguous paths faster (outer loop adds +1, so total +2 = instantly hits MAX_RETRIES) + self.action_failures[(pre_action_screen_type, action)] = ( + self.action_failures.get((pre_action_screen_type, action), 0) + 1 + ) return False def _execute_recalled_path(self, steps: List[Dict], goal: str) -> bool: diff --git a/GramAddict/core/perception/action_memory.py b/GramAddict/core/perception/action_memory.py index 99a5986..0e19570 100644 --- a/GramAddict/core/perception/action_memory.py +++ b/GramAddict/core/perception/action_memory.py @@ -167,7 +167,8 @@ class ActionMemory: and "row_feed_button_like" not in post_xml_lower and "clips_viewer" not in post_xml_lower ): - return None # Still on grid, inconclusive + logger.warning(f"⚠️ [ActionMemory] Still on grid after trying to '{intent}'. Verification FAIL.") + return False # Still on grid, definitely failed state_toggles = ["like", "save", "follow", "heart"] is_toggle = any(t in intent_lower for t in state_toggles) @@ -269,7 +270,10 @@ class ActionMemory: if diff > 0: logger.debug(f"🧠 [ActionMemory] Structural delta detected for toggle '{intent}'. Verification PASS.") return True - else: + logger.warning( + f"⚠️ [ActionMemory] Zero structural shift (diff={diff}) for state-toggle '{intent}'. Verification FAIL." + ) + return False # If the intent is an abstract goal (like "find customers"), diff > 50 is NOT enough. # We must force visual VLM confirmation because clicking the wrong thing (like "Create highlight") # also produces a large diff but achieves the wrong goal. @@ -317,6 +321,12 @@ class ActionMemory: logger.warning(f"⚠️ [ActionMemory] Cannot visually verify abstract intent '{intent}'. Failing safe.") return False + # If diff <= 50 for non-toggle + logger.warning( + f"⚠️ [ActionMemory] Insufficient structural change (diff={diff}) for non-toggle '{intent}'. Verification FAIL." + ) + return False + def _intent_matches_node(intent: str, semantic_string: str) -> bool: """Checks if the clicked element semantically matches the toggle intent. diff --git a/tests/e2e/test_goap_loop_prevention.py b/tests/e2e/test_goap_loop_prevention.py index 8009b27..9290f83 100644 --- a/tests/e2e/test_goap_loop_prevention.py +++ b/tests/e2e/test_goap_loop_prevention.py @@ -54,9 +54,9 @@ def test_goap_planner_avoids_infinite_loop_on_masked_edge(): action_failures=action_failures, ) - # The HD Map should fail, and because the planner is trapped, it forces a restart + # The HD Map should fail, and because the planner is trapped, it falls back to back-tracking assert ( - action_avoided == "force start instagram" + action_avoided == "press back" or action_avoided == "force start instagram" ), "Planner routed BLIND into the dead end despite the edge being masked!" diff --git a/tests/e2e/test_system_llm_perception.py b/tests/e2e/test_system_llm_perception.py index f75d8de..f0d3a5b 100644 --- a/tests/e2e/test_system_llm_perception.py +++ b/tests/e2e/test_system_llm_perception.py @@ -855,14 +855,13 @@ class TestVerifySuccessStructuralDelta: result = memory.verify_success("like", pre_click_xml=pre_xml, post_click_xml=post_xml) assert result is True - def test_toggle_zero_diff_is_none(self): - """Zero XML change for a toggle = inconclusive (not confirmed).""" + def test_toggle_zero_diff_is_false(self): + """Zero XML change for a toggle = fail.""" memory, _ = self._make_memory() same_xml = '' result = memory.verify_success("like", pre_click_xml=same_xml, post_click_xml=same_xml) - # Zero diff, no markers → None (inconclusive) not True - assert result is None or result is False + assert result is False def test_follow_success_with_requested_marker(self): """ @@ -888,7 +887,7 @@ class TestVerifySuccessStructuralDelta: result = memory.verify_success("view a post", pre_click_xml="", post_click_xml=post_xml) assert result is True - def test_view_post_still_on_grid_is_inconclusive(self): + def test_view_post_still_on_grid_is_false(self): """ If after 'view a post' the XML still shows explore_action_bar WITHOUT post detail markers, navigation failed. @@ -896,7 +895,7 @@ class TestVerifySuccessStructuralDelta: memory, _ = self._make_memory() post_xml = '' result = memory.verify_success("grid item", pre_click_xml="", post_click_xml=post_xml) - assert result is None # Inconclusive + assert result is False def test_semantic_gate_blocks_wrong_toggle_element(self): """