fix: Fenêtre incorrecte strict → pause supervisée pour apprentissage

Symétrie avec le fix 7cc03f6f1 (no_screen_change strict → paused_need_help).

Avant : si l'agent détecte en pré-vérification que la fenêtre active
n'est pas celle attendue, l'erreur retombait dans la branche retry+stop
legacy → 3 retries inutiles puis status=error et queue vidée.

C'est une violation de feedback_failure_is_learning.md : un échec Léa
n'est jamais un "stop avec error", c'est un moment pédagogique.

Maintenant :
  1. L'agent envoie warning="wrong_window" dans le résultat (en plus
     de l'error textuel existant). Ajouté aux 2 chemins :
     - pré-vérif (expected_window_before mismatch, executor.py ~587)
     - post-vérif strict (expected_window_title timeout, executor.py ~820)
  2. Le serveur détecte warning="wrong_window" AVANT la branche
     retry+stop legacy → redirection vers paused_need_help
  3. pause_message explicite : "Je m'attendais à voir la bonne fenêtre
     mais je vois autre chose. Peux-tu vérifier que l'application est
     au premier plan ?"
  4. Queue intacte (l'action reste en tête, prête à être relancée)
  5. log_replay_failure pour l'apprentissage futur

Cause fréquente identifiée : les popups de Léa elle-même (notifications,
fenêtre de chat) volent le focus Windows pendant le replay → l'app cible
perd le premier plan → pré-vérif détecte le mismatch. Bug UX séparé à
traiter (Léa ne devrait pas prendre le focus pendant un replay actif).

Appliqué aux 2 copies de l'agent (dev + deploy).

Tests : 56 E2E + Phase0 passent, 0 régression.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-04-12 10:41:29 +02:00
parent 47993e2ee9
commit 02ee2d7b5b
3 changed files with 1370 additions and 132 deletions

View File

@@ -585,6 +585,7 @@ class ActionExecutorV1:
pass pass
result["success"] = False result["success"] = False
result["error"] = f"Fenêtre incorrecte: '{current_title}' (attendu: '{expected_title}')" result["error"] = f"Fenêtre incorrecte: '{current_title}' (attendu: '{expected_title}')"
result["warning"] = "wrong_window"
return result return result
else: else:
logger.info(f"[LEA] Pré-vérif OK : '{current_title}'") logger.info(f"[LEA] Pré-vérif OK : '{current_title}'")
@@ -818,6 +819,7 @@ class ActionExecutorV1:
f"Post-vérif échouée : fenêtre '{post_title}' " f"Post-vérif échouée : fenêtre '{post_title}' "
f"au lieu de '{expected_after}'" f"au lieu de '{expected_after}'"
) )
result["warning"] = "wrong_window"
print( print(
f" [POST-VÉRIF] STOP STRICT — l'étape ne s'est " f" [POST-VÉRIF] STOP STRICT — l'étape ne s'est "
f"pas déroulée comme prévu, arrêt du replay" f"pas déroulée comme prévu, arrêt du replay"

File diff suppressed because it is too large Load Diff

View File

@@ -3170,6 +3170,68 @@ async def report_action_result(report: ReplayResultReport):
replay_state["completed_actions"] += 1 replay_state["completed_actions"] += 1
replay_state["current_action_index"] += 1 replay_state["current_action_index"] += 1
elif not report.success and agent_warning == "wrong_window":
# L'agent a détecté en pré-vérification que la fenêtre active
# n'est pas celle attendue. Même philosophie que no_screen_change :
# un échec est un moment pédagogique, pas un stop.
#
# Causes fréquentes : Léa elle-même a pris le focus (popups de
# notification/chat), l'app cible s'est fermée, une popup système
# est apparue, l'écran a changé entre deux actions.
#
# On redirige vers paused_need_help pour que l'humain intervienne.
_tspec_ww = (original_action or {}).get("target_spec") or report.target_spec or {}
_intent_ww = ""
_idx_ww = replay_state.get("current_action_index", 0)
_actions_ww = replay_state.get("actions", [])
if 0 <= _idx_ww < len(_actions_ww):
_intent_ww = str((_actions_ww[_idx_ww] or {}).get("intention", "") or "")
_target_desc_ww = (
_intent_ww
or _tspec_ww.get("by_text", "")
or _tspec_ww.get("vlm_description", "")[:80]
or "cette action"
)
replay_state["status"] = "paused_need_help"
replay_state["failed_action"] = {
"action_id": action_id,
"type": (original_action or {}).get("type", "unknown"),
"target_description": _target_desc_ww,
"screenshot_b64": screenshot_after or report.screenshot,
"target_spec": _tspec_ww,
"reason": "wrong_window",
"error_detail": report.error or "",
}
replay_state["pause_message"] = (
f"Je m'attendais à voir la bonne fenêtre mais je vois autre "
f"chose. Peux-tu vérifier que l'application est au premier "
f"plan ? ({report.error or ''})"
)
error_entry = {
"action_id": action_id,
"error": report.error or "wrong_window",
"retry_count": retry_count,
"timestamp": time.time(),
}
replay_state["error_log"].append(error_entry)
logger.warning(
f"Replay PAUSE supervisée (wrong_window) : {action_id} "
f"{report.error or 'fenêtre incorrecte'} — en attente "
f"d'intervention humaine"
)
try:
log_replay_failure(
replay_id=replay_state["replay_id"],
action_id=action_id,
target_spec=_tspec_ww,
screenshot_b64=screenshot_after or report.screenshot,
error="wrong_window",
extra={"error_detail": report.error or "", "intent": _intent_ww},
)
except Exception as _log_exc:
logger.debug("log_replay_failure skip: %s", _log_exc)
elif not report.success and agent_warning == "no_screen_change": elif not report.success and agent_warning == "no_screen_change":
# L'action a été exécutée mais l'écran n'a pas changé. # L'action a été exécutée mais l'écran n'a pas changé.
# #