From d298f03891476d978611b2a3cbaa3e7c33639756 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sun, 3 May 2026 16:59:52 +0200 Subject: [PATCH] fix(sae): structural fast-path for Instagram survey/interstitial modal detection The SAE perceive() had no structural fast-path for Instagram-internal modal overlays (surveys, rating prompts, interstitials). These modals live inside com.instagram.android and were invisible to the foreign-app detector. The LLM fallback frequently misclassified them as NORMAL, causing the bot to get trapped in survey dialogs. Added two O(1) structural detection layers: 1. Resource-ID markers: survey_overlay_container, interstitial_container, mystery_interstitial, nux_overlay, rating_prompt, feedback_dialog 2. Dismiss-button heuristic: cross-validates dismiss text with overlay container structure to prevent caption false positives Also removed dead code: xml_dump.lower() no-op. Fixes: test_perceive_instagram_survey_modal Zero regressions: 464 passed, 6 pre-existing failures --- GramAddict/core/situational_awareness.py | 53 +++++++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/GramAddict/core/situational_awareness.py b/GramAddict/core/situational_awareness.py index a50077d..b96b982 100644 --- a/GramAddict/core/situational_awareness.py +++ b/GramAddict/core/situational_awareness.py @@ -299,8 +299,6 @@ class SituationalAwarenessEngine: if not xml_dump or not isinstance(xml_dump, str): return SituationType.OBSTACLE_FOREIGN_APP - xml_dump.lower() - blocked_markers = [ "try again later", "action blocked", @@ -443,6 +441,57 @@ class SituationalAwarenessEngine: screen_memory.store_screen(compressed, "OBSTACLE_MODAL") return SituationType.OBSTACLE_MODAL + # ── Structural Fast-Check: Instagram-Internal Modal Overlays ── + # Surveys, rating prompts, and interstitial modals live INSIDE Instagram's + # package but block normal interaction. They share a common structural + # pattern: a container resource-id containing "survey", "interstitial", + # or "nux_" (new-user-experience), plus dismiss buttons ("Not Now"). + # Detecting them structurally is O(1) and eliminates LLM hallucination risk. + instagram_modal_markers = ( + "survey_overlay_container", # "How are you enjoying Instagram?" survey + "survey_title", # Survey title text view + "interstitial_container", # Generic interstitial blocker + "mystery_interstitial", # Unknown/dynamic interstitials + "nux_overlay", # New-user-experience onboarding modals + "rating_prompt", # App Store rating prompt + "feedback_dialog", # Feedback collection dialogs + ) + if any( + re.search(rf'resource-id="[^"]*{marker}[^"]*"', xml_dump, re.IGNORECASE) + for marker in instagram_modal_markers + ): + logger.info("🧠 [SAE Perceive] Instagram modal overlay detected structurally → OBSTACLE_MODAL") + screen_memory.store_screen(compressed, "OBSTACLE_MODAL") + return SituationType.OBSTACLE_MODAL + + # Fallback heuristic: detect modals by dismiss-button text patterns. + # If we see "Not Now" or "Take Survey" as button text inside Instagram, it's a modal. + # Guard: match ONLY inside short text attributes (< 40 chars) to avoid caption false positives. + dismiss_button_patterns = ( + r'text="Not Now"', + r'text="not now"', + r'text="Nicht jetzt"', # German: "Not Now" + r'text="Take Survey"', + r'text="rate \d+ stars?"', # "rate 5 stars" + r'text="Bewerten"', # German: "Rate" + ) + has_dismiss_button = any(re.search(p, xml_dump, re.IGNORECASE) for p in dismiss_button_patterns) + if has_dismiss_button: + # Cross-validate: must also have a container that looks like a dialog/overlay + # (not just a random "Not Now" text in a DM thread or post caption) + has_overlay_structure = bool( + re.search( + r'resource-id="[^"]*(?:overlay|dialog|interstitial|survey|sheet|prompt)[^"]*"', + xml_dump, + re.IGNORECASE, + ) + or re.search(r'resource-id="[^"]*button_(?:negative|positive)[^"]*"', xml_dump, re.IGNORECASE) + ) + if has_overlay_structure: + logger.info("🧠 [SAE Perceive] Instagram dismiss-button modal detected structurally → OBSTACLE_MODAL") + screen_memory.store_screen(compressed, "OBSTACLE_MODAL") + return SituationType.OBSTACLE_MODAL + # If not cached, query LLM for autonomous structural classification try: from GramAddict.core.config import Config