fix(navigation): harden obstacle guard and structural sanity checks against phantom keyboards and reply-box traps

This commit is contained in:
2026-05-05 11:27:31 +02:00
parent c3f84088aa
commit 59cd5477af
5 changed files with 58 additions and 17 deletions

View File

@@ -47,21 +47,27 @@ class ObstacleGuardPlugin(BehaviorPlugin):
logger.warning("⚠️ [ObstacleGuard] System permission dialog detected. Dismissing with BACK...")
ctx.device.press("back")
sleep(1.5 * ctx.sleep_mod)
return BehaviorResult(executed=True, should_skip=True)
res = BehaviorResult(executed=True, should_skip=True)
res.skip_type = "no_scroll"
return res
# ── Foreign App Takeover (e.g. browser opened, wrong app in foreground) ──
if situation == SituationType.OBSTACLE_FOREIGN_APP:
logger.warning("⚠️ [ObstacleGuard] Foreign app detected. Pressing BACK to recover...")
ctx.device.press("back")
sleep(1.5 * ctx.sleep_mod)
return BehaviorResult(executed=True, should_skip=True)
res = BehaviorResult(executed=True, should_skip=True)
res.skip_type = "no_scroll"
return res
# ── On-Screen Keyboard (e.g. hallucinated click on comment field) ──
if situation == SituationType.OBSTACLE_KEYBOARD:
logger.warning("⚠️ [ObstacleGuard] On-screen Keyboard is open. Pressing BACK to dismiss...")
ctx.device.press("back")
sleep(1.0 * ctx.sleep_mod)
return BehaviorResult(executed=True, should_skip=True)
res = BehaviorResult(executed=True, should_skip=True)
res.skip_type = "no_scroll"
return res
# ── Instagram Modal / Overlay (survey, "Not Now" prompt, creation flow) ──
if situation == SituationType.OBSTACLE_MODAL:
@@ -88,6 +94,8 @@ class ObstacleGuardPlugin(BehaviorPlugin):
else:
ctx.shared_state["consecutive_marker_misses"] = misses + 1
return BehaviorResult(executed=True, should_skip=True) # Restart loop for same post or next
res = BehaviorResult(executed=True, should_skip=True) # Restart loop for same post or next
res.skip_type = "no_scroll"
return res
return BehaviorResult(executed=False)

View File

@@ -1134,6 +1134,9 @@ def _run_zero_latency_feed_loop(
elif skip_type == "fast":
_humanized_scroll(device, is_skip=True)
sleep(1.0 * sleep_mod)
elif skip_type == "no_scroll":
logger.debug("⏭️ Skipping post without scrolling (e.g., dismissing obstacle).")
sleep(1.0 * sleep_mod)
else:
_humanized_scroll(device, is_skip=False)
sleep(1.5 * sleep_mod)

View File

@@ -347,8 +347,21 @@ class SituationalAwarenessEngine:
"com.apple.android.inputmethod",
}
if any(pkg in keyboard_pkgs for pkg in packages):
logger.info("📱 [SAE Perceive] On-screen Keyboard explicitly detected. Treating as obstacle.")
return SituationType.OBSTACLE_KEYBOARD
# Differentiate between actual EditText focus and layout-induced phantom keyboard signals
is_actual_keyboard = False
if 'focused="true"' in xml_dump and "EditText" in xml_dump:
if re.search(r'class="[^"]*EditText"[^>]*focused="true"', xml_dump) or re.search(
r'focused="true"[^>]*class="[^"]*EditText"', xml_dump
):
is_actual_keyboard = True
if is_actual_keyboard:
logger.info(
"📱 [SAE Perceive] On-screen Keyboard explicitly detected (EditText focused). Treating as obstacle."
)
return SituationType.OBSTACLE_KEYBOARD
else:
logger.debug("📱 [SAE Perceive] Phantom keyboard signal detected (No focused EditText). Ignoring.")
# ── Foreign Environment Detection (package-based) ──
# If the main app package is completely absent from the UI hierarchy,

View File

@@ -85,6 +85,18 @@ class TelepathicEngine:
filtered_candidates.append(c)
candidates = filtered_candidates
# 2.5 Structural Sanity Check (Filter out traps before VLM sees them)
safe_candidates = []
for c in candidates:
if self._structural_sanity_check(c, intent_description):
safe_candidates.append(c)
if not safe_candidates:
logger.warning(f"All candidates failed structural sanity check for intent: '{intent_description}'")
return None
candidates = safe_candidates
# 3. Resolve intent against candidates
best_node = self._resolver.resolve(intent_description, candidates, device=device)
@@ -224,13 +236,18 @@ class TelepathicEngine:
def _is_instagram_context(self, xml_string: str) -> bool:
return "com.instagram.android" in xml_string
def _structural_sanity_check(self, node: dict, intent_description: str, screen_height: int = 2400) -> bool:
def _structural_sanity_check(self, node: SpatialNode, intent_description: str, screen_height: int = 2400) -> bool:
"""
Structural guard to ensure nodes are in valid locations for their intents.
"""
intent = intent_description.lower()
y = node.get("y", 0)
semantic = (node.get("semantic_string", "") or "").lower()
y = node.center_y
semantic = ((node.text or "") + " " + (node.content_desc or "") + " " + (node.resource_id or "")).lower()
# 0. EXTREME GUARD: NEVER pick an input field (EditText) or reply box unless explicitly asked to type/reply.
if "edittext" in (node.class_name or "").lower() or "reply" in semantic:
if "type" not in intent and "reply" not in intent and "message" not in intent and "search" not in intent:
return False
# 1. Post Username Guard
if "post username" in intent or "author username" in intent:
@@ -277,7 +294,7 @@ class TelepathicEngine:
# 6. Block massive layout containers UNLESS specifically looking for feed/post
MAX_CONTAINER_AREA = 500000
area = node.get("area", 0)
area = node.area
is_feed_or_post = "feed" in intent or "post" in intent
is_grid_item = "grid" in intent or "list" in intent

View File

@@ -11,15 +11,15 @@ def test_media_intent_rejects_grid_containers():
engine = TelepathicEngine()
screen_height = 2400
from GramAddict.core.perception.spatial_parser import SpatialNode
# Mock node representing a massive RecyclerView containing the entire grid
# Area is 1080 * 2400 = 2592000 > MAX_CONTAINER_AREA (500000)
massive_grid_container = {
"semantic_string": "id context: 'swipeable nav view pager inner recycler view'",
"area": 2592000,
"y": 1200,
"class_name": "androidx.recyclerview.widget.RecyclerView",
"resource_id": "com.instagram.android:id/swipeable_nav_view_pager_inner_recycler_view",
}
massive_grid_container = SpatialNode(
bounds=(0, 0, 1080, 2400),
class_name="androidx.recyclerview.widget.RecyclerView",
resource_id="com.instagram.android:id/swipeable_nav_view_pager_inner_recycler_view",
)
# Intent
intent = "first image post in profile grid"