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>
Bench standalone qui exécute build_dpi_enriched + appel LLM sur les 11
dossiers POC GHT Sud 95 (docs/clients/ght_sud_95/mockup_easily_assure/data.js),
sans passer par Demo_urgence_2 ni Léa/Windows. Permet de mesurer la
convergence durée/décision Python ↔ LLM sur un panel représentatif AVANT
d'écrire le garde-fou serveur du commit 2.
core/llm/t2a_mappings.py :
- Module partagé TERRAIN_VERS_T2A (4 entrées validées par Dom 12/05)
- Importé par le bench, sera importé aussi par le garde-fou serveur commit 2
- Cas non mappés volontairement documentés (Retour structure d'origine,
chaîne vide pour statut_attente)
scripts/bench_t2a_dryrun.py :
- Parsing data.js via node (vm.runInContext) → 11 dossiers en JSON
- Reconstruction d'un dpi_raw plat simulant l'OCR scroll auto :
bandeau Easily Assure répété 5x (1 par onglet) + sections motif /
examens / imagerie / notes médicales / Synthèse Urgences au format
LIBELLÉ VALEUR
- NE bypasse PAS build_dpi_enriched : le dpi_raw est texte plat re-parsé
par la fonction (test de robustesse réel du parser regex)
- Appel LLM déterministe : temperature=0, seed=42, model=gemma4:31b-cloud
- Vérification empirique du respect du seed (2 appels successifs sur 1er
dossier, comparaison decision/durée/justif) → warning si bruit cloud
- 4 traces structurées par dossier dans logs/t2a_dryrun/<IPP>_<ts>.log :
[t2a_dryrun_metadata] / [t2a_dryrun_prompt] / [t2a_dryrun_response]
ou [t2a_dryrun_error] en cas d'échec API
- Filet data_quality_warning (incohérence âge déclaré vs date naissance,
motif vs diagnostic principal, décision vide) — filet, pas analyse
exhaustive ; signale sans corriger (anonymisation v1 incertaine)
- Tableau récap stdout 9 colonnes + CSV scripts/bench_t2a_dryrun_<ts>.csv
- Stats agrégées : convergence durée X/N, convergence décision X/N
mappés, liste détaillée des divergences avec pointeurs vers logs
- Recommandation auto : réécrire PROMPT 3 ou non selon convergence durée
Activation : T2A_DRYRUN=1 python scripts/bench_t2a_dryrun.py
Options : --ipp <IPP> (1 dossier), --skip-seed-check
Smoke test pré-commit (sans LLM) : parsing + dpi_raw + build_dpi_enriched
sur les 11 dossiers → 11/11 metadata complets, 0 parsing_warning,
durées calculées de 2.0h à 12.02h, décompo décisions terrain conforme
(7 Consultation + 1 Hosp + 1 UHCD + 1 Transfert + 1 Retour structure).
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>
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>
Module pur core/grounding/smart_resize.py implémentant la formule
smart_resize officielle (transformers.qwen2_vl.image_processing_qwen2_vl,
utilisée par Qwen3VLProcessor pour les images via wrap Qwen2VLImageProcessor).
Helpers exposés : _round_by_factor, _floor_by_factor, _ceil_by_factor.
Constantes : FACTOR_DEFAULT=28, MIN_PIXELS_DEFAULT=3136,
MAX_PIXELS_DEFAULT=1_003_520, MAX_RATIO_DEFAULT=200.
Tests : tests/unit/test_smart_resize.py — 32 cas, 100% coverage sur le
module (mesure via coverage API directe, pytest-cov bloqué par bug cv2
préexistant tracé dans DETTE-011).
refs DETTE-006 (étape 1/5 du fix smart_resize)
refs DETTE-007 (création de la 3ème implémentation, à unifier post-démo)
refs DETTE-010 (vérif preprocessor_config.json checkpoint Qwen3-VL-8B
bloquante avant Étape 2)
refs DETTE-011 (bug cv2 contourné pour mesure coverage)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 commits du jour : pré-check OCR réactivé + instrumenté + bug
spatial documenté. Plan demain : fix smart_resize vLLM ciblé
selon MIGRATION_VLM_PLAN_2026-05-09.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug découvert pendant test live du 2026-05-08.
_text_match_fuzzy valide la présence du texte dans le crop (560×560 px)
sans vérifier sa position au point cliqué. Sur onglets serrés (3 px),
valide à tort les clics adjacents.
À fixer post-démo Kerella - Option B préférée
(bboxes EasyOCR + distance).
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>
Audit exhaustif des 101 fichiers .md de ~/.claude/projects/-home-dom-ai-rpa-vision-v3/memory/.
Aucun fichier mémoire modifié — diagnostic seul, à valider par Dom.
Constats critiques :
- MEMORY.md = 273 lignes (limite chargement 200) → ~73 lignes
silencieusement perdues à chaque démarrage de session
- ~50% des fichiers réels ne sont pas indexés dans MEMORY.md
- Référence cassée : MEMORY ligne 257 pointe vers
feedback_pull_not_push.md qui n'existe pas
- 3 feedback NEW créés le 7 mai (non ajoutés à l'index) sont
précisément les règles qui sécurisent la démo GHT jeudi 8 mai :
* feedback_orphans_are_projections.md
* feedback_verifier_avant_apres_clic.md
* architecture_lea_v1_find_text_client.md
Risque concret : un Claude futur (sans ces feedback en mémoire active)
va reproposer les bourdes que Dom a explicitement nommées hier soir :
"re-capturer les ancres" et "nettoyer les modules orphelins".
Top 7 feedback proposés en TOP CRITICAL :
1. prendre_le_temps (DEVISE)
2. orphans_are_projections (NEW)
3. verifier_avant_apres_clic (NEW)
4. lea_v1_find_text_client (NEW architecture)
5. ollama_vs_transformers
6. no_rustine
7. anonymisation_stricte
Proposition réorganisation 4 zones :
- 🔥 TOP CRITICAL ~12 fichiers
- 📌 ACTIVE ~25 fichiers
- 📚 REFERENCE ~12 fichiers
- 🗄️ ARCHIVE ~50 fichiers
Compactage cible : MEMORY.md → 150 lignes (marge 50 avant
retrigger limite chargement).
4 décisions ouvertes pour Dom (cf rapport §8) :
1. feedback_pull_not_push.md — créer ou supprimer la référence
2. Valider l'archivage des ~45 fichiers proposés
3. Trancher 4 fichiers INCERTAIN (dashboard_config, data_extraction,
objectif_6avril, actor_*)
4. Approuver 7 règles de gestion future (1 feedback = 1 violation
observée, MEMORY ≤ 180 lignes, rotation sessions > 21j, etc.)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit du cœur métier de la démo GHT Sud 95 (8 mai 2026), du point de vue
d'un médecin DIM senior qui se ferait challenger par le DSI Carvella.
Confronte : arbre officiel RPU UHCD IA.pptx (7 slides), code métier
agent_chat/urgences_orchestrator.py + core/llm/t2a_decision.py, prompts
LLM en place, 11 dossiers anonymisés data.js, bench Dom 18 modèles,
référentiels officiels (SFMU 2024, instructions DGOS, arrêtés 2021/2024
ATIH, recommandations IPAQSS).
Findings critiques (avant démo) :
1. Bug silencieux modèle — t2a_decision.py:28 met DEFAULT_MODEL=qwen2.5:7b
(64 % accuracy au bench Dom) alors que gemma3:27b-cloud (73 %) est
retenu par BENCH_T2A_DECISION_11DOSSIERS. Si T2A_MODEL pas posé via
env, on tourne sur le mauvais modèle. 9 points d'accuracy laissés
sur la table.
2. Règle de combinaison incorrecte dans le prompt — code dit "au moins
2 sur 3 ⇒ REQUALIFICATION" alors que l'arbre PPTX d'Eaubonne dit
"si oui aux 3 critères". Cause probable des faux positifs UHCD du
bench (25003284, 25056615). Quick win = passer à 3/3.
3. Trous métier dans le prompt : aucune mention CCMU, GEMSA, durée,
mode de sortie, type de forfait précis (SU2/PE2/Standard). C'est
exactement où se loge le ROI 100k€/mois. 5 quick wins prompt
rédigés prêts à coller dans §E.4 du rapport.
4. Trois dossiers à NE PAS montrer en démo (25056615, 25151530, 25003475,
25048485) — trop ambigus, hallucinations LLM, structure non tranchée.
5. Trois dossiers à mettre en avant (25003451 SU2 plaie 2h, 25010621
PE2 laryngite, 25003364 UHCD pneumo SLA) — décisions justes,
justifications béton.
Argumentaire pré-démo : 9 questions/réponses face à Carvella
(instructions DGOS, SFMU, cumul SU2+PE2, hallucination LLM, ROI 100k€).
Roadmap post-démo pour Amina : bench étendu 50-100 dossiers + 3
inférences/dossier, fine-tune t2a-gemma3-27b, distinction forfaits
fine, module ATIH-aware, couverture pédia/géria/psy, sortie contre
avis, transferts.
Note : aucun changement de code dans ce commit. Rapport seul. Les
quick wins identifiés (3/3, modèle par défaut, prompts enrichis)
sont à appliquer demain matin avec validation Dom + Amina.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Réduit le cycle debug d'un workflow de 1-2 min (replay manuel via
Windows + Léa V1 + maquette) à ~2-5s (mock client Linux contre
serveur de streaming localhost:5005). 30-60× plus rapide.
Architecture :
- tools/test_replay_e2e.py — harness CLI (~580 lignes), reproduit la
chaîne réelle : VWB /api/v3/execute-windows → streaming /replay/raw
→ boucle /replay/next côté harness avec resolve_target sur un
screenshot fixture → POST /replay/result. Pas de modification serveur.
- tests/e2e/test_urgence_aiva_demo.py — wrapper pytest (smoke).
- tests/e2e/urgence_aiva_demo_expected.yaml — référence générée par
--export-expected, pour comparaison régression auto.
- pytest.ini — ajout du marqueur e2e.
Usage :
python tools/test_replay_e2e.py --execution-mode autonomous --max-iter 120 --verbose
python tools/test_replay_e2e.py --single-step 8 --shot <heartbeat>.png
python tools/test_replay_e2e.py --expected tests/e2e/urgence_aiva_demo_expected.yaml
pytest tests/e2e -v -m e2e
Sortie : tableau Markdown step × méthode × score × pos × status × diag.
Limitations connues (extensions post-démo) :
- Une seule fixture screenshot pour tout le replay → click_anchor réalistes
échouent dès qu'on dépasse l'écran fixture. Carte step_id → fixture à venir.
- extract_text/table/t2a_decision exécutés côté serveur, observables mais
pas modifiables.
- Pas de simulation screenshot_after → ReplayVerifier (Critic VLM) ne tourne pas.
🤖 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.
Quand le frontend ne passe pas de machine_id explicite, le backend VWB
auto-sélectionne une machine Windows en interrogeant /api/v1/traces/
stream/machines. Le code prenait la première de la liste sans tri, donc
l'ordre dépendait de l'ordre arbitraire renvoyé par le streaming server.
Conséquence reproduite ce 2026-05-06 : un replay du workflow Urgence a
été dispatché vers DESKTOP-ST3VBSD_windows alors que l'agent V1 actif
polait depuis DESKTOP-58D5CAC_windows. /replay/next ne dispatchait
aucune action puisque state.machine_id != polling_machine_id.
Symptôme côté Dom : "rien ne se passe sur Windows".
Correction : tri explicite par last_activity desc avant sélection.
La machine retenue est désormais celle qui a heartbeaté le plus
récemment (= celle qui POLLE actuellement le serveur).
Le workflow.machine_id (machine d'origine d'enregistrement) reste
distinct de la cible d'exécution : un workflow enregistré sur PC A
peut être rejoué sur PC B grâce au pipeline 100% visuel qui recalcule
anchors et coordonnées selon la résolution courante. C'était la
vraie intention architecturale, masquée par le bug de tri.
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 à la mise à jour système qui a basculé Dom de Xorg vers Wayland,
les 4 services systemd côté serveur partaient en boucle restart :
pyautogui levait DisplayConnectionError / KeyError(DISPLAY) à l'import
dans 3 modules, mais l'except n'attrapait qu'ImportError → crash fatal.
Le contournement « ajouter DISPLAY=:1 + XAUTHORITY=/run/user/1000/gdm/
Xauthority dans .service » introduit fin avril était fragile : chemin
invalide en Wayland (Mutter utilise un Xauthority à suffixe aléatoire
qui change à chaque login). Le bon fix est de rendre les imports
pyautogui robustes — le serveur n'a aucun usage légitime de pyautogui,
c'est le client Agent V1 Windows qui pilote souris/clavier.
Modifications :
1. Élargi `except ImportError` → `except Exception` pour pyautogui :
- agent_chat/autonomous_planner.py
- core/execution/input_handler.py
- core/execution/observe_reason_act.py
(action_executor.py était déjà robuste avec except Exception.)
2. Retiré DISPLAY/XAUTHORITY des 4 .service (rustines) :
- rpa-streaming.service
- rpa-vision-v3-{api,worker,dashboard}.service
Block grounding (RPA_GROUNDING_SOCKET) préservé (initiative
séparée de partage VRAM, in-flight).
PYAUTOGUI_AVAILABLE=False est désormais attendu côté serveur Linux ;
les chemins aval (action_executor, autonomous_planner) gèrent déjà
ce cas via des branches "actions simulées" / "pyautogui non disponible".
Prépare la roadmap autonome (Léa daemon Linux + VM Windows) qui
tournera headless via systemd au boot, sans dépendre d'aucune
session graphique active.
Tests : 27/27 baseline sprint QW verts.
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 frontend VWB ne savait pas qu'un replay Agent V1 (Windows)
était en pause supervisée : le seul polling (App.tsx) interrogeait
/execute/status (exécution locale Linux) et n'avait jamais l'info
safety_checks / pause_message du replay distant.
Côté backend (dag_execute.py) :
- ajout du proxy GET /api/v3/replay/state/<replay_id> qui forward vers
/api/v1/traces/stream/replay/<id> avec Bearer token.
Côté frontend :
- ExecutionControls : nouvelle prop onWindowsReplayStarted, appelée avec
le replay_id retourné par /api/v3/execute-windows.
- App.tsx : nouveau state streamingReplayId + useEffect qui poll
/api/v3/replay/state/<id> toutes les secondes et fusionne status,
pause_message, pause_reason, safety_checks dans appState.execution.
Le PauseDialog existant s'affiche donc automatiquement quand
status = paused_need_help.
Le polling s'arrête quand le replay est completed/error/cancelled.
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>
Plan exécutable seul par Dom : 9 sections (préflight, QW1 mono/multi-écran,
QW2 boucle, QW4 backward/déclaratif/medical_critical, bus events, kill-switches,
rollback) avec checklist OK/KO et procédures d'urgence en pleine démo.
Validation pour démo GHT (1ère sem mai 2026).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Doc condensée des 3 quick wins livrés (QW1 multi-écrans, QW2 LoopDetector,
QW4 safety_checks hybrides) avec :
- procédures kill-switch et rollback
- table des env vars
- smoke tests manuels à effectuer avant démo GHT
- statut composant par composant
Pointe vers spec et plan d'exécution complets.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PauseDialog (composant nouveau) :
- 2 modes selon payload : bulle simple legacy si safety_checks vide,
ChecklistPanel sinon
- Continuer désactivé tant que required non cochés
- Badge [obligatoire] et [Léa] (avec evidence en tooltip)
- POST /api/v3/replay/resume avec acknowledged_check_ids quand replay_id
présent, fallback api.resumeExecution() pour la voie locale
types.ts : SafetyCheck, SafetyLevel, extension Execution
(pause_reason, pause_message, safety_checks, replay_id, status
'paused_need_help'). Action pause_for_human enrichie de safety_level
et safety_checks dans le catalogue ACTIONS.
PropertiesPanel : éditeur safety_level (dropdown standard/medical_critical)
+ liste éditable de safety_checks (id/label/required + ajout/suppression).
App.tsx : rendu conditionnel du PauseDialog en overlay quand
status == paused_need_help, ou paused avec safety_checks. Backward 100% :
workflows existants sans safety_checks affichent la bulle legacy.
CSS : .pause-dialog-overlay/.pause-dialog-checks/.checklist-panel/
.check-item/.badge-required/.badge-lea/.check-editor-row.
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>