# 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 ` - En cas de succès : feedback « → Reprise demandée… » avec mention du canal dans le log (`replay_resume émis pour 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 ```bash 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`).