Bug observé sur replay_sess_56c10222 (2026-05-24 20:14) :
action 11 (clic 'Enregistrer' expected_after='Enregistrer sous')
marquée success=True alors que 2 actions plus tard la fenêtre observée
est 'NoMachine Desktop Viewer'. Le polling post-vérif a probablement
matché brièvement 'Enregistrer sous' puis l'écran a changé sans
qu'on ne revérifie.
Dom : "Le contrat est rompu : Léa passe d'une action à l'autre sans
vérifier que la précédente est bonne. Il faut un contrôle de résultat,
si on ne sait pas on demande."
Patch : juste après le match initial, attendre 0.5s et reverifier
la fenêtre active. Si elle a divergé (race condition, dialog auto-
fermée, focus change OS) → matched=False, le flow strict existant
prend le relais avec wrong_window + needs_human.
Ne touche que les cas où expected_after est défini ET pas de
runtime_dialog géré entre temps (le runtime_dialog est légitime de
changer la fenêtre).
Tag rollback : rollback/pre-P0.9-2026-05-24_2148
Léa avait déjà une infra pour les dialogs runtime (`_match_known_runtime_dialog`
+ `_handle_known_runtime_dialog`) mais avec un catalog local limité à
2 entrées. Le DialogResolver R2 côté serveur a 10 entrées centralisées.
P1.MVP : `_try_dialog_resolver_server()` consulte l'endpoint
`/api/v1/dialog/resolve` quand le catalog local n'a pas matché. La
réponse `DialogResolution` est convertie en dialog_spec compatible
avec `_handle_known_runtime_dialog` qui réutilise la cascade existante
(serveur VLM grounding + template matching local).
- Flag `RPA_DIALOG_RESOLVER_AGENT_ENABLED` (OFF par défaut) — rollback runtime
- Auth Bearer via `_auth_headers()` existant
- Timeout 3s, fail-safe sur exception/503/no-match → fallback humain intact
- Zéro régression sur les chemins existants (le catalog local reste 1ère ligne)
Tests unitaires en local (6/6 OK) :
- flag OFF → None
- serveur 503 → None
- matched=False → None
- policy=pause (UAC) → None
- match auto + click_button → dialog_spec valide
- exception réseau → None
Tag rollback : rollback/pre-P1-2026-05-24_2105
Friction UX remontée par Dom sur replay live (replay_sess_63a1313b) :
latence excessive 2-3 minutes après un échec d'action avant que Léa
ne reprenne la main. 120s = trop long pour un humain en supervision.
10s d'inactivité reste le critère prioritaire (déjà en place), donc :
- humain actif : la correction est captée et le replay reprend en ~1s
- humain absent : on libère après 30s au lieu de 120s
5 sites d'appel + signature de fonction (default param) alignés.
Tag rollback : rollback/pre-P0.8-2026-05-24_1912
Référence : message 2026-05-24_1910_claude-to-codex_p07-memory-sanity-fix-human-supervised-bug-frictions-ux.md
Bug observé sur replay_sess_63a1313b 2026-05-24 18:31-18:32 :
_capture_human_correction() côté Léa retourne des human_actions sans
clic humain réel (cause racine côté agent à investiguer = P0.6).
En cascade, memory_record_success était appelé avec coords (0.0, 0.0)
et stockait des entrées poison dans target_memory.db.
Le sanity check existant rejetait < 0 ou > 1 mais laissait passer (0,0)
qui est mathématiquement valide. Au prochain replay, memory_lookup
trouvait l'entrée poison et faisait cliquer Léa au coin haut-gauche.
Patch : rejet explicite de (0,0) + warning au lieu de debug pour les
coords hors [0,1] (besoin de tracabilité runtime).
Filet en aval — la vraie cause côté Léa reste à corriger (P0.6).
Tag rollback : rollback/pre-P0.7-2026-05-24_1850
Symptôme observé sur replay_sess_7a4c8e72 (24/05 17:57) :
- Action act_setup_sess_verify (type=verify_screen) échoue 4x (+3 retries)
- Logs: [VALIDATOR_V2] override success→False verdict=continue conf=0.30
failure_category=None reason='Aucun changement visible pour
verify_screen (normal pour ce type d'action)'
- Replay tombe en status=error à 7/15 (régression vs 12/15 sans V2)
Cause: api_stream.py:3674 testait `if verdict != COMPLETE` (trop large) →
toute action qui ne change pas drastiquement l'écran (verify_screen, wait,
key_combo Ctrl+S avant ouverture dialog, etc.) renvoie verdict=CONTINUE
conf=0.30 du PixelDiffChecker via le default_checker de l'orchestrator,
ce qui était traité comme un échec à overrider.
Fix: override SEULEMENT sur verdict=TERMINATE (échec certain avec
failure_category). CONTINUE = faible signal = on laisse le pipeline
historique trancher.
COMPLETE n'a pas besoin d'être traité ici car on est déjà dans
`if report.success:` (success initial vrai).
Effet:
- verify_screen/wait/key_combo non-interactif → orchestrator retourne
CONTINUE conf=0.30 → V2 ne touche pas report.success (comportement
legacy préservé)
- click qui rate (act_raw_6c1432b3 type cible) → OcrRoiChecker retourne
TERMINATE conf=0.85 failure_category=WRONG_APPLICATION → override OK
Tests R1 inchangés (TERMINATE branch testée explicitement).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symptôme observé replay_sess_4c38dbb8 (24/05) :
- 0/15 actions avec expected_result rempli
- Conséquence : api_stream.py:3630 verify_with_critic() jamais appelé
(conditionné à action.expected_result non vide)
- Donc Critic sémantique (Ollama) désarmé en production, seul le
pixel-diff tournait
Causes racines identifiées :
1. _GEMMA4_PORT=11435 hardcodé (legacy Docker dédié supprimé) →
check /api/tags timeout silencieux → fonction sort early
2. _CRITIC_MODEL="gemma4:e4b" hardcodé → modèle non installé
3. "think": True dans le payload → "qwen2.5vl:7b-rpa" does not
support thinking → 400 sur tous les appels → if not resp.ok: continue
4. Prompt sans few-shot → qwen2.5vl converse au lieu de respecter
le format strict INTENTION/AVANT/APRES → parsing vide
Fix (stream_processor.py) :
- _GEMMA4_PORT default 11435 → 11434 (Ollama native)
- _CRITIC_MODEL = os.environ.get("RPA_CRITIC_MODEL", "qwen2.5vl:7b-rpa")
- Remplacement de 3 "gemma4:e4b" hardcodés → _CRITIC_MODEL
- _unload_gemma4() → no-op (legacy Docker n'existe plus)
- Prompt enrichissement : ajout exemple few-shot (Cliquer Enregistrer)
- "think": True → False (qwen2.5vl ne supporte pas)
Config .env.local :
- RPA_VLM_MODEL=qwen2.5vl:7b → qwen2.5vl:7b-rpa (variant num_ctx=8192,
créé via Modelfile pour permettre offload partiel GPU sur RTX 5070
12 GB ; sans ça, num_ctx=128k par défaut = 12.5 GB requis = OOM full
CPU fallback observé 17:11 le 24/05)
Validation :
- Avant fix : 0/8 actions enrichies (110 ms total = appels échoués
immédiatement avec 400)
- Après fix : 5/8 actions enrichies en 35s (~7s/action, cohérent avec
appels VLM réels qwen2.5vl)
Side effects systemd (à committer séparément côté infra) :
- OLLAMA_KEEP_ALIVE: 5m → 24h
- t2a-viewer.service stopped + disabled (libère ~2.9 GB VRAM)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symptome (3 incidents 24h les 24/05) : apres relance distante de Lea via SSH,
les polls /replay/next repartent un moment puis s'arretent. Diagnostic :
- agent_v1/ui/smart_tray.py:875 utilise pystray.Icon.run() comme boucle principale
- main.py:132-133 lance _replay_poll_loop et _background_heartbeat_loop en
daemon threads
- Quand Lea est lancee via sshpass sans session interactive Windows, pystray
echoue (pas de systray accessible) et icon.run() sort
- agent.run() retourne, main() retourne, main thread termine
- Les daemon threads meurent avec le main thread (par design Python)
Fix : _headless_keepalive() maintient le main thread vivant via threading.Event
quand agent.run() sort en laissant agent.running=True (cas anormal). Handlers
SIGTERM/SIGINT/SIGBREAK pour shutdown propre.
Invisible en mode interactif normal (icon.run() ne sort jamais).
Pas de modification de smart_tray ni de la cascade visuelle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backup état complet après enregistrement vidéo démo de bout en bout.
À utiliser comme point de référence pour la consolidation post-démo.
Changements majeurs de la session 18-19 mai :
- AIVA-URGENCE : page autonome avec preset URL + auto-focus chain
- Workflow Demo_urgence_3_db : merge linux_db + steps AIVA + pause humaine NoMachine
- Bypass LLM (static_result / static_text) dans replay_engine
pour démos déterministes sans appel Ollama
- Fix api_stream:3013 — replay_paused au premier polling /next
- dag_execute : lift duration_ms vers top-level pour wait runtime
- NPM bypass auth /aiva-urgence/ via location ^~ (proxy_host/10.conf hors git)
- scripts/cancel-replays.sh — workaround Stop VWB qui ne purge pas la queue
Anchors visuels (468) forcés dans le commit pour garantir restorabilité.
DB workflows actuelle + ~12 .bak DB de la journée incluses.
Sujets identifiés pour consolidation post-démo (TODO) :
1. Bug VWB recapture anchor ne régénère pas le PNG
2. Léa client accumule état mémoire (restart périodique requis)
3. Stop VWB ne purge pas la queue serveur (lien manquant vers /replay/cancel)
4. Bug coord client mss tronqué 2560x60 → mapping Y cassé
5. delay_before/delay_after ignorés au runtime (fix partiel duration_ms)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Préprocesseur Python qui injecte un bloc FAITS_CALCULÉS en tête du DPI
avant l'appel LLM, pour neutraliser l'hallucination de durée (bug "23h"
sur cas MOREL, confusion avec "depuis 23h" de l'Observ. IDE Urg).
Extrait depuis le bandeau Easily Assure et la Synthèse Urgences :
- âge (dateutil.relativedelta)
- date admission / sortie + durée passage (format humain + décimal)
- CCMU / GEMSA libellé complet (parser multi-ligne)
- priorité IAO, mode de venue / médicalisation / mode d'entrée
- diagnostic principal
- decision_terrain + orientation_terrain (metadata only, jamais injectés
dans le prompt pour ne pas biaiser le LLM)
Retour tuple (dpi_enriched, metadata) pour permettre les garde-fous
serveur Python ↔ LLM au commit 2.
Robustesse :
- re.search 1re occurrence + WARNING si bandeau divergent multi-occurrences
- Synthèse Urgences priorité sur bandeau pour dates
- Valeur exigée sur même ligne que label (évite capture de section title)
- Cas négatif (horaires absents) → "NON CALCULABLE" + parsing_warnings
- Jamais de crash, retour tuple toujours valide
Tests : 4/4 verts (golden MOREL string + metadata, négatif sortie absente,
DPI vide). Pas de régression sur tests/integration/test_t2a_extract.py.
Brief complet : docs/handoffs/2026-05-12_brief_S1_build_dpi_enriched.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Avant : 4 occurrences de parsing en cascade dans resolve_engine.py
(L840-885, L903-915, L2569-2580, ~110 lignes au total).
Après : centralisation dans core/grounding/bbox_parser.py avec
paramètre formats= permettant de filtrer les formats reconnus
selon le contrat sémantique de chaque site d'appel.
Préservation des contrats sémantiques (strict no-op) :
- Occ 1+2 (cascade principale) : tous formats (par défaut)
- Occ 3 (retry multi-image) : formats={"xy_json", "raw_array"}
pour respecter le prompt qui impose {"x": NNN, "y": NNN} in pixels
- Occ 4 (_locate_popup_button) : formats={"bbox_2d"} pour respecter
le prompt qui demande "bounding box"
Notes :
- Mini-bug Occ 3 retry multi-image (division systématique sans
heuristique x>1, produisait coordonnées aberrantes ~0.0004 si
VLM retournait déjà du pourcentage) corrigé incidemment via
centralisation. Pas de régression possible (résultat précédent
aberrant par construction).
- Occ 4 : bbox_2d strict 4-coords élargi à bbox_2d 2 ou 4 coords.
Contrat sémantique "bounding box" respecté ; un point 2-coords
interprété comme centre de bbox.
Tests : 26 cas dans test_bbox_parser.py (tous formats × cascade
+ filtre formats= + validated). 121 PASS / 0 FAIL sur le périmètre
refactor (5 fichiers ciblés).
Net : -96 lignes dans resolve_engine.py, +120 lignes module
+ 250 lignes tests.
refs DETTE-006 (étape 2/5 du fix smart_resize)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Avant : succès silencieux (seul rejet loggé)
Après : log INFO à chaque appel avec by_text, position, méthode,
observed, is_valid, latence
Permet de valider en runtime que le pré-check OCR tourne bien
sur les résolutions resolved=True (cf commit 731b5bcae).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diagnostic post-bench E2E (rapport docs/E2E_TEST_RUN_2026-05-08.md) :
1. BUG SILENCIEUX MAJEUR (api_stream.py:4549) — quand le pré-check OCR
rejette, mon code de rejet hier soir met x_pct=None / y_pct=None.
Le log structuré faisait result.get('x_pct', 0):.4f → None:.4f →
TypeError → réponse "analysis_error" qui MASQUE le vrai motif
"rejected_text_mismatch". Conséquence : pendant toute la session
du 7 mai soir, les rejets pré-check ont été silencieusement
transformés en erreurs analyse → cascade locale Léa V1 → clic au pif.
Fix : `(result.get('x_pct') or 0):.4f` traite None | None | 0
uniformément.
2. FLAG ENV pré-check OFF par défaut — le pré-check
_validate_text_at_position introduit hier soir a 2 défauts
identifiés par le bench E2E sur 8 click_anchor :
* radius_px=200 trop petit pour les tabs à 2 tokens (Examens
cliniques, Synthèse Urgences) — OCR voit un crop tronqué
"Maquette POC ler en cours Codage Statistiques" qui n'inclut
pas "Examens" → fuzzy match 1/2 = 50% < seuil 0.60 → REJET.
À radius 300/400 le mot est inclus → match passe.
* min_token_ratio=0.60 trop strict pour cibles 2 tokens.
Solution démo : flag env RPA_ENABLE_TEXT_PRECHECK (défaut "false").
Le pré-check est désactivé par défaut → retour au comportement
stable d'avant-hier (hybrid_text_direct ≥ 0.80 utilisé direct,
exemption drift préservée). Code et fonction _validate_text_at_position
conservés en place pour reprise post-démo après calibrage radius
adaptatif (≈ 0.17 × min(screen_w, screen_h)) et token_ratio descendu
à 0.50.
Pour ré-activer en dev/test : `RPA_ENABLE_TEXT_PRECHECK=true`
dans .env.local ou env du service rpa-streaming.
Inclus aussi :
- docs/E2E_TEST_RUN_2026-05-08.md (rapport agent test E2E ~1700 mots)
- tests/e2e/urgence_aiva_demo_expected.yaml (tolérances re-écrites)
- tests/e2e/fixtures/urgence_aiva_demo/live/*.png (8 fixtures
recapturées headless 1920x1080 pour itérer demain)
- _ocr_inventory.json + _run_resolve_results.json (raw runs)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Démo GHT 8 mai 2026 — Dom utilise UNIQUEMENT Léa V1 sur Windows pendant
la démo (pas le frontend VWB Linux), donc les pause_message du serveur
doivent être visuellement évidents sur l'écran Windows. Modifications
client validées par Dom + redéployées via SCP (procédure 2026-04-28).
1. ui/paused_toast.py (NEW) — Toast Tkinter custom autonome :
Toplevel topmost overrideredirect, fond bleu Léa (#2563EB), 380px,
haut-droite, auto-close 15s, click-to-close. Re-pin -topmost à
100/500/2000 ms (Windows démet le flag quand le focus part). Rate
limit 3s sur message identique. Aucune dépendance externe (tkinter
stdlib uniquement). Thread-safe : root.after si Tk root existe,
sinon Tk dédié dans un daemon thread. Remplace plyer qui s'avère
silencieux sur Windows 11 (Focus Assist + manque app-id COM).
2. ui/chat_window.py — _add_paused_bubble force la visibilité :
La fenêtre Léa démarrait avec root.withdraw() — la bulle paused
était bien rendue mais invisible. Ajout deiconify+lift+focus_force
avant render, plus appel à show_paused_toast en complément.
3. ui/notifications.py — niveau BLOCAGE déclenche aussi le toast :
Quand notify_message reçoit un MessageUtilisateur.BLOCAGE (cible
non trouvée, mode apprentissage, fenêtre incorrecte), appelle
show_paused_toast en plus de plyer. Couvre la branche supervision
client (executor.py:1012) qui ne passe pas par Plan B serveur.
4. core/executor.py — Plan B replay_paused (lignes 1812-1850) :
Intercepte data["replay_paused"]=True dans la réponse /replay/next,
appelle chat_window._add_paused_bubble si _chat_window_ref défini,
sinon fallback notifier.notify. Idempotence via _last_pause_msg_shown
pour ne pas spammer (1 toast par (replay_id, message) unique).
Threshold FIND-TEXT _find_text_on_screen : 0.50 → 0.75 pour rejeter
les faux positifs (placeholders italiques, tabs voisins) et tomber
en mode apprentissage humain plutôt qu'un clic au pif.
5. main.py — Wiring ChatWindow → Executor pour Plan B.
6. tools/test_lea_toast.py + ui/_test_paused_toast.py (NEW) — Scripts
de test isolé pour validation visuelle rapide sans relancer un
replay complet (commande dans les docstrings).
Validé visuellement sur DESKTOP-58D5CAC. Toasts apparaissent en haut-
droite, fond bleu, auto-close 15s. Test isolé Dom : 3 toasts successifs
visibles sans accroc.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trois changements complémentaires dans la cascade de résolution serveur,
finis ce soir 7 mai pour la démo GHT 8 mai. Restaure le comportement strict
d'avril 2026 (workflow qui passait 20 fois d'affilée sans incident).
1. resolve_engine.py — _validate_resolution_quality (lignes 2255-2289) :
Le commit b584bbabc du 1er mai 2026 ("fix(stream): démo UHCD") avait
transformé le rejet strict (resolved=False, method="rejected_drift_*")
en fallback aveugle (resolved=True, method="fallback_recorded_coords",
coords du record). Symptôme observé : Léa cliquait sur "Dossier en
cours" du menu au lieu de "Synthèse Urgences" du tab — le VLM Quick
Find Ollama hallucinait à (0.526, 0.918), drift dépassé, fallback
ratait. Restauré : resolved=False explicite, le client passe en
pause supervisée comme prévu (philosophie échec = apprentissage).
2. resolve_engine.py — exemption high-confidence élargie :
L'exemption drift>0.20 IGNORÉ ne couvrait que template_matching ≥ 0.95
(commit 35b27ae49 du 2 mai). Étendue à hybrid_text_direct ≥ 0.80 :
un OCR direct qui trouve le texte cible exact à score 0.80+ est aussi
sûr qu'un template à 0.95 — la position est sémantiquement vraie,
le drift reflète juste un changement de layout (résolution écran,
refonte UI, scroll), pas une erreur de résolution.
3. resolve_engine.py + api_stream.py — pré-check OCR sémantique :
Nouvelle fonction _validate_text_at_position (singleton EasyOCR fr+en,
crop 200px autour de la coord résolue, fuzzy match 60% des tokens
≥3 caractères de l'expected_text). Câblée dans api_stream.py juste
après _validate_resolution_quality. Si le by_text attendu n'est PAS
présent dans la zone autour de la coord résolue → resolved=False
method="rejected_text_mismatch" → pause supervisée.
Pattern Verification-Aware Planning (state of the art 2026 — voir
recommandations agent archéologue + agent SOTA review) : le serveur
ne renvoie une coord que s'il est sémantiquement sûr du résultat.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Deux corrections liées au scenario démo Urgence GHT (workflow lecture
multi-onglets + t2a_decision + pause_for_human + saisies dans Codage) :
1. Mode supervised propagé jusqu'au pipeline replay
---------------------------------------------------
Symptôme constaté ce 7 mai : Léa lit les onglets, t2a_decision tourne
(variable `dec` présente avec decision="FORFAIT_URGENCE"), mais la
pause_for_human est SKIPPÉE silencieusement et les saisies type_text
s'enchaînent dans le mauvais écran.
Cause : api_stream.py:2140 passait `params={}` codé en dur lors de la
création du replay_state. Conséquence : le code en aval qui lit
`replay_state.params.execution_mode` (api_stream.py:2964) avait toujours
le défaut "autonomous" → branche QW4 :
# Mode autonome sans safety_checks → skip (comportement legacy)
logger.info("pause_for_human ignorée (mode autonome)")
Modifications :
- RawReplayRequest gagne un champ `params: Optional[Dict[str, Any]]`
- start_raw_replay propage `request.params or {}` à _create_replay_state
- dag_execute.execute_windows force par défaut
`data['params']['execution_mode'] = 'supervised'` quand le frontend
ne précise rien (cas démo VWB → Windows). Override possible.
Conséquence : la pause_for_human du workflow Urgence déclenche bien la
PauseDialog VWB ("Décision : {{dec.decision_court}}"). Le médecin valide
ou annule avant que les saisies type_text ne s'exécutent dans Codage.
Note pour la démo réelle (post-aujourd'hui) : le scénario crédible
veut que Léa soit déclenchée depuis SON chat (port 5004), pas depuis
VWB. C'est un autre commit à venir — pour l'instant VWB suffit pour
le développement (cf. handoff session).
2. Seuil détection image tronquée élargi
----------------------------------------
Le seuil initial (height < 200 OR width < 400) ne capturait que les
cas extrêmes 2560x60 / 600x72. Mais le client envoie aussi 622x856
(Edge en fenêtre réduite ?) qui passait sous le radar. Élargi à
height < 800 OR width < 1200 — un écran moderne fait toujours ≥
1920x1080, donc le seuil est sain.
Sans ce fallback élargi, _resolve_target_sync recevait une image
trop petite pour matcher l'anchor → cascade VLM hallucinante.
Bug client constaté ce 2026-05-07 sur PC Windows 192.168.1.11 (agent V1) :
mss.monitors[1] retourne parfois une image tronquée type 2560x60, 2560x108,
600x72 — possiblement la barre des tâches Windows confondue avec un monitor,
ou un état mss corrompu. Reproduit même PC en mono physique. Cause exacte
non isolée côté client.
Sans cette image, _resolve_target_sync ne peut rien résoudre :
- Template matching échoue (anchor 104x31 vs image 600x72)
- OCR direct ne trouve pas la cible (texte hors de l'image tronquée)
- VLM Quick Find hallucine systématiquement la même position
- Fallback recorded_coords clique au mauvais endroit
Conséquence reproduite hier soir : "Léa clique partout au pif"
(cf. session_20260506_handoff_v2.md).
Filet de sécurité côté serveur : si l'image reçue est anormalement
tronquée (height < 200 ou width < 400), le serveur la remplace par le
dernier heartbeat full screen avant la cascade _resolve_target_sync.
Sources de fallback dans l'ordre :
1. _last_heartbeat (mémoire, peuplé par /stream/image en runtime)
2. Scan disque data/training/live_sessions/*/bg_*/shots/heartbeat_*.png
(utile après restart serveur ou si l'agent V1 ne polle pas)
Validé en isolation : image tronquée 600x60 → fallback heartbeat 2560x1600
→ template matching score 0.999 → coords (0.0312, 0.3500) = exactement
la position de l'IPP cible '25003284' en première ligne d'Easily Assure.
Bug client à traiter post-démo. Le fallback heartbeat reste utile en
roadmap autonome (résilience aux états mss transitoires).
Note : également retiré un import os local redondant dans le finally
(masquait la variable globale et provoquait UnboundLocalError dans
le scope du bloc fallback).
Revert effectif du commit c969f93a2.
Le Win+D auto au retry 1 produit un cercle vicieux quand combiné avec
le VLM-first qui hallucine systématiquement (positions répétitives
type 0.529/0.874 avec confidence 0.93 sans justification) :
click rate (cible mal localisée par VLM) → no_screen_change
→ Win+D auto → minimise Easily Assure
→ retry click → cible plus visible (Easily masquée par Win+D)
→ no_screen_change → Win+D encore → boucle infernale
Reproduit ce 2026-05-06 sur le workflow Urgence : 10 Win+D dispatchés
en moins de 2 minutes. Régression majeure ressentie par Dom :
"clic partout au pif, aucune action contrôlée".
L'idée du self-healing par gesture reste valide mais demande :
1. un déclenchement plus sélectif (genre overlay/popup détecté
visuellement, pas no_screen_change générique)
2. ou un Alt+Tab plutôt que Win+D (fait passer la fenêtre arrière
sans minimiser l'app cible)
3. ou une vraie analyse "y a-t-il une fenêtre qui obstrue ma cible"
avant de décider du gesture
À retravailler post-démo avec un vrai détecteur d'obstruction.
Audit project-quality-guardian (2026-05-06) Cas #2 : le mécanisme
qui invoquait gesture_catalog.win_minimize_all (Win+D) en cas
d'échec de grounding a été archivé le 24/04 dans
_archive/dead_code_20260424/core/visual/rpa_integration_manager.py
(_attempt_self_healing_resolution). Le catalogue
agent_chat/gesture_catalog.py:84 reste intact mais orphelin —
aucun caller actif.
Conséquence : quand une fenêtre/popup obstrue la cible, Léa
retente N fois la même action ratée puis pose une pause supervisée,
alors qu'un Win+D ("Afficher le bureau") règle souvent le problème
en 200 ms.
L'audit proposait observe_reason_act.py mais ce module est utilisé
uniquement par /execute/instruction (lui aussi sans client actif,
Cas #10). Le bon point d'insertion dans le pipeline replay actif
est _schedule_retry (replay_engine.py) — la fonction qui construit
la liste d'actions à réinjecter en tête de queue avant chaque retry.
Modification :
Au next_retry == 1 ET reason in ("verification_failed",
"no_screen_change"), insertion en tête de queue de :
1. Action key_combo {keys: ["super", "d"]} (format reconnu par
agent_v1/core/executor.py:1151), tagué
_recovery_gesture: "win_minimize_all" pour audit.
2. Wait 500 ms pour laisser l'OS terminer l'animation Win+D.
3. Le retry de l'action originale.
Au retry 2 et au-delà, comportement inchangé (wait 2s + retry).
Tests : 27/27 baseline sprint QW verts.
Audit project-quality-guardian (2026-05-06) : la fonction
_resolve_by_ocr_text (resolve_engine.py:1447) existait déjà mais
n'était appelée QUE depuis _resolve_with_precompiled_order (V4),
endpoint sans client côté frontend (Cas #5 du même audit). La
cascade legacy _resolve_target_sync sautait directement d'étape 0
(grounding-window) → étape 0' (template icônes) → étape 1 (VLM
Quick Find) sans tenter l'OCR direct.
Conséquence reproduite ce 2026-05-06 sur le workflow Urgence :
chaque action visuelle avec by_text payait 2-23 s de VLM Quick
Find (ui-tars-1.5-7b-q8_0 sur Ollama) au lieu de <500 ms d'OCR
direct, total replay > 10 min vs quelques secondes attendues.
Constat utilisateur : "habituellement on est plutôt à quelques
secondes". Régression silencieuse.
Modification :
Étape 0.5 ajoutée entre l'étape 0' (template icônes) et l'étape 1
(VLM Quick Find). Si by_text_strict est non vide, appel à
_resolve_by_ocr_text — fonction docTR existante, cache singleton
_V4_OCR_PREDICTOR, score 1.0 si match exact, 0.9 si mot exact,
0.8 si contenu. Seuil de retour : 0.80 (cohérent avec
_RESOLUTION_MIN_SCORES["hybrid_text_direct"]).
Le method retourné est rebadgé "hybrid_text_direct" pour cohérence
avec :
- _RESOLUTION_MIN_SCORES (seuil 0.80, ligne 2092)
- agent_v0/agent_v1/core/executor.py:1534 (client Windows)
- logs Learning historiques ([hybrid_text_direct])
Tests : 39/39 sprint QW + grounding/resolver verts.
Suite directe des commits 35b27ae49 (lock async sur /replay/next) et
87dbe8c5f (get_replay_status non-bloquant) qui n'avaient traité que
2 endpoints sur les 19 utilisant _replay_lock dans api_stream.py.
Reproduit aujourd'hui en pré-démo : un replay urgences a réussi
extract_text + t2a_decision (50s, OK), puis a hang sur l'action
suivante. start_raw_replay (POST /replay) du nouveau replay a tenté
`with _replay_lock:` synchrone à la ligne 2085 → MainThread asyncio
gelé → tous les endpoints derrière. Stack via py-spy confirmée.
Le pattern systémique : 17 sites `with _replay_lock:` synchrones
dans des handlers `async def` (start_replay, start_raw_replay,
replay_from_session, enqueue_single_action, launch_replay_from_plan,
get_next_action [×3], report_action_result [×5], register_error_callback,
list_replays, resume_replay, cancel_replay). Chacun gèle l'event
loop FastAPI dès qu'un autre thread tient le lock.
Modifications :
1. Helper _async_replay_lock(timeout=4.5) (api_stream.py:516).
Acquire via run_in_executor (event loop libre pendant l'attente),
timeout 4.5s puis HTTPException 503 plutôt que gel infini.
Sémantique acquire+release identique au `with` synchrone.
2. Remplacement automatisé des 17 sites async :
`with _replay_lock:` → `async with _async_replay_lock():`
2 sites sync intentionnellement préservés (cleanup loop ligne 689,
chat_status_provider ligne 5048 — pas dans des handlers async).
3. Import contextlib ajouté en haut du fichier.
Tests : 27/27 baseline sprint QW verts, /health 200 (3ms),
/replays 200 (2ms — endpoint qui utilise le nouveau helper).
Suite du commit 35b27ae49 (lock async sur /replay/next) qui n'avait
traité que la moitié du problème. Le sprint QW4 (commit f5c33477f)
a recâblé le polling frontend PauseDialog vers /replay/{replay_id} →
get_replay_status, qui gardait un `with _replay_lock:` synchrone.
Conséquence : dès qu'une action serveur (extract_text/extract_table/
t2a_decision) tient le lock, l'event loop FastAPI gèle entièrement
(heartbeats Windows, polls replay/next, get_replay_status, tout).
Reproduit aujourd'hui en pré-démo : un replay urgences a fait
extract_text → la queue suivante a tenu le lock → polling VWB sur
get_replay_status a bloqué le MainThread asyncio → 23 minutes de
gel total (py-spy a confirmé MainThread sur api_stream.py:4117).
Modifications :
1. get_replay_status : acquire timeboxé 0.5s via run_in_executor
(même pattern que /replay/next ligne 2815). Si le lock est tenu,
retour immédiat {status: "busy"} → le frontend retentera dans 1s.
Aucun cas où ce poll bloque l'event loop.
2. Actions serveur lignes 2994/3000/3006 : enveloppées dans
asyncio.wait_for(timeout=180). Borne dure pour qu'un hang
d'EasyOCR / Ollama / I/O ne tienne plus jamais le lock
indéfiniment. TimeoutError est rattrapée par l'except Exception
existant → queue.pop(0) → on continue.
Tests : 27/27 baseline sprint QW verts.
- safety_checks_provider : tous les logger.warning d'échec LLM préfixés
[BUS] lea:safety_checks_llm_failed avec une raison spécifique
(exception, http_status, timeout, network, json_decode).
- monitor_router : émission [BUS] lea:monitor_invalid_index si l'index
explicite passé dans l'action est hors limites de monitors_geometry,
et [BUS] lea:monitor_unavailable si focus actif demandé mais introuvable.
Ces deux events permettent au bus de tracer chaque fallback de la cascade
de routage QW1.
- safety_checks_provider : import io supprimé (inutilisé).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Avant ce fix, le _heartbeat_loop côté Agent V1 deploy Windows
n'enrichissait pas son payload, donc QW1 multi-écran ne s'activait sur Windows
que via les events window_capture (déclenchés par les clics), pas en continu.
La source agent_v0/agent_v1/main.py portait déjà l'enrichissement (commit 2d71e2a24)
mais le snapshot deploy/windows_client/agent_v1/main.py n'avait pas été synchronisé.
Désormais chaque heartbeat porte monitor_index + monitors_geometry, le serveur
peut donc résoudre l'écran cible en permanence, même sans clic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
replay_state enrichi de _screenshot_history (5 dernières images PIL) et
_action_history (5 dernières signatures action).
report_action_result :
- met à jour les deux anneaux après chaque action
- évalue le LoopDetector (singleton lazy avec _clip_embedder serveur)
- si detected → bascule paused_need_help avec pause_reason="loop_detected"
et bus event lea:loop_detected (signal + evidence)
Tous les chemins d'erreur (embedder absent, OOM, exception) loggent et
laissent le replay continuer — aucun blocage par la couche détection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cleanup post-review QW1 :
- Émission bus lea:monitor_routed dans /replay/next (idx, source, replay_id, action_id, offset, wh)
via logger.info "[BUS] lea:monitor_routed ..." (le serveur streaming n'a pas
de SocketIO local, agent_chat émet déjà lea:* sur 5004 ; ici on logge en INFO
bien lisible, prêt pour un parser/pont futur)
- Executor Agent V1 (deploy/windows_client) lit action.monitor_resolution.{offset_x, offset_y, idx}
et applique l'offset aux coords absolues du clic/type/scroll/popup quand idx >= 0
- composite_fallback (idx=-1) : pas d'offset appliqué (backward compat mono-écran)
- Log INFO "QW1 monitor cible idx=N source=X offset=(dx,dy) — appliqué aux coords"
émis une fois par action quand un offset non nul s'applique
Tests : baseline 95 passed (e2e + phase0_integration + stream_processor + monitor_router + grounding_offset)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Côté client Agent V1 :
- helpers _get_monitors_geometry() / _get_active_monitor_index() via screeninfo
(fallback gracieux [] / None si screeninfo absent)
- _enrich_with_monitor_info() ajouté aux payloads dict de capture_dual,
capture_active_window, et heartbeat_event poussé par main.py
- screeninfo>=0.8 ajouté aux requirements (source + deploy Windows)
- Deploy capturer.py reçoit l'enrichissement de manière additive (pas de
copie verbatim qui aurait introduit BLUR_SENSITIVE absent côté deploy)
Côté serveur :
- import resolve_target_monitor depuis monitor_router (créé en QW1.1)
- /replay/next : enrichissement action.monitor_resolution avant envoi
au client (idx, offset_x/y, w, h, source de la décision)
- live_session_manager.add_event : propagation monitor_index +
monitors_geometry depuis window_capture ET depuis le payload event
brut (cas heartbeat enrichi sans window/window_title)
Cascade de résolution (cf monitor_router.py) :
1. action.monitor_index (hérité de la session source)
2. session.last_focused_monitor (focus actif vu en dernier heartbeat)
3. composite_fallback (offset 0,0) — backward compat strict
Backward 100% : si geometry vide, fallback composite identique au
comportement actuel mss.monitors[0].
Tests : baseline 89/89 préservée, monitor_router 4/4 OK (total 93/93).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six modifications structurelles côté serveur, non destructives, aboutissant à un
pipeline replay bien plus stable pour la démo GHT Sud 95 (Urgences UHCD).
1. visual_workflow_builder/backend/app.py
load_dotenv() chargeait .env (cwd) au lieu de .env.local racine projet.
Conséquence : RPA_API_TOKEN absent après chaque restart manuel du backend
et tous les proxies VWB→streaming échouaient en 401 « Token API invalide ».
Charge maintenant explicitement .env.local du project root.
2. visual_workflow_builder/backend/api_v3/learned_workflows.py
Quatre appels proxy /api/v1/traces/stream/* ne portaient pas le Bearer.
Helper _stream_headers() factorisé et appliqué (workflows list/detail,
workflow detail, reload-workflows).
3. visual_workflow_builder/backend/api_v3/dag_execute.py
_ANCHOR_CLICK_TYPES excluait type_text/type_secret : pas de pre-click de
focus avant la frappe → texte tapé sans focus → textareas vides au replay.
Helper _inject_anchor_targeting() factorisé (centre bbox + visual_mode +
target_spec) appliqué aux click_anchor* ET aux type_text/type_secret dès
qu'un anchor_id est présent. Workflows historiques sans anchor sur
type_text → comportement inchangé.
4. agent_v0/server_v1/api_stream.py — endpoint /replay/next
_replay_lock (threading.Lock global) tenu pendant les actions serveur
lentes (extract_text OCR ~5s, t2a_decision LLM ~8-13s). Comme le handler
est async def, l'event loop FastAPI était bloqué : les polls clients
timeout à 5s, leurs actions étaient popped serveur sans destinataire,
perdues silencieusement. Mesure : 8 actions/25 perdues sur replay Urgence.
acquire(timeout=4.5) puis run_in_executor pour libérer l'event loop
pendant l'attente du lock ET pendant les handlers serveur synchrones.
Pendant un t2a_decision en cours, les polls concurrents reçoivent
immédiatement {action: null, server_busy: true} → l'agent ne timeout
plus, aucune action n'est popped sans destinataire.
5. agent_v0/server_v1/resolve_engine.py — _validate_resolution_quality
Drift > 0.20 par rapport aux coords enregistrées → fallback aux coords
enregistrées même quand le template matching trouve l'image avec un
score quasi parfait. Or un score >= 0.95 signifie que l'image EST
visuellement à l'écran à l'endroit indiqué, le drift reflète juste
un changement de layout (scroll, F11, redimensionnement), pas une
erreur. Exception ajoutée : score >= 0.95 sur template_matching →
ignore drift check, utilise position visuelle.
6. core/llm/t2a_decision.py — prompt T2A/PMSI
Ancien prompt autorisait « Critère non validé » en fallback creux.
Nouveau prompt impose au moins une CITATION LITTÉRALE entre « ... »
du DPI dans chaque preuve_critereN, qu'elle soutienne ou infirme le
critère. Si non validé : factualisation explicite (« Aucune ... »,
« Sortie à H+2 ») citée du dossier. Sortie = preuves cliniques
traçables et professionnelles, pas du remplissage.
État DB : aucun changement net (bbox patchés puis revertés depuis backup
visual_anchors_backup_20260501 ; by_text re-aligné sur 25003284). Le
re-enregistrement du workflow Urgence en conditions bureau standard
(Chrome normal, taille fenêtre standard) est l'étape suivante côté Dom.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dag_execute.py /execute-windows :
- Bearer token sur appels VWB→streaming (machines, replay/raw).
Sans cela : 401 Unauthorized et le workflow ne démarre pas.
- Auto-injection session_id='agent_demo_user' si absent.
Sans cela : /replay/raw bascule sur l'auto-détection sess_* et lève
"Aucune session Agent V1 active" après tout restart du streaming server.
- Propagation by_text dans target_spec pour ciblage textuel
(résolution hybrid_text_direct côté executor) — utile quand
deux numéros se ressemblent visuellement (ex 25003284 vs 2500341).
t2a_decision.py : prompt enrichi avec decision_court (UHCD / Forfait
Urgences) + 3 critères PMSI (preuve_critereN + critereN_valide booléen)
pour piloter case-à-cocher dans l'arbre décisionnel. num_predict=1500,
num_ctx=16384.
resolve_engine.py : un drift trop grand bascule sur les coords
enregistrées (fallback_recorded_coords, resolved=True) au lieu de
rejeter la résolution. Permet au replay de continuer en cas de scroll
plutôt que de s'arrêter net.
workflows.db : by_text='25003284' sur le step de sélection patient
du workflow Urgence (démo GHT Sud 95).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Nouvelle action native VWB qui force le replay à basculer en paused_need_help
avec un message custom. Quand Léa atteint cette étape, elle ne tente pas
d'exécuter — elle pose immédiatement le state, ce qui déclenche la bulle
interactive ChatWindow (J3.5) avec boutons Continuer / Annuler.
Asset démo majeur GHT Sud 95 : permet de scénariser le moment "Léa doute"
au bon endroit dans le workflow, sans dépendre d'un échec aléatoire.
Chaîne complète :
- VWB UI (types.ts) : nouvelle entrée ACTIONS catégorie 'logic', icône ⏸,
paramètre 'message' éditable (textarea).
- Bridge VWB → core (learned_workflow_bridge.py) : passthrough du type +
préservation du message dans parameters.
- Pipeline replay (replay_engine.py) : type ajouté à _ALLOWED_ACTION_TYPES,
conversion edge → action normalisée préserve le message.
- Streaming server (api_stream.py /replay/next) : interception avant envoi
à l'Agent V1 → bascule state en paused_need_help avec pause_message,
retourne {action: None, replay_paused: True}.
- L'action n'est jamais transmise à l'Agent V1 — pure logique serveur.
10 nouveaux tests pytest. Total branche : 57/57 verts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Quand Léa bascule en pause supervisée (event 'lea:paused'), affichage d'une
bulle dédiée dans ChatWindow avec encadré orangé, raison de la pause, et deux
boutons Continuer/Annuler. C'est le moment qui incarne la différence RPA classique
vs Léa devant Carvella : Léa SAIT qu'elle ne sait pas et demande de l'aide.
Architecture (canal SocketIO bidirectionnel, pas de nouvel endpoint streaming) :
ChatWindow ──[lea:replay_resume]──> agent_chat ──POST /resume──> streaming
ChatWindow ──[lea:replay_abort ]──> agent_chat (running=False local)
Composants ajoutés :
- agent_chat/app.py : handlers 'lea:replay_resume' / 'lea:replay_abort' +
acks 'lea:resume_acked' / 'lea:abort_acked' pour feedback côté client
- network/feedback_bus.py : méthodes resume_replay() / abort_replay() avec
helper _safe_emit (silencieux + retourne bool succès)
- ui/chat_window.py : palette PAUSED_*, _add_paused_bubble(),
_render_paused_bubble(), _close_active_paused_bubble() (auto-fermeture
sur lea:resumed/done), _on_paused_resume/abort
8 nouveaux tests pytest (4 handlers serveur + 4 méthodes client).
Total branche : 29/29 verts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Import fail-safe : si python-socketio manquant (ancienne install Pauline),
_HAS_FEEDBACK_BUS=False, ChatWindow tourne normalement sans bus
- Bus démarré à la fin de _run_tk_loop si LEA_FEEDBACK_BUS=1 dans l'env
- Callback _on_lea_event → _add_lea_message (thread-safe via root.after)
- Cleanup : _bus.stop() ajouté dans _do_destroy avant la destruction tkinter
Formatage des bulles minimal pour J3.3 (texte brut "[event] key=value").
Le style mixte métier+tech viendra en J3.4. La bulle paused interactive J3.5.
Aucun crash si bus indisponible. Aucun changement de comportement si flag off.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Consomme les events 'lea:*' émis par agent_chat (port 5004) et les dispatche
vers un callback fourni par ChatWindow (J3.3 à venir).
Caractéristiques :
- Connexion en thread daemon (non-bloquant pour la mainloop tkinter)
- Reconnect auto illimité (delay 2s → 30s exponentiel)
- Auth Bearer Token via header HTTP au handshake
- Fail-safe : connect échoué, callback qui raise, disconnect qui raise
→ tout silencieusement loggé, ChatWindow continue normalement
13 tests pytest verts (tests/integration/test_feedback_bus_client.py).
Pas de connexion réseau réelle dans les tests (python-socketio mocké).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Compatible Flask-SocketIO 5.3.x côté serveur. Ajouté aux deux requirements
client (agent_v1/ et deploy/windows_client/) — le second est utilisé par
l'installeur Pauline (setup_v1.bat).
ATTENTION : redéploiement client requis (PC Windows + VM Linux) avant la démo
GHT Sud 95. La dep ne sert à rien tant que J3.2 (FeedbackBusClient) n'est pas en
place ; aucun impact runtime sur l'agent V1 actuel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Point de sauvegarde incluant les fichiers non committés des sessions
précédentes (systemd, docs, agents, GPU manager).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>