fix: contrôle strict des étapes + routage par machine_id
Corrections critiques après test E2E qui montrait des clics au mauvais endroit :
1. Routage par machine_id (api_stream.py)
Quand 2 machines partagent le même session_id (agent_demo_user),
les actions d'un replay pour la VM ne doivent PLUS être distribuées
au PC physique. Vérification que le replay_state appartient bien à
la machine qui poll avant de consommer la queue.
2. IRBuilder extrait expected_window_before/after (ir_builder.py)
Pour chaque action click/type/key_combo, stocke le titre de la fenêtre
au moment du clic (before) et le titre du prochain événement (after).
Ces champs alimentent le contrôle strict au runtime.
3. ExecutionCompiler crée SuccessCondition title_match (execution_compiler.py)
Quand expected_window_after est défini, crée une condition de succès
STRICTE avec method="title_match" et expected_title. Plus de simple
"l'écran a changé" — on vérifie la fenêtre résultante.
4. Runner propage expected_window_before et success_strict
Le flag success_strict indique à l'agent que le contrôle post-action
DOIT être strict (STOP sur mismatch au lieu de warning).
5. UIA strict sur parent_path (executor.py)
_resolve_via_uia_local REJETTE un match si l'élément trouvé n'est pas
dans la bonne fenêtre parente (évite ex: "Rechercher" taskbar confondu
avec "Rechercher" explorateur).
6. Pré/post vérif stricte et bloquante (executor.py)
- expected_window_before lu en priorité depuis l'action (plan V4)
- Post-vérif : si success_strict=True et timeout, result.success=False
→ le replay s'arrête au lieu de continuer avec des warnings.
Validé sur la VM :
- Le replay s'arrête proprement quand l'étape 2 aboutit dans "Propriétés de
Internet" au lieu de "blocnote.txt - Bloc-notes"
- Plus de clics en aveugle / saisie au mauvais endroit
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2594,10 +2594,35 @@ async def get_next_action(session_id: str, machine_id: str = "default"):
|
||||
"replay_id": state["replay_id"],
|
||||
}
|
||||
|
||||
queue = _replay_queues.get(session_id, [])
|
||||
# CRITIQUE : vérifier que la queue appartient BIEN à cette machine.
|
||||
# Quand 2 machines partagent le même session_id (ex: agent_demo_user),
|
||||
# il faut s'assurer qu'elles ne volent PAS les actions l'une de l'autre.
|
||||
# Un replay est lié à UNE machine_id spécifique via replay_states.
|
||||
# On cherche d'abord si cette machine a un replay actif qui lui est propre.
|
||||
queue = []
|
||||
owning_replay = None
|
||||
for state in _replay_states.values():
|
||||
if (state.get("machine_id") == machine_id
|
||||
and state.get("status") == "running"
|
||||
and state.get("session_id") == session_id):
|
||||
owning_replay = state
|
||||
break
|
||||
|
||||
if owning_replay:
|
||||
# Cette machine a un replay actif → consommer sa queue
|
||||
queue = _replay_queues.get(session_id, [])
|
||||
else:
|
||||
# Pas de replay pour cette machine sur cette session → NE RIEN DISTRIBUER
|
||||
# Même si _replay_queues[session_id] contient des actions, elles
|
||||
# appartiennent à une autre machine.
|
||||
queue = []
|
||||
|
||||
# Log seulement quand il y a des actions à distribuer
|
||||
if queue:
|
||||
logger.info(f"[REPLAY-QUEUE] session={session_id}, actions_en_attente={len(queue)}")
|
||||
logger.info(
|
||||
f"[REPLAY-QUEUE] session={session_id}, machine={machine_id}, "
|
||||
f"actions_en_attente={len(queue)}"
|
||||
)
|
||||
|
||||
if not queue and machine_id != "default":
|
||||
# Lookup 1 : machine_replay_target (mapping explicite POST /replay)
|
||||
@@ -2605,14 +2630,21 @@ async def get_next_action(session_id: str, machine_id: str = "default"):
|
||||
if target_sid and target_sid != session_id:
|
||||
target_queue = _replay_queues.get(target_sid, [])
|
||||
if target_queue:
|
||||
queue = target_queue
|
||||
_replay_queues[session_id] = target_queue
|
||||
del _replay_queues[target_sid]
|
||||
# Vérifier que le replay_state ciblé concerne BIEN cette machine
|
||||
target_state = None
|
||||
for state in _replay_states.values():
|
||||
if state["session_id"] == target_sid and state["status"] == "running":
|
||||
state["session_id"] = session_id
|
||||
_machine_replay_target[machine_id] = session_id
|
||||
logger.info(f"Replay machine-target: {machine_id} -> {target_sid} -> {session_id}")
|
||||
if (state.get("session_id") == target_sid
|
||||
and state.get("machine_id") == machine_id
|
||||
and state["status"] == "running"):
|
||||
target_state = state
|
||||
break
|
||||
if target_state:
|
||||
queue = target_queue
|
||||
_replay_queues[session_id] = target_queue
|
||||
del _replay_queues[target_sid]
|
||||
target_state["session_id"] = session_id
|
||||
_machine_replay_target[machine_id] = session_id
|
||||
logger.info(f"Replay machine-target: {machine_id} -> {target_sid} -> {session_id}")
|
||||
|
||||
# Lookup 2 : chercher dans les replay_states actifs pour cette machine
|
||||
if not queue:
|
||||
|
||||
Reference in New Issue
Block a user