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

@@ -338,6 +338,50 @@ class LeaServerClient:
except Exception:
return None
def resume_replay(self, replay_id: str) -> bool:
"""Reprendre un replay en pause supervisée via HTTP direct.
Fallback du chemin SocketIO (`lea:replay_resume` → agent_chat)
utilisé quand le bus feedback est déconnecté au moment où
l'utilisateur clique « Continuer » dans la bulle paused.
Retourne True si le serveur streaming a accepté la reprise.
"""
if not replay_id:
return False
try:
import requests
resp = requests.post(
f"{self._stream_url}/traces/stream/replay/{replay_id}/resume",
headers=self._auth_headers(),
timeout=10,
)
return bool(resp.ok)
except Exception:
logger.debug("resume_replay HTTP silenced", exc_info=True)
return False
def abort_replay(self, replay_id: str) -> bool:
"""Annuler un replay en pause supervisée via HTTP direct.
Symétrique de ``resume_replay`` : fallback du chemin SocketIO
(`lea:replay_abort`) quand le bus feedback est déconnecté.
POSTe sur ``/replay/{id}/cancel`` côté serveur streaming.
"""
if not replay_id:
return False
try:
import requests
resp = requests.post(
f"{self._stream_url}/traces/stream/replay/{replay_id}/cancel",
headers=self._auth_headers(),
timeout=10,
)
return bool(resp.ok)
except Exception:
logger.debug("abort_replay HTTP silenced", exc_info=True)
return False
def report_action_result(
self,
session_id: str,