fix: no_screen_change strict → pause supervisée pour apprentissage

Rectification de la branche C introduite dans a21f1ea9f.

## Ce qui était faux

a21f1ea9f faisait :
  strict + no_screen_change → retry × 3 → status=error → queue vidée

C'est le réflexe d'un RPA classique qui se casse la figure quand ça
rate. Ce n'est PAS la philosophie Léa.

Dom m'a rappelé que j'avais oublié ma propre vision documentée dans
project_lea_apprentissage_plan.md et feedback_not_a_click_box.md :
*"Quand elle dit qu'elle n'a pas trouvé X, elle demande montre-moi.
C'est à ce moment qu'il faudrait passer en mode apprentissage."*

## Ce qui est correct maintenant

  strict + no_screen_change
    → status = "paused_need_help"
    → failed_action stocké (target, screenshot, method, score, reason)
    → pause_message demandant l'intervention humaine
    → queue intacte (l'action reste en tête, prête à être relancée)
    → log_replay_failure pour l'apprentissage futur
    → l'agent reçoit replay_paused=True dans /replay/next et s'arrête
    → l'humain corrige physiquement sur la machine cible
    → le replay reprend via /replay/{replay_id}/resume

Redirection vers le mécanisme paused_need_help qui existe déjà pour le
cas target_not_found. Zéro nouveau code de pause, juste une 2ème entrée
dans ce mécanisme.

Le comportement legacy (success_strict=False) reste inchangé : on
log un warning et on continue, comportement tolérant pour les actions
non-critiques.

## Lesson apprises

1. Toujours relire les fichiers mémoire pertinents AVANT d'implémenter
   une branche de gestion d'erreur (nouvelle règle dans
   feedback_reread_before_code.md)
2. Un échec Léa n'est jamais un "stop avec error" — c'est un moment
   pédagogique (nouvelle règle dans feedback_failure_is_learning.md)
3. Ne pas s'auto-presser quand Dom n'a jamais demandé d'aller vite

## Tests

- 56 tests E2E + Phase0 passent, 0 régression
- Comportement vérifié par inspection du code : pause_message formé
  correctement, queue préservée, log_replay_failure appelé

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-04-11 09:27:45 +02:00
parent a21f1ea9fa
commit 7cc03f6f10

View File

@@ -3173,56 +3173,84 @@ async def report_action_result(report: ReplayResultReport):
elif not report.success and agent_warning == "no_screen_change":
# L'action a été exécutée mais l'écran n'a pas changé.
#
# Philosophie Léa (feedback_failure_is_learning.md) : un échec
# n'est jamais un stop avec error — c'est un **moment pédagogique**.
# Léa demande à l'humain de montrer ce qu'elle aurait dû faire.
#
# Comportement legacy (success_strict=False) : loguer l'échec
# et continuer vers l'action suivante. Justifié pour les
# workflows tolérants où un clic "sans effet" peut être normal
# (ex: cliquer sur une case déjà cochée).
#
# Comportement strict (success_strict=True) : écran inchangé =
# vraie erreur. On déclenche un retry, puis stop après épuisement.
# Justifié pour les workflows critiques où chaque action doit
# produire un effet visuel observable.
# "je n'ai pas su faire". On redirige vers le mécanisme de pause
# supervisée existant (paused_need_help) pour que Léa demande à
# l'humain de montrer. Pas de retry automatique, pas de stop —
# on laisse la queue intacte et on attend l'intervention.
_is_strict = False
_intent_strict = ""
_idx_strict = replay_state.get("current_action_index", 0)
_actions_meta_strict = replay_state.get("actions", [])
if 0 <= _idx_strict < len(_actions_meta_strict):
_current_strict = _actions_meta_strict[_idx_strict] or {}
_is_strict = bool(_current_strict.get("success_strict", False))
_intent_strict = str(_current_strict.get("intention", "") or "")
if _is_strict and retry_count < MAX_RETRIES_PER_ACTION:
# Strict + retries restants : on réinjecte l'action
logger.warning(
f"Action {action_id} STRICT : écran inchangé — "
f"retry {retry_count + 1}/{MAX_RETRIES_PER_ACTION}"
if _is_strict:
# Apprentissage supervisé : pause, demande d'intervention
_tspec = (original_action or {}).get("target_spec") or report.target_spec or {}
_target_desc = (
_intent_strict
or _tspec.get("by_text", "")
or _tspec.get("vlm_description", "")[:80]
or "cette action"
)
_schedule_retry(
session_id, replay_state,
original_action or {"action_id": action_id},
retry_count, "no_screen_change_strict",
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,
"screenshot_b64": screenshot_after or report.screenshot,
"target_spec": _tspec,
"reason": "no_screen_change_strict",
"resolution_method": report.resolution_method or "",
"resolution_score": report.resolution_score or 0,
}
replay_state["pause_message"] = (
f"Mon clic sur '{_target_desc}' n'a produit aucun effet. "
f"Peux-tu me montrer où je devais cliquer ?"
)
elif _is_strict:
# Strict + retries épuisés : échec définitif, stop replay
replay_state["failed_actions"] += 1
replay_state["unverified_actions"] += 1
error_entry = {
"action_id": action_id,
"error": "no_screen_change after strict retries",
"error": f"no_screen_change_strict: {_target_desc}",
"retry_count": retry_count,
"timestamp": time.time(),
}
replay_state["error_log"].append(error_entry)
replay_state["status"] = "error"
_replay_queues[session_id] = []
logger.error(
f"Replay {replay_state['replay_id']} STRICT échoué : "
f"action {action_id} écran inchangé après {retry_count} retries"
logger.warning(
f"Replay PAUSE supervisée (apprentissage) : {action_id} "
f"écran inchangé sur '{_target_desc}' — en attente "
f"d'intervention humaine"
)
_notify_error_callback(
replay_state, action_id,
"no_screen_change after strict retries",
# Logger l'échec pour l'apprentissage futur
try:
log_replay_failure(
replay_id=replay_state["replay_id"],
action_id=action_id,
target_spec=_tspec,
screenshot_b64=screenshot_after or report.screenshot,
error="no_screen_change_strict",
extra={
"target_description": _target_desc,
"resolution_method": report.resolution_method or "",
"resolution_score": report.resolution_score or 0,
"actions_completed": replay_state["completed_actions"],
},
)
except Exception as _log_exc:
logger.debug("log_replay_failure skip: %s", _log_exc)
else:
# Legacy : on continue
# Legacy (non-strict) : on continue, comportement historique
replay_state["unverified_actions"] += 1
replay_state["completed_actions"] += 1
replay_state["current_action_index"] += 1