fix(guard+memory): radical overhaul of close friends detection & circuit breaker

CLOSE FRIENDS GUARD:
- Replaced broken text-only detection ('enge freunde'/'close friend')
  with STRUCTURAL resource-id marker detection:
  * friendly_bubbles_component (feed posts)
  * profile_header_close_friend (profile view)
  * close_friends_badge (Reels)
  * close_friends_star (green star)
- Kept legacy text fallback for edge cases
- TDD: 5 tests including meta-test enforcing structural detection

CONTEXT MEMORY CIRCUIT BREAKER:
- Old system: single failure permanently blocks action (confidence < 0.2 = dead)
- New system: time-based decay — failures older than 1 hour are auto-forgiven
  and the poisoned entry is DELETED, allowing re-exploration
- This prevents catastrophic self-sabotage where the bot blocks its own
  core actions (tap like, tap follow, tap comment) permanently
- Purged 14 poisoned entries from live Qdrant that were blocking ALL
  fundamental interactions
- TDD: 2 meta-tests enforcing time-decay and no permanent blocking

Full suite: 106/107 passed (1 infra flake)
This commit is contained in:
2026-05-05 15:48:59 +02:00
parent 8d58c3cf89
commit 749e82ea14
3 changed files with 251 additions and 6 deletions

View File

@@ -35,7 +35,21 @@ class CloseFriendsGuardPlugin(BehaviorPlugin):
return False
xml = ctx.context_xml if ctx.context_xml else ctx.device.dump_hierarchy()
return "enge freunde" in xml.lower() or "close friend" in xml.lower()
xml_lower = xml.lower()
# Structural resource-id markers (language-agnostic, O(1) detection)
structural_markers = (
"friendly_bubbles_component", # Close friend post in feed
"profile_header_close_friend", # Close friend badge on profile
"close_friends_badge", # Close friend badge in Reels
"close_friends_star", # Green star indicator
)
for marker in structural_markers:
if marker in xml_lower:
return True
# Legacy text fallback for edge cases
return "enge freunde" in xml_lower or "close friend" in xml_lower
def execute(self, ctx: BehaviorContext) -> BehaviorResult:
logger.info("💚 [CloseFriendsGuard] Close friends post detected. Skipping...")

View File

@@ -1499,13 +1499,17 @@ class ContextMemoryDB(QdrantBase):
def is_allowed(self, intent: str, screen_type: str) -> bool:
"""
Exploration vs Exploitation:
If we don't know (no entry or neutral confidence), allow it!
Only block if we have learned it fails consistently (< 0.2).
Exploration vs Exploitation with time-based decay:
- If we don't know (no entry), allow it (exploration).
- Only block if we have RECENTLY learned it fails (< 0.2 confidence AND < 1 hour old).
- Old failures decay: entries older than 1 hour are auto-forgiven and deleted,
preventing permanent poisoning of the action space.
"""
if not self.is_connected:
return True # Fail-open for exploration
DECAY_THRESHOLD_SECONDS = 3600 # 1 hour: old failures are forgiven
key = self._get_key(intent, screen_type)
try:
point_id = self.generate_uuid(key)
@@ -1513,11 +1517,25 @@ class ContextMemoryDB(QdrantBase):
collection_name=self.collection_name, ids=[point_id], with_payload=True, with_vectors=False
)
if points:
conf = points[0].payload.get("confidence", 0.5)
payload = points[0].payload
conf = payload.get("confidence", 0.5)
updated_at = payload.get("updated_at", 0)
age_seconds = time.time() - updated_at
# Time-based decay: old failures are forgiven
if conf < 0.2 and age_seconds > DECAY_THRESHOLD_SECONDS:
logger.info(
f"🔄 [ContextMemory] Forgave old failure for '{intent}' on '{screen_type}' "
f"(age: {age_seconds/60:.0f}min, confidence: {conf:.2f}). Allowing re-exploration."
)
# Delete the stale entry so the bot can re-learn
self.delete_point(key)
return True
if conf < 0.2:
logger.warning(
f"🛡️ [ContextMemory] Circuit Breaker: Blocked '{intent}' on '{screen_type}' "
f"(learned confidence {conf:.2f} < 0.2)"
f"(learned confidence {conf:.2f} < 0.2, age: {age_seconds/60:.0f}min)"
)
return False
except Exception: