fix(perception): 3 production bug regressions from 2026-05-01 run

Bug 1 — VLM Profile Tab Hallucination:
  - VLM confused 'Profile' nav tab with post author username
  - Added Author Tab Guard to filter_navigation_conflicts()
  - Fixed mock LLM to use structural row_feed_photo_profile matching
    instead of hardcoded username (was the test lie)

Bug 2 — Like Button 1-Byte Delta Kill:
  - Toggle actions (like/save) produce 1-byte XML deltas
  - GOAP's MIN_UI_CHANGE_BYTES=50 threshold killed them as 'no change'
  - Split interaction gate: interactions use ANY xml diff, navigations
    keep the 50-byte threshold

Bug 3 — Empty Username Silently Accepted:
  - PostDataExtraction returned 'Post by @' with no warning
  - Added 'username_missing' reliability flag to result dict
  - Downstream consumers can now detect degraded data quality

Cleanup:
  - Removed all debug print() statements from production code
  - Replaced with structured logger.debug() calls
This commit is contained in:
2026-05-01 21:57:56 +02:00
parent df18a48a84
commit 2e1edec56a
15 changed files with 1824 additions and 7 deletions

View File

@@ -427,7 +427,11 @@ class GoalExecutor:
action_success = False
else:
# For interactions (like, follow) or unknown goals, use XML delta + semantic verify
if ui_changed:
# REGRESSION FIX 2026-05-01: Toggle actions (like/save) produce tiny XML deltas
# (e.g. checked="false" → "true" = 1 byte). We must NOT gate interactions on
# MIN_UI_CHANGE_BYTES. ANY change at all warrants semantic verification.
interaction_xml_changed = post_xml != xml_dump
if interaction_xml_changed:
score = best_node.get("score", 0.0) if best_node else 0.0
verification = engine.verify_success(action, post_xml, device=self.device, confidence=score)
if verification is True:

View File

@@ -52,9 +52,9 @@ def extract_post_content(context_xml: str, device=None) -> dict:
This is the BOT'S EYES — what it actually "sees" about each post.
Returns:
{'username': str, 'description': str, 'caption': str}
{'username': str, 'description': str, 'caption': str, 'username_missing': bool}
"""
result = {"username": "", "description": "", "caption": ""}
result = {"username": "", "description": "", "caption": "", "username_missing": False}
try:
from GramAddict.core.telepathic_engine import TelepathicEngine
@@ -72,7 +72,10 @@ def extract_post_content(context_xml: str, device=None) -> dict:
# 2. Learn/extract post media description dynamically
media_node = telepath.find_best_node(
context_xml, "post media content (the actual image or video, exclude bottom tabs)", min_confidence=0.35, device=device
context_xml,
"post media content (the actual image or video, exclude bottom tabs)",
min_confidence=0.35,
device=device,
)
if media_node and media_node.get("original_attribs", {}).get("desc"):
result["description"] = media_node["original_attribs"]["desc"].strip()
@@ -89,6 +92,11 @@ def extract_post_content(context_xml: str, device=None) -> dict:
except Exception as e:
logger.warning(f"Error extracting post content autonomously: {e}")
# REGRESSION FIX 2026-05-01: Flag unreliable data when username is empty
if not result["username"]:
result["username_missing"] = True
logger.warning("⚠️ [PostDataExtraction] Username is empty — data may be unreliable.")
return result

View File

@@ -48,10 +48,14 @@ class IntentResolver:
Production bug 2026-04-30: VLM picked action_bar_button_back
for "tap profile tab" → account switch failed.
Production bug 2026-05-01: VLM picked profile_tab (desc='Profile')
for "post author username text" → navigated to own profile instead.
Rules:
- For tab intents: exclude nodes with "back" in resource_id or
content_desc == "Back"
- For back/close intents: no filtering (Back is the correct target)
- For author/username intents: exclude bottom navigation tabs
"""
intent_lower = intent_description.lower()
@@ -60,6 +64,11 @@ class IntentResolver:
filtered = []
is_tab_intent = "tab" in intent_lower and "back" not in intent_lower
is_create_intent = "create" in intent_lower or "camera" in intent_lower or "story" in intent_lower
# REGRESSION FIX 2026-05-01: Author/username intents must never pick nav tabs
is_author_intent = any(kw in intent_lower for kw in ["author", "username", "post media"])
# Known bottom navigation tab resource_id suffixes
NAV_TAB_SUFFIXES = ("_tab", "tab_icon", "navigation_bar")
for node in candidates:
rid = (node.resource_id or "").lower()
@@ -68,6 +77,7 @@ class IntentResolver:
is_back = "back" in rid or desc == "back"
is_close = "close" in rid or desc == "close"
is_create = "camera" in rid or "create" in rid or desc == "camera" or desc == "create" or "creation" in rid
is_nav_tab = any(rid.endswith(s) for s in NAV_TAB_SUFFIXES)
if is_tab_intent and (is_back or is_close):
logger.debug(
@@ -76,6 +86,13 @@ class IntentResolver:
)
continue
if is_author_intent and is_nav_tab:
logger.debug(
f"🛡️ [Author Tab Guard] Excluded nav tab '{node.resource_id}' "
f"(desc='{node.content_desc}') for author intent '{intent_description}'"
)
continue
if not is_create_intent and is_create:
logger.debug(
f"🛡️ [Creation Conflict Guard] Excluded '{node.resource_id}' "
@@ -374,8 +391,7 @@ class IntentResolver:
label_parts.append("(no visible text)")
box_legend_lines.append(f" [{idx}] {', '.join(label_parts)}")
box_legend = "\n".join(box_legend_lines)
print("BOX LEGEND:")
print(box_legend)
logger.debug(f"BOX LEGEND:\n{box_legend}")
prompt = (
f"You are looking at a mobile app screenshot with numbered bounding boxes drawn around interactive UI elements.\n"