Files
rpa_vision_v3/docs/CR_AUDIT_PAUSED_RESUME_BUS_2026-05-22.md

10 KiB
Raw Blame History

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 :

  1. ChatWindow._on_paused_resume(replay_id) (agent_v0/agent_v1/ui/chat_window.py:1016) teste self._bus is not None and self._bus.connected.
  2. Si vrai → self._bus.resume_replay(replay_id)FeedbackBusClient._safe_emit("lea:replay_resume", …) (agent_v0/agent_v1/network/feedback_bus.py:135).
  3. _safe_emit re-vérifie self._sio.connected, sinon retourne False (feedback_bus.py:141-149).
  4. Côté serveur, c'est agent_chat (port 5004, SocketIO) qui relaie en HTTP POST /api/v1/traces/stream/replay/{id}/resume vers 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_help jusqu'à 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, retourne resp.ok.
  • abort_replay(replay_id) -> bool : POST /traces/stream/replay/{id}/cancel, retourne resp.ok.
  • Toutes deux : guard replay_id vide, lazy import requests, 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_resume et _on_paused_abort utilisent 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.connected est True mais le socket est en fait mort (cas rare), le bus émettra et retournera True au niveau client — le serveur ne recevra rien et le replay restera figé. Mitigation indirecte : _safe_emit re-vérifie connected juste avant le emit, et le pattern try/except attrape les erreurs réelles. Pas de fix supplémentaire, hors scope.
  • Endpoint /cancel côté serveur : utilisé par abort_replay. Hypothèse : il fonctionne comme attendu (idempotent, accepte un replay déjà annulé). Référence api_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._bus reste None. Le patch couvre ce cas : HTTP est appelé direct. À garder à l'esprit pour la doc de déploiement Windows.
  • server_client non câblé après instanciation de ChatWindow : update_server_client() existe (chat_window.py:255), donc le wiring tardif est OK. Si server_client reste None pendant 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 :

  1. _generate_setup_actions produit bien les actions Notepad (act_setup_sess_click_start, click_search, click_result).
  2. Après _trim_redundant_setup_events, aucun event mouse_click ne porte un window.title contenant l'URL http192.168.1.40 (le clic redondant a été coupé).
  3. Après build_replay_from_raw_events, la première action utile est type "test" — pas un click by_text="Sans titre" que _infer_tab_switch_target aurait 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).