snapshot: WIP 5j replay reliability (B1 watchdog + dialog handlers + grounding drift)

Snapshot avant correction du blocage relance Léa (3 incidents 24h: SSH refusé,
polls morts ×2). Point de rollback stable.

Contenu:
- agent_v1/core/executor.py: 5 patchs dialog handling (saveas drift, close_tab
  hotkey fallback, confirm_save Unicode apostrophe, foreground dialog
  recontextualization, runtime_dialog in-loop) + helpers normalize_window_hint,
  requires_post_verify_window_transition
- agent_v1/core/grounding.py: garde drift template fix (fallback_x/y plumbed)
- server_v1/replay_watchdog.py (NEW): orphan watchdog B1, scan 10s timeout 30s
- server_v1/api_stream.py: dispatched_action plumbing, watchdog lifespan,
  metrics endpoint
- server_v1/replay_engine.py: _schedule_retry préserve original_action +
  dispatched_action
- stream_processor.py: gardes _infer_tab_switch_target (no false switch_tab
  on save_as dialog open) + _attach_expected_window_before
- tests/integration: test_replay_watchdog.py (8 cas), test_stream_processor.py
- tests/unit: test_executor_verify_window_guard.py (start_button, close_tab,
  runtime_dialog, post_verify, transition fallbacks)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-05-24 16:48:37 +02:00
parent 5ea4960e65
commit 7df51d2c79
47 changed files with 9811 additions and 451 deletions

View File

@@ -504,6 +504,100 @@ class SmartTrayV1:
threading.Thread(target=_replay, daemon=True).start()
def _launch_replay_request(
self,
replay_request: Dict[str, Any],
replay_name: str,
) -> None:
"""Lance un replay direct depuis un payload `replay_request` serveur."""
endpoint = (replay_request or {}).get("endpoint", "")
session_id = (replay_request or {}).get("session_id", "")
machine_id = (replay_request or {}).get("machine_id") or self.machine_id
if endpoint != "/api/v1/traces/stream/replay-session" or not session_id:
logger.warning("Replay request non supporté: %s", replay_request)
self._notifier.notify(
"Léa",
"Je ne peux pas lancer ce test automatique pour le moment.",
)
return
def _replay():
if self.server_client is None:
return
with self._state_lock:
self._replay_active = True
self._update_icon()
self._notifier.notify(
"Léa",
f"Le système d'intelligence artificielle exécute la "
f"tâche '{replay_name}' sur votre écran.",
)
try:
import requests
auth_headers = {}
if self.server_client is not None:
auth_headers = self.server_client._auth_headers()
resp = requests.post(
f"{self.server_client._stream_base}{endpoint}",
params={
"session_id": session_id,
"machine_id": machine_id,
},
headers=auth_headers,
timeout=30,
allow_redirects=False,
)
if resp.ok:
logger.info(
"Replay direct démarré pour session %s (machine=%s)",
session_id,
machine_id,
)
else:
self._notifier.notify(
"Léa",
"Hmm, le serveur a refusé le test immédiat.",
)
except Exception as e:
logger.error("Erreur lancement replay direct : %s", e)
self._notifier.notify(
"Léa",
f"Oups, un problème : {e}",
)
finally:
with self._state_lock:
self._replay_active = False
self._update_icon()
threading.Thread(target=_replay, daemon=True).start()
def offer_finalize_replay(
self,
replay_request: Dict[str, Any],
replay_name: str,
) -> None:
"""Proposer à l'utilisateur de tester immédiatement la tâche apprise."""
if not replay_request or not replay_request.get("session_id"):
return
def _offer():
self._notifier.notify(
"Léa",
f"J'ai compris la tâche '{replay_name}'. Voulez-vous la tester ?",
)
if not _ask_consent(
"Léa — Test immédiat",
f"J'ai compris la tâche '{replay_name}'. "
"Voulez-vous la tester maintenant ?",
):
return
self._launch_replay_request(replay_request, replay_name)
threading.Thread(target=_offer, daemon=True).start()
def _on_emergency_stop(self, _icon=None, _item=None) -> None:
"""Arret d'urgence — stoppe TOUTES les activites de l'agent immediatement.