26 KiB
Diagnostic — Replay replay_free_68ca51ab bloqué sur l'onglet Imagerie
Date : 8 mai 2026 (matin avant démo GHT Sud 95)
Workflow : Urgence_aiva_demo (wf_a38aeebea5e6_1778162737)
Replay : replay_free_68ca51ab (cancellé manuellement à 10:34, après pause supervisée step 18)
Auteur : ingénieur senior debug RPA / vision
TL;DR — il y a deux causes simultanées, l'une explique l'autre.
Cause primaire (réseau) : le client Léa V1 Windows utilise un
read_timeout=5 ssurGET /replay/next. Sur cette même connexion, le serveur exécute parfois unextract_text(5–7 s) PUIS dispatche unclickdans le même appel. Le client coupe avant la réponse. L'actionclickétait déjà poppée de la queue serveur (_retry_pending), donc elle est perdue silencieusement — pas de retry automatique, pas de re-dispatch. C'est ce qui s'est passé pour les steps 10, 12, 14 et 17 (clic Imagerie, Notes médicales, Synthèse Urgences, Codage). Le client a poursuivi son polling, mais quand il est revenu à l'écoute, le serveur en était déjà au step 18 (Coller ou saisir le dossier patient) — qui n'existe que sur la maquette aiva-vision (codage.html), donc échectarget_not_found→ pause supervisée.Cause aggravante (vision) : la cascade
OCR-DIRECT(_resolve_by_ocr_text) renvoie le centre de la ligne entière quand letarget_textn'est qu'un sous-fragment (score=0.8). Pour la barre de tabs Easily, docTR détecte les 5 tabs comme une seule ligne. Conséquence :Imagerie,Notes médicalesetSynthèse Urgencesretournent quasiment les mêmes coordonnées (~0.23, 0.28) — c'est-à-dire le centre de la rangée de tabs (qui tombe sur Imagerie). Même si le client avait reçu chaque action, le clic aurait probablement raté la cible.
1. Reconstruction temporelle
Workflow Urgence_aiva_demo (steps issues de la DB SQLite, ordre 1→22) :
| Order | id (court) | type | label |
|---|---|---|---|
| 1 | 43ab3c1417d3 | extract_table | Lire liste patients (IPP) |
| 2 | 1dada40f6a44 | pause_for_human | Confirmer démarrage |
| 3 | 288d0bceea90 | click_anchor | Ouvrir dossier MOREL (25003284) |
| 4 | 5388268582d6 | extract_text | Lire Motif d'admission (t_motif) |
| 5 | b18e530526bb | keyboard_shortcut | Scroll fin de page (End) |
| 6 | b425b17b37f6 | extract_text | Lire bas de Motif (t_motif_bas) |
| 7 | fc4cf0a78b65 | keyboard_shortcut | Retour haut (Home) |
| 8 | 45f5d7fb7456 | click_anchor | Onglet Examens cliniques |
| 9 | 4148c9e8caa4 | extract_text | Lire Examens cliniques (t_examens) |
| 10 | 4c0663941f22 | click_anchor | Onglet Imagerie |
| 11 | 93cf4c6651f3 | extract_text | Lire Imagerie (t_imagerie) |
| 12 | 3b13c973d737 | click_anchor | Onglet Notes médicales |
| 13 | a5840d6bf8ed | extract_text | Lire Notes médicales (t_notes) |
| 14 | 8767d8e2e221 | click_anchor | Onglet Synthèse Urgences |
| 15 | 835e5dd54bb7 | extract_text | Lire Synthèse (t_synthese) |
| 16 | fc5a9676af55 | t2a_decision | Décision T2A (LLM) |
| 17 | 156d7cd29ebb | click_anchor | Onglet « Codage > » (vers maquette aiva) |
| 18 | 36346c1c40b9 | click_anchor | Cliquer textarea DPI (sur codage.html) |
| ... |
Logs serveur — journalctl --user -u rpa-streaming
Filtrage replay_free_68ca51ab + RESOLVE_* + REPORT + extract_text. Extraits pertinents :
10:25:46 RESOLVE_ENTRY by_text='Examens cliniques' strict_mode=True screen=2560x1490 has_anchor=True
10:25:48 Strict resolve OCR-DIRECT : OK 'Examens cliniques' → (0.2305, 0.2676) score=0.80
10:25:48 RESOLVE_EXIT resolved=True method='hybrid_text_direct' coords=(0.2305, 0.2676) score=0.8
10:25:49 REPORT step_45f5d7fb7456 success=True actual_position=(0.2305, 0.2798)
10:25:55 extract_text → variable 't_examens' (1689 chars)
10:25:55 DISPATCH action_id=step_4c0663941f22 (click) by_text='Imagerie' ← client a déjà timeout
10:26:01 extract_text → variable 't_imagerie' (1084 chars)
10:26:01 DISPATCH action_id=step_3b13c973d737 (click) by_text='Notes médicales' ← perdu
10:26:08 extract_text → variable 't_notes' (1084 chars)
10:26:08 DISPATCH action_id=step_8767d8e2e221 (click) by_text='Synthèse Urgences' ← perdu
10:26:17 extract_text → variable 't_synthese' (1084 chars)
10:26:27 t2a_decision → variable 'dec' decision=FORFAIT_URGENCE (10.0s)
10:26:27 DISPATCH action_id=step_156d7cd29ebb (click) by_text='Codage' ← perdu (concurrence de polls)
10:26:27 DISPATCH action_id=step_36346c1c40b9 (click) by_text='Coller ou saisir le dossier patient'
10:26:28 RESOLVE_ENTRY by_text='Coller ou saisir le dossier patient' strict_mode=True
10:26:30 Strict resolve OCR-DIRECT : 'Coller ou saisir le dossier patient' non trouvé, passage VLM
10:26:36 RESOLVE_EXIT resolved=False method='strict_vlm_template_failed'
…
10:28:47 REPORT step_36346c1c40b9 success=False error='target_not_found' warning='visual_resolve_failed'
10:34:00 Replay annulé manuellement
Ce qui ne figure PAS dans les logs serveur : aucun RESOLVE_ENTRY pour by_text='Imagerie', 'Notes médicales', 'Synthèse Urgences' ou 'Codage' côté replay live. La cascade de résolution n'a JAMAIS été appelée pour ces tabs. → Le client n'a jamais frappé /resolve_target ni reçu l'action.
Logs client — C:\rpa_vision\agent_debug.log
10:25:44.710 Action de replay recue : click (id=step_45f5d7fb7456 — Examens cliniques)
10:25:47.448 Server resolve OK [hybrid_text_direct] score=0.80
10:25:48.008 Replay click [VISUAL] : (0.230, 0.280) -> (590, 447) sur (2560x1600)
10:25:48.324 Ecran change apres ~200ms
10:25:48.537 Resultat rapporte : replay_status=running, restant=14
10:25:53.771 WARNING : HTTPConnectionPool(host=192.168.1.40, port=5005): Read timed out (read timeout=5)
10:25:53.771 Replay termine - retour en mode capture
10:25:53.780 shared_state Replay termine
← 33 s de silence
10:26:26.608 Action de replay recue : click (id=step_36346c1c40b9 — Coller ou saisir...)
10:26:35.409 Server resolve échoué : vlm_and_template_all_failed
10:26:39.096 Server resolve échoué : no_target_criteria
10:26:44.178 Server resolve échoué : vlm_and_template_all_failed
10:26:45.585 ERROR [LEA] Léa a besoin d'aide: Je n'y arrive pas (« Coller ou saisir... »)
…
10:28:45.762 [APPRENTISSAGE] Timeout global → 0 actions capturées
10:28:46.231 Replay termine
→ Confirmation directe : le client a sauté 9 actions serveur+visuelles entre l'OK Examens cliniques (10:25:48) et la réception de step 18 (10:26:26).
2. Diagnostic causal
Chaîne de responsabilité
+---------------------------------------------------------------------------+
| Hyp #1 (cascade serveur foire) — INFIRMÉE |
| La cascade serveur n'est même jamais invoquée pour ces 4 tabs. |
| |
| Hyp #2 (cascade locale Léa V1 prend le relais) — INFIRMÉE |
| Le client n'a pas reçu d'action → rien à résoudre localement. |
| |
| Hyp #3 (coords brutes du record obsolètes) — INFIRMÉE |
| L'ancre `anchor_0438bd2d9bdd_1778161174` (« Notes médicales ») a |
| bbox (444, 424, 146, 48) qui dans l'image de référence pointe sur |
| « Imagerie » et NON Notes médicales (le crop le confirme : |
| /tmp/anchor_0438bd2d9bdd_1778161174_bbox.png montre « Imagerie »). |
| Pareil pour anchor_6a2591e7c51c (« Synthèse Urgences ») dont la |
| bbox (580, 423, 192, 47) crop « Notes médicales ». |
| → Les bboxes des tabs sont décalées d'un cran à gauche dans la DB, |
| mais ce n'est PAS la cause du blocage actuel : le mode strict + OCR- |
| DIRECT ignore la bbox et part de by_text. Anomalie cosmétique à |
| nettoyer hors-démo. |
| |
| Hyp #4 (offset écran live vs record) — PARTIELLEMENT VRAIE |
| Voir §3. |
| |
| Hyp #5 (event onclick JS) — INFIRMÉE |
| Voir §3. |
| |
| Hyp #6 (cache client/serveur) — INFIRMÉE |
| Aucun `from_memory=True` dans les logs ; TargetMemoryStore pas hit. |
| |
| Cause primaire = HTTP TIMEOUT 5 s côté client |
| + actions serveur lentes (extract_text 5-7 s, t2a_decision 10 s) |
| + pas de watchdog d'orphelins dans `_retry_pending` |
| |
| Cause aggravante = OCR-DIRECT center-of-line bug |
| score=0.8 → coords = centre de la ligne docTR entière, pas du span. |
+---------------------------------------------------------------------------+
Mécanique exacte du timeout
agent_v0/server_v1/api_stream.py (get_next_action, lignes 2816-3083) :
- Acquiert
_replay_lockavecacquire_timeout=4.5 s. Sinon retourne{server_busy: True}— OK. - Une fois le lock pris, boucle
while queue:qui exécute toutes les actions « serveur » (extract_text,extract_table,t2a_decision,pause_for_humannon bloquant) dans le même appel HTTP, jusqu'à tomber sur une action visuelle (click/type/key_combo) qu'il dispatch et retourne. extract_textest wrappé dansloop.run_in_executor(...)(timeout 180 s) pour ne pas bloquer l'event loop FastAPI — bon design.- Mais le client appelle ce endpoint avec
timeout=5(executor.py:1786). Si la chaîneextract_text + dispatch_clickprend plus de 5 s, la réponse arrive après que le client ait fermé sa socket. La réponse contient leclickaction et est perdue. - Côté serveur (ligne 3209-3224), l'action est déjà poppée de la queue et stockée dans
_retry_pending[action_id]au moment du dispatch. Pas de retry automatique tant que le client ne renvoie pas un report (qui ne viendra pas). - Le client repasse en
_replay_active=False(main.py:331) — cosmétique — puis continue de poller. Au poll suivant, la queue est passée à l'action suivante (extract_text), idem boucle.
Aucun watchdog ne ré-énonce _retry_pending au client. L'unique chemin pour récupérer une action perdue serait que le client envoie un report avec success=False (jamais le cas ici puisqu'il n'a pas reçu l'action).
Mécanique exacte de l'OCR-DIRECT center-of-line
agent_v0/server_v1/resolve_engine.py:1447-1527 :
# Match exact > contient > mot par mot
score = 0.0
if target_lower == line_lower: score = 1.0
elif target_lower in line_lower: score = 0.8
elif any(target_lower == w.value.lower() for w in line_obj.words): score = 0.9
if score > best_score:
box = line_obj.geometry # bbox de la LIGNE ENTIÈRE
cx = (box[0][0] + box[1][0]) / 2
cy = (box[0][1] + box[1][1]) / 2
Quand docTR voit la barre de tabs Easily comme une seule ligne : "Motif d'admission Examens cliniques Imagerie Notes médicales Synthèse Urgences Codage >", il retourne pour CHAQUE target qui est un sous-fragment de cette ligne le centre de la rangée entière (~50 % en x, ~28 % en y). Les coords ne dépendent pas du tab demandé.
Preuve archivée dans les logs e2e_singleshot du 8 mai à 09:58 (single screenshot, donc résultats reproductibles) :
'Imagerie'→(0.2305, 0.2805)score 0.8'Notes médicales'→(0.2285, 0.2805)score 0.8'Synthèse Urgences'→(0.2285, 0.2805)score 0.8- (delta ~5 px, trois tabs visuellement à 4-5 cm d'écart les uns des autres)
→ Si le client avait reçu les 4 actions, il aurait cliqué 4 fois quasiment au même endroit (vers Imagerie). Bug latent indépendant du timeout.
3. Vérification des hypothèses 4 et 5
Hyp #4 — offset écran live vs record
Géométrie réelle du rendu Easily Assure dans une fenêtre Edge fullscreen 2560×1600 :
- Edge title bar : ~40 px (offset_y de la fenêtre = 49 d'après le log « Grounding contraint à la fenêtre : 2560x1490 (0, 49) »)
- Edge tabs/URL/bookmarks : ~250 px
.app-headerEasily (bleu) : 36 px (padding 8 + font 18).menu-bar(Patients/Planning/...) : 32 px.patient-banner(IPP MOREL...) : ~50 px.tabs(Motif/Examens/...) : 36 px (height CSS) → y range ≈ 410-450 dans l'image 2560x1600
Le crop de référence (ancre anchor_0438bd2d9bdd_1778161174) à y=420-480 montre exactement la rangée de tabs (cf. /tmp/tabs_row_full.png). Pas d'offset majeur entre record et live. Une éventuelle dérive ±10-30 px est gérable par un click au pixel central.
→ Hypothèse 4 partiellement vraie : il y a effectivement un offset, mais il n'est pas la cause du bug. Et il est dégradé par le bug OCR-DIRECT center-of-line (cause #2) puisque le centre de la ligne tombe au milieu de la barre, pas sur le tab demandé.
Hyp #5 — event onclick JS de la maquette
docs/clients/ght_sud_95/mockup_easily_assure/dossier.html:36-43 :
<div class="tabs">
<a class="tab active" data-tab="motif">Motif d'admission</a>
<a class="tab" data-tab="examens">Examens cliniques</a>
<a class="tab" data-tab="imagerie">Imagerie</a>
<a class="tab" data-tab="notes">Notes médicales</a>
<a class="tab" data-tab="synthese">Synthèse Urgences</a>
<a class="tab" id="tab-vers-codage" href="codage.html">Codage ></a>
</div>
app.js:377-401 :
function installTabs() {
const tabs = document.querySelectorAll('.tabs .tab[data-tab]');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const target = tab.getAttribute('data-tab');
history.replaceState(null, '', '#' + target + location.search);
activate(target);
window.scrollTo(0, 0);
});
});
}
→ Mécanisme propre. addEventListener('click') directement sur les <a class="tab">. Aucun overlay, aucun event swallow. Un MouseEvent (Win32 SendInput → Windows Edge → DOM) sur le pixel d'un tab DÉCLENCHE le listener. Le tab Codage est un lien href="codage.html" → navigation native. Aucun problème côté maquette.
→ Hypothèse 5 infirmée.
4. Reproduction en isolation
Données déjà disponibles via le test e2e_singleshot du 8 mai 09:58 (session=e2e_singleshot_1778227119_1fe686), qui appelle /resolve_target sur un screenshot fixe (probablement l'onglet Imagerie ouvert) :
| target_spec.by_text | Résolution | x_pct | y_pct | score |
|---|---|---|---|---|
25003284 |
hybrid_text_direct | 0.0312 | 0.3539 | 1.00 |
Examens cliniques |
hybrid_text_direct | 0.0610 | 0.3195 | 1.00 |
Imagerie |
hybrid_text_direct | 0.2305 | 0.2805 | 0.80 |
Notes médicales |
hybrid_text_direct | 0.2285 | 0.2805 | 0.80 |
Synthèse Urgences |
hybrid_text_direct | 0.2285 | 0.2805 | 0.80 |
Codage |
hybrid_text_direct | 0.1279 | 0.1641 | 0.80 |
Coller ou saisir le dossier patient |
hybrid_text_direct | 0.0630 | 0.4125 | 1.00 |
Justification de la décision |
template_matching | 0.5000 | 0.5000 | 1.00 |
→ Reproduction confirmée : trois tabs (Imagerie / Notes / Synthèse) renvoyés à coords pratiquement identiques. Le test du 10:01 (1920x1080) reproduit la même chose : Notes médicales → (0.2227, 0.1259), Imagerie → (0.2256, 0.1267), même row.
Pour reproduire en CLI sans flask :
cd /home/dom/ai/rpa_vision_v3 && source .venv/bin/activate
python -c "
from agent_v0.server_v1.resolve_engine import _resolve_by_ocr_text
img='/home/dom/ai/rpa_vision_v3/visual_workflow_builder/backend/data/anchors/anchor_0438bd2d9bdd_1778161174_full.png'
for t in ['Imagerie','Notes médicales','Synthèse Urgences','Codage','Examens cliniques']:
r = _resolve_by_ocr_text(img, t, 2560, 1600)
print(f'{t:25s} -> {r}')
"
Ce smoke test (offline, ~30 s la 1re fois pour télécharger le modèle docTR) prouve la cause #2 sans dépendre du PC Windows.
5. Trois correctifs proposés (sans appliquer)
Quick fix démo (5–10 min) — passer le client en timeout=30 pour /replay/next
Fichier : agent_v1/core/executor.py:1786 côté Windows.
Changer :
timeout=5,
en :
timeout=30,
Justification : la borne dure côté serveur est déjà 180 s par action serveur ; le serveur retourne aussi server_busy=True au plus tard à 4.5 s. Un timeout client à 30 s laisse passer un extract_text de 5-10 s + dispatch d'un click sans couper la connexion. Pas d'effet de bord majeur — au pire le client attend 30 s en cas de mort serveur, déjà couvert par la backoff.
Effet :
- Le client ne loupera plus aucun click même si extract_text/t2a_decision est devant.
- Le bug OCR-DIRECT center-of-line reste, mais
Examens cliniques(score 1.0) etCodage(autre ligne) seront correctement résolus → la maquette aiva-vision finira par s'afficher. - Imagerie/Notes/Synthèse cliqueront tous les trois sur le centre de la rangée (en pratique au-dessus d'Imagerie). C'est cosmétiquement faux mais t_imagerie/t_notes/t_synthese seront tous identiques ; il faut prévenir Amina qu'on n'aura qu'une seule lecture du DPI multi-onglets.
Risque : très bas. Modifier un seul littéral. Redéploiement SSH du fichier executor.py.
Quick fix démo bis (10–15 min) — boost OCR-DIRECT pour ne renvoyer que le centre du span matché
Fichier : agent_v0/server_v1/resolve_engine.py:1486-1519.
Idée : quand le score est 0.8 (substring match) ou 0.9 (mot exact dans la ligne), recalculer cx, cy à partir des bboxes des words qui composent le target_text, pas de la ligne entière.
Pseudo-patch (à appliquer après-démo) :
elif target_lower in line_lower:
score = 0.8
# Recalculer la bbox du span uniquement
matched_words = [w for w in line_obj.words if w.value.lower() in target_lower]
if matched_words:
xs = [pt[0] for w in matched_words for pt in w.geometry]
ys = [pt[1] for w in matched_words for pt in w.geometry]
cx = (min(xs) + max(xs)) / 2
cy = (min(ys) + max(ys)) / 2
Effet : chaque tab résolu à son propre centre, plus de collision.
Risque : moyen — il faut tester avec docTR pour vérifier que les geometry des words sont normalisées dans le même repère que celui de la ligne. Possible que le nettoyage du substring matching soit tordu par les accents/casse. À NE PAS appliquer à la chaude pour la démo, mais pour le runner 2.
Combo conseillé pour la démo : appliquer SEULEMENT le fix #1 (timeout 30 s). Le bug center-of-line tab fait que t_imagerie/t_notes/t_synthese seront tous = même contenu (Imagerie). Si Amina utilise déjà
t_imagerie ∪ t_notes ∪ t_synthesedans le prompt T2A, ça reste exploitable (juste moins de variété). Le clic Codage > marchera (autre ligne docTR).
Fix moyen terme (30–60 min) — watchdog _retry_pending côté serveur
Ajout d'une boucle background dans api_stream.py qui scanne _retry_pending toutes les 10 s et :
- Si une action a été dispatchée il y a > 30 s sans
REPORT→ la repush en tête de queue (avec un_resent=Trueflag pour stats). - Émission
[BUS] lea:dispatch_orphan_resent.
Justification : aujourd'hui une action perdue (timeout, kill client, déconnexion réseau) est perdue silencieusement. C'est un trou de fiabilité indépendamment de la démo GHT. Le watchdog garantit la reprise sans intervention manuelle.
Risque : moyen — il faut bien gérer la concurrence avec le client qui pourrait finalement renvoyer le report tardivement. Idempotence des reports déjà gérée dans report_action_result (line 3356 : _retry_pending.pop(action_id)), donc resend = réponse éventuelle ignorée.
Fix structurel (post-démo, refonte) — Server-Sent Events (SSE) ou WebSocket pour le push d'actions
Le pattern « pull avec long poll 5 s » est intrinsèquement fragile dès que les étapes serveur sont imprévisibles. Solutions architecturales :
- SSE (
text/event-stream) : connexion persistante, le serveur push chaque action quand prête. Pas de timeout client à régler. Reconnexion automatique gérée nativement par EventSource. Plus simple à implémenter que WebSocket en FastAPI. - WebSocket : full duplex, idéal pour heartbeat + actions + monitoring. Plus de code mais futur-proof.
- HTTP/2 server push + chunked responses : entre les deux. Pas standard côté requests Python.
Bénéfices :
- Suppression du bug timeout pour de bon.
_retry_pendingdevient quasi inutile (push ack-based).- Réduction du trafic (pas de poll inutile ~1/s).
- Détection immédiate de déconnexion client → déclenche pause supervisée serveur.
Coût : 1-2 jours dev + tests E2E.
6. Notes annexes (à nettoyer hors démo)
-
Anomalie d'ancrage DB : les ancres
anchor_0438bd2d9bdd_1778161174(Notes médicales label) etanchor_6a2591e7c51c_1778229076(Synthèse Urgences label) ont des bboxes pointant un cran à gauche du tab nommé. Ce n'est pas la cause du bug (mode strict + OCR-DIRECT bypass la bbox) mais c'est trompeur en debug. À reposer en VWB record session post-démo. -
target_textmal-OCRisé en DB : le champtarget_textde l'ancre Notes médicales contient"ine Né(e) le 14/03/1947 I 77 ans es Imagerie Notes médical J scan, echograj phie". C'est un OCR brut de la zone capturée — utile en debug, à ne pas confondre avec un identifiant fiable. -
Pré-check OCR post-cascade désactivé (
RPA_ENABLE_TEXT_PRECHECK=false) : pour la démo c'est OK. Mais à activer post-démo car il aurait peut-être attrapé le cas (clic sur centre de rangée = OCR autour ne voit pas exactement le by_text demandé). À recalibrer (radius_px et min_token_ratio) pour ne pas faux-rejeter sur les tabs à 2 tokens. -
Pas de
RESOLVE_ENTRYdans les logs serveur du replay live pour les tabs perdus : confirme que/resolve_targetn'est PAS appelé tant que le client n'a pas reçu l'action. Aucun chemin caché côté serveur. -
Concurrence de polls vue à 10:26:27 : deux DISPATCHes en 0.6 s pour 2 polls quasi-simultanés. C'est cohérent avec deux requêtes en attente sur l'acquire lock + une qui retourne
server_busypuis une qui acquiert. Le bug fundamental reste le timeout client trop court, pas la concurrence.
Synthèse (≤ 400 mots)
Le replay s'est bloqué non pas à cause d'un échec de résolution visuelle, mais à cause d'une désynchronisation client-serveur silencieuse.
À 10:25:48, le client Léa Windows a cliqué avec succès « Examens cliniques » et reporté success=True. Cinq secondes plus tard (10:25:53.771), il poste un nouveau GET /replay/next qui timeout à 5 s — parce que côté serveur l'appel commence par extract_text (~5–7 s pour récupérer t_examens 1689 chars) puis dispatche le click Imagerie. Le serveur a déjà poppé l'action de la queue et stocké dans _retry_pending, mais la réponse HTTP arrive après que le client ait fermé sa socket. L'action est perdue. Aucun watchdog côté serveur ne la republie. Le client repasse en mode capture cosmétique mais continue à poller. Pendant 33 s, à chaque /next il aspire de l'action serveur (extract Imagerie, dispatch Notes — perdu, extract Notes, dispatch Synthèse — perdu, extract Synthèse, t2a_decision 10 s, dispatch Codage — perdu) jusqu'à recevoir directement step 18 (« Coller ou saisir le dossier patient ») qui n'existe que sur la maquette codage.html. Échec target_not_found → pause supervisée → l'utilisateur cancel.
C'est pour ça que t_examens, t_imagerie, t_notes, t_synthese ont tous le même contenu (1689 puis 1084 chars répétés) : l'écran n'a jamais changé d'onglet ; le DPI envoyé à T2A est mutilé.
Bug aggravant indépendant : _resolve_by_ocr_text (resolve_engine.py:1447) renvoie le centre de la ligne docTR entière quand le target_text est un sous-fragment (score 0.8). docTR détecte la barre de tabs comme une ligne unique → Imagerie/Notes/Synthèse renvoient tous (0.23, 0.28). Confirmé par le test e2e_singleshot du même jour à 09:58. Même si le client recevait les actions, le clic raterait la cible. Latent dès que plusieurs tokens cibles partagent la même ligne docTR.
Recommandation démo : passer le read_timeout client de 5 s à 30 s (agent_v1/core/executor.py:1786). Quick win, zéro risque, suffit pour que le pipeline aboutisse à codage.html et que la maquette aiva-vision se remplisse. Accepter pour la démo que les 3 tabs Imagerie/Notes/Synthèse cliqueront tous au centre de la rangée (le DPI multi-onglets sera dégradé mais le t2a_decision restera exploitable car t_motif et t_motif_bas portent l'essentiel du diagnostic).
Priorité post-démo : (1) watchdog _retry_pending, (2) fix OCR-DIRECT center-of-span, (3) refonte SSE/WebSocket.