10 KiB
CR — Audit paused_bubble: bus déconnecté, resume non émis + fallback HTTP
Date : 2026-05-22
Branche : backup/post-demo-2026-05-19
Périmètre : agent-side uniquement (agent_v0/agent_v1/** + agent_v0/lea_ui/**). agent_v0/server_v1/replay_engine.py non touché.
Statut : patch + tests implémentés et verts (19 tests neufs + 1 test intégration trim).
1. Cause exacte la plus probable
Le bouton Continuer de la bulle paused suit un chemin unique, sans fallback :
ChatWindow._on_paused_resume(replay_id)(agent_v0/agent_v1/ui/chat_window.py:1016) testeself._bus is not None and self._bus.connected.- Si vrai →
self._bus.resume_replay(replay_id)→FeedbackBusClient._safe_emit("lea:replay_resume", …)(agent_v0/agent_v1/network/feedback_bus.py:135). _safe_emitre-vérifieself._sio.connected, sinon retourneFalse(feedback_bus.py:141-149).- Côté serveur, c'est
agent_chat(port 5004, SocketIO) qui relaie en HTTPPOST /api/v1/traces/stream/replay/{id}/resumevers le serveur streaming (port 5005).
Le bug : si le bus SocketIO est tombé (network blip, agent_chat redémarré, LEA_FEEDBACK_BUS=0, ou socket cassé entre connect() et le emit), le clic est perdu :
- log
paused_bubble: bus déconnecté, resume non émis(chat_window.py:1036) - boutons disabled (
_disable_paused_buttons) - fenêtre minimisée 500 ms plus tard (
self._root.after(500, self._do_hide)) - UX affiche « ⚠ Bus indisponible — réessayez dans 5 s » mais l'utilisateur ne peut pas réessayer (boutons figés + fenêtre cachée)
- côté serveur : le replay reste
paused_need_helpjusqu'à expiration / cancel manuel
L'endpoint HTTP qui ferait le job existe pourtant déjà côté serveur (api_stream.py:4333 POST /replay/{id}/resume et :4443 /cancel) — il n'est juste pas appelé directement par l'agent quand le bus est down.
Confiance haute : la chaîne du chemin nominal et le défaut de fallback ont été tracés ligne par ligne ; le log exact correspond bien à ce branchement.
2. Fichiers / fonctions concernés
| Fichier | Fonctions clés |
|---|---|
agent_v0/agent_v1/ui/chat_window.py |
_on_paused_resume:1016, _on_paused_abort:1044, _disable_paused_buttons:1071 |
agent_v0/agent_v1/network/feedback_bus.py |
FeedbackBusClient.resume_replay:130, abort_replay:137, _safe_emit:141, connected:122 |
agent_v0/agent_v1/main.py |
wiring chat_window._bus (start/stop dans _start_chat, fenêtre start_session) |
agent_v0/lea_ui/server_client.py |
_auth_headers:114, _stream_url, base requests existante (resume/abort absents avant ce patch) |
agent_v0/server_v1/api_stream.py (référence, non modifié) |
/replay/{id}/resume:4333, /replay/{id}/cancel:4443 |
3. Patch minimal recommandé (implémenté)
Choix : ajouter un fallback HTTP direct côté agent vers /replay/{id}/resume et /replay/{id}/cancel, déclenché quand le bus SocketIO est down ou que l'emit échoue. En cas d'échec sur les deux canaux, ne PAS désactiver les boutons et ne PAS auto-hide la fenêtre → l'utilisateur peut réessayer.
Pas de queue persistante, pas de retry automatique : minimum viable, déterministe, traçable dans les logs (channel=bus vs channel=http vs aucun).
Changements de code
agent_v0/lea_ui/server_client.py — ajout de deux méthodes HTTP symétriques au flux SocketIO :
resume_replay(replay_id) -> bool: POST/traces/stream/replay/{id}/resume, retourneresp.ok.abort_replay(replay_id) -> bool: POST/traces/stream/replay/{id}/cancel, retourneresp.ok.- Toutes deux : guard
replay_idvide, lazy importrequests, try/except → False sur exception,_auth_headers()pour le Bearer.
agent_v0/agent_v1/ui/chat_window.py — refactor de la décision d'envoi :
- Nouveau helper
_dispatch_paused_action(replay_id, bus_method, client_method) -> (emitted, channel)qui essaie bus puis HTTP fallback. Retourne le canal utilisé pour le log ("bus"/"http"/""). _on_paused_resumeet_on_paused_abortutilisent ce helper. En cas d'échec sur les deux canaux :- feedback UI : « ⚠ Serveur injoignable — réessayez »
_enable_paused_buttons()(nouveau) réactive les deux boutons- pas de
_root.after(500, self._do_hide)(pas d'auto-hide) - log warning
paused_bubble: bus et HTTP indisponibles, resume non émis pour <id>
- En cas de succès : feedback « → Reprise demandée… » avec mention du canal dans le log (
replay_resume émis pour <id> via bus|http).
Aucun changement de signature publique ; aucun touchage côté agent_v0/server_v1/.
4. Tests ajoutés
| Fichier | Tests | Bilan |
|---|---|---|
tests/unit/test_server_client_replay_controls.py |
10 tests (resume_replay × 5 + abort_replay × 5) : succès, échec serveur, replay_id vide, exception réseau, URL & header auth |
✅ 10/10 |
tests/unit/test_chat_window_paused_dispatch.py |
9 tests sur _dispatch_paused_action en isolation Tkinter (bus OK, bus down, bus emit False, bus raise, no bus, all-fail, no-client, méthode absente, abort symétrique) |
✅ 9/9 |
tests/integration/test_replay_session_trim_neutral.py |
1 test bout-en-bout _extract_required_apps → _generate_setup_actions → _trim_redundant_setup_events → build_replay_from_raw_events sur fixture reproduisant sess_20260520T102916_066851 — vérifie que la première action utile post-setup est type 'test', pas un click by_text="Sans titre" |
✅ 1/1 |
Total 20 tests neufs, 79 tests verts sur le périmètre (les 59 existants test_env_setup.py n'ont pas régressé).
Commandes de validation
cd /home/dom/ai/rpa_vision_v3
source .venv/bin/activate
set -a && source .env.local && set +a
python -m pytest tests/unit/test_server_client_replay_controls.py -v
python -m pytest tests/unit/test_chat_window_paused_dispatch.py -v
python -m pytest tests/integration/test_replay_session_trim_neutral.py -v
python -m pytest tests/unit/test_env_setup.py tests/unit/test_server_client_replay_controls.py tests/unit/test_chat_window_paused_dispatch.py tests/integration/test_replay_session_trim_neutral.py
5. Fichiers modifiés
| Fichier | Nature | SCP Windows requis |
|---|---|---|
agent_v0/lea_ui/server_client.py |
Ajout resume_replay + abort_replay (~45 lignes) |
Oui → dom@192.168.1.11:C:/rpa_vision/lea_ui/server_client.py |
agent_v0/agent_v1/ui/chat_window.py |
Refactor _on_paused_resume, _on_paused_abort ; ajout _dispatch_paused_action, _enable_paused_buttons (~110 lignes touchées) |
Oui → dom@192.168.1.11:C:/rpa_vision/agent_v1/ui/chat_window.py |
tests/unit/test_server_client_replay_controls.py |
NEW (109 lignes) | Non |
tests/unit/test_chat_window_paused_dispatch.py |
NEW (115 lignes) | Non |
tests/integration/test_replay_session_trim_neutral.py |
NEW (130 lignes) | Non |
⚠️ Le miroir agent_v0/deploy/windows_client/lea_ui/server_client.py est obsolète (setup initial, pas l'incrémental — cf. handoff 2026-05-20). Le canal réel reste le SCP manuel direct vers C:/rpa_vision/.
6. Risques et limites
- Pas de queue persistante : si l'utilisateur clique Continuer pendant un blackout réseau total (bus + HTTP indisponibles), le clic n'est pas mis en attente. Le patch garantit juste qu'il pourra réessayer (boutons restent actifs, pas d'auto-hide). Une vraie queue serait une refacto, hors scope « minimal ».
- Pas d'invalidation du bus : si l'attribut
self._sio.connectedestTruemais le socket est en fait mort (cas rare), le bus émettra et retourneraTrueau niveau client — le serveur ne recevra rien et le replay restera figé. Mitigation indirecte :_safe_emitre-vérifieconnectedjuste avant leemit, et le pattern try/except attrape les erreurs réelles. Pas de fix supplémentaire, hors scope. - Endpoint
/cancelcôté serveur : utilisé parabort_replay. Hypothèse : il fonctionne comme attendu (idempotent, accepte un replay déjà annulé). Référenceapi_stream.py:4443— pas re-vérifié dans cet audit. LEA_FEEDBACK_BUS=0: si le flag d'env désactive le bus côté Windows,self._busresteNone. Le patch couvre ce cas : HTTP est appelé direct. À garder à l'esprit pour la doc de déploiement Windows.server_clientnon câblé après instanciation de ChatWindow :update_server_client()existe (chat_window.py:255), donc le wiring tardif est OK. Siserver_clientresteNonependant le clic, le patch tombe en(False, "")proprement.
7. Bonus — test d'intégration de non-régression pour le trim
Le test demandé en second choix est livré dans tests/integration/test_replay_session_trim_neutral.py. Il exécute la chaîne complète replay-session côté serveur sur une fixture synthétique reproduisant le pattern de sess_20260520T102916_066851 :
- focus initial Notepad sur un titre non-neutre (
http…txt – Bloc-notes) - clic intra-Notepad à rel_y ≈ 40 sur la barre d'onglets
- focus_change vers
Sans titre – Bloc-notes(titre neutre = état setup auto) - saisie
test
Le test vérifie trois invariants stricts :
_generate_setup_actionsproduit bien les actions Notepad (act_setup_sess_click_start,click_search,click_result).- Après
_trim_redundant_setup_events, aucun event mouse_click ne porte unwindow.titlecontenant l'URLhttp192.168.1.40(le clic redondant a été coupé). - Après
build_replay_from_raw_events, la première action utile esttype "test"— pas un clickby_text="Sans titre"que_infer_tab_switch_targetaurait pu produire si le clic redondant avait survécu au trim.
Si la régression du bug du 20 mai revient (par exemple un revert silencieux du patch _NEUTRAL_TITLE_TOKENS), ce test échoue immédiatement avec un message clair.
8. Synthèse pour décision
- Cause : pas de fallback HTTP, UI bloque l'utilisateur dès qu'un emit SocketIO échoue → replay paused figé.
- Patch :
ServerClient.resume_replay/abort_replay(HTTP direct) +ChatWindow._dispatch_paused_action(bus → HTTP) + ré-activation boutons + skip auto-hide sur échec total. - Scope : 2 fichiers prod (≤ 160 lignes touchées), 3 fichiers test (354 lignes). Pas de refacto.
- Validation : 79/79 tests verts. À valider en condition réelle : kill agent_chat (port 5004) pendant un replay paused, cliquer Continuer → côté Léa log
replay_resume émis pour … via http+ replay redémarre. - SCP requis : 2 fichiers vers Windows avant relance Léa (
lea_ui/server_client.py,agent_v1/ui/chat_window.py).