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:
@@ -17,6 +17,20 @@ from typing import Any, Dict, List, Optional
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _infer_machine_id_from_session_id(session_id: str, fallback: str = "default") -> str:
|
||||
"""Déduire le machine_id depuis un session_id spécial si possible.
|
||||
|
||||
Les heartbeats de fond de Léa utilisent `bg_<machine_id>` comme
|
||||
identifiant de session. Lors d'un redémarrage serveur, ces sessions
|
||||
peuvent être restaurées depuis la persistance JSON avec `machine_id`
|
||||
resté à `default`. On rétablit ici l'information machine pour que les
|
||||
replays ciblés retrouvent bien la session de fond active.
|
||||
"""
|
||||
if session_id.startswith("bg_") and len(session_id) > 3:
|
||||
return session_id[3:]
|
||||
return fallback
|
||||
|
||||
|
||||
@dataclass
|
||||
class LiveSessionState:
|
||||
"""État d'une session active en mémoire."""
|
||||
@@ -86,11 +100,18 @@ class LiveSessionManager:
|
||||
def _load_persisted_sessions(self):
|
||||
"""Charger les sessions sauvegardées au démarrage (JSON state files)."""
|
||||
count = 0
|
||||
for session_file in sorted(self._persist_dir.glob("sess_*.json")):
|
||||
session_files = sorted(self._persist_dir.glob("sess_*.json"))
|
||||
session_files += sorted(self._persist_dir.glob("bg_*.json"))
|
||||
for session_file in session_files:
|
||||
try:
|
||||
with open(session_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
session = LiveSessionState.from_dict(data)
|
||||
if session.machine_id == "default":
|
||||
session.machine_id = _infer_machine_id_from_session_id(
|
||||
session.session_id,
|
||||
fallback=session.machine_id,
|
||||
)
|
||||
self._sessions[session.session_id] = session
|
||||
count += 1
|
||||
except Exception as e:
|
||||
@@ -117,7 +138,7 @@ class LiveSessionManager:
|
||||
for jsonl_file in sorted(live_dir.glob("**/live_events.jsonl")):
|
||||
session_dir = jsonl_file.parent
|
||||
session_id = session_dir.name
|
||||
if not session_id.startswith("sess_"):
|
||||
if not (session_id.startswith("sess_") or session_id.startswith("bg_")):
|
||||
continue
|
||||
if session_id in self._sessions:
|
||||
continue
|
||||
@@ -125,7 +146,7 @@ class LiveSessionManager:
|
||||
# Déduire le machine_id depuis le chemin parent
|
||||
parent_name = session_dir.parent.name
|
||||
if parent_name == live_dir.name:
|
||||
machine_id = "default"
|
||||
machine_id = _infer_machine_id_from_session_id(session_id)
|
||||
else:
|
||||
machine_id = parent_name
|
||||
|
||||
|
||||
Reference in New Issue
Block a user