# Audit runtime Léa-first — `capture → replay direct → memory` **Date** : 2026-05-19 **Auteur** : Claude (mission audit n°1, lecture seule) **Périmètre** : `agent_v0/agent_v1/`, `agent_v0/server_v1/`, `core/learning/` **Branche** : `backup/post-demo-2026-05-19` (HEAD `5ea4960e6`) **Objectif** : identifier 5-10 blocages concrets qui empêchent la voie nominale `capture → replay direct → memory` d'être fiable. **Pas de VWB, pas de démo, pas de bench modèles, pas de refonte large.** --- ## Verdict global La voie nominale **existe partiellement en code** mais comporte 3 ruptures fonctionnelles (P0) et 4 dégradations silencieuses (P1) qui la rendent **non fiable en pratique**. Le contournement actuel = passer par VWB pour fabriquer un workflow réutilisable, ou par le worker VLM offline. Pas de boucle "Léa capture → Léa rejoue" directe. État composant par composant : | Composant | État réel | |---|---| | Capture événements client (`captor.py`, `vision/capturer.py`) | mature, production-grade, bug coord critique | | Streaming vers serveur (`streamer.py`) | mature, robuste (retry, buffer SQLite, backpressure) | | Accumulation côté serveur (`live_session_manager.py`) | OK, `to_raw_session` câblé au worker VLM | | Construction workflow depuis session Léa (`workflow_replay.py`) | **ORPHELIN** (0 caller runtime) | | Replay direct sans VWB | **N'EXISTE PAS** (replay actuel consomme workflow VWB) | | Memory lookup (resolve_engine + replay_memory) | branché, **gated silencieusement** sur `window_title` | | Memory record_success / failure | branché, même gating | | Memory record_human_correction (apprentissage supervisé) | **CASSÉ** (double bug) | | `core/learning/*` (continuous_learner, feedback_processor, learning_manager) | **NON BRANCHÉ** au runtime serveur Léa-first | | Observabilité mémoire | **AVEUGLE** (logs only, aucun endpoint) | --- ## P0 — Ruptures (la voie nominale ne marche pas) ### Blocage #1 — `record_human_correction` cassé, double bug **Fichier** : `agent_v0/server_v1/replay_learner.py:210-219` **Fonction** : `ReplayLearner.record_human_correction()` **Bug A — import inexistant** : ```python from .replay_memory import get_target_memory_store store = get_target_memory_store() ``` La fonction `get_target_memory_store` **n'existe pas** dans `replay_memory.py`. La vraie s'appelle `get_memory_store` (`replay_memory.py:47`). Le `try/except` à la ligne 224 avale silencieusement l'`ImportError`. **Aucune trace dans les logs au niveau INFO.** **Bug B — signature obsolète** : ```python store.record_success( screen_signature="human_correction", target_spec=target_spec, resolved_position={"x_pct": x_pct, "y_pct": y_pct}, method="human_supervised", score=1.0, ) ``` La vraie signature (`core/learning/target_memory_store.py:212-219`) attend : ```python def record_success( self, screen_signature: str, target_spec, fingerprint: TargetFingerprint, strategy_used: str, confidence: float, ) ``` Les paramètres `resolved_position`, `method`, `score` **n'existent pas**. `TypeError` garanti si bug A est fixé sans fixer B. **Impact produit** : l'apprentissage par correction humaine — la boucle "Léa apprend en regardant l'humain corriger" — est **totalement inopérant**. La correction est juste loguée en JSONL local (`record()` ligne 206), jamais hoistée dans la mémoire persistante consultée au prochain run. **Gravité** : P0 **Catégorie** : bug réel (double) --- ### Blocage #2 — `build_workflow_replay` orphelin (pas de pont capture → replay direct) **Fichier** : `agent_v0/server_v1/workflow_replay.py:29-186` **Fonction** : `build_workflow_replay()` **Constat** : ```bash $ grep -rn "build_workflow_replay" --include="*.py" | grep -v "workflow_replay.py:" # (vide) ``` 0 caller runtime. Le code décrit pourtant exactement le pont attendu (workflow enrichi → actions de replay avec vérification FAISS par node), mais il n'est appelé **nulle part**. **Ce qui marche aujourd'hui à la place** : - `api_stream.py:1479-1525` (`POST /finalize`) → enqueue session au worker VLM (process séparé) - Le worker construit un workflow via `GraphBuilder` (cf. `stream_processor.py:2306-2335`) - Mais **rien ne renvoie ces actions à un replay direct**. Le replay (`replay_engine.py`) consomme un workflow VWB (table `steps` DB), pas une séquence construite à partir d'une session Léa. **Impact produit** : pas de chemin "Léa enregistre → on rejoue la session telle quelle". Toute session Léa doit transiter par VWB ou un commit DB manuel pour devenir rejouable. **Gravité** : P0 **Catégorie** : branche non branchée (code mort) --- ### Blocage #3 — Memory gated sur `target_spec.window_title` silencieusement inopérante **Fichiers** : - `agent_v0/server_v1/resolve_engine.py:1541-1554` (lookup) - `agent_v0/server_v1/api_stream.py:3634-3639, 3666-3672` (record) **Bug structurel** : la signature d'écran V4 = `sha256(normalize(window_title))[:16]` (cf. `replay_memory.py:94-103`). Si `target_spec.window_title` est vide ou absent : ```python def compute_screen_sig(window_title: str) -> str: norm = _norm_text(window_title) if not norm: return "" # → memory_lookup/record skip silencieux return hashlib.sha256(norm.encode("utf-8")).hexdigest()[:16] ``` **Conséquence runtime** : sur les workflows édités à la main dans VWB ou construits sans renseigner `window_title` (cas dominant aujourd'hui), `screen_sig=""` → **ni lookup ni record déclenchés**. Pas de log d'erreur, pas de signal. La mémoire reste vide pendant des semaines sans alerter. **Validation** : sur le workflow Demo_urgence_3_db, beaucoup de steps ont `target_spec` sans `window_title` (anchors ciblés par `by_text`). Vérifiable rapidement par : ```bash sqlite3 visual_workflow_builder/backend/instance/workflows.db \ "SELECT json_extract(parameters_json, '$.target_spec.window_title') FROM steps WHERE workflow_id='wf_483910cdd851_1778750587';" ``` **Impact produit** : la mémoire persistante peut paraître branchée (singleton init OK, JSONL/SQLite créés) et **ne stocker aucune entrée** sur les workflows réels. **Gravité** : P0 **Catégorie** : dette (gating sur condition fragile) --- ### Blocage #4 — `mss.monitors[1]` aveugle aux dims aberrantes (corruption en amont) **Fichier** : `agent_v0/agent_v1/vision/capturer.py` **Sites** : `:107` (`capture_full_context`), `:150` (`capture_dual`), `:247` (`capture_active_window`) **Code commun** : ```python with mss.mss() as sct: monitor = sct.monitors[1] sct_img = sct.grab(monitor) img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX") ``` **Bug observé en démo (19 mai)** : `mss.monitors[1]` retourne intermittemment `{width: 2560, height: 60}` au lieu de `{width: 2560, height: 1600}` → coords `y_pct × 60 = 16 px` au lieu de `y_pct × 1600 = 424 px`. Aucune défense dans le code. **Impact produit** : toute capture servant de référence à la mémoire peut être corrompue. Un fingerprint enregistré avec `y_pct = 0.0099` au lieu de `0.265` **empoisonne le store** : au prochain hit, Léa clique à 16 px du haut au lieu du bon endroit. Et le `fail_count` augmente sans que la cause soit visible. **Fix attendu** (lecture seule, donc juste indiqué) : refuser la capture si `monitor.height < 200` (ou autre seuil sain), fallback sur autre monitor ou nouvelle tentative. **Gravité** : P0 **Catégorie** : bug réel --- ## P1 — Dégradations silencieuses (la voie marche mais fausse) ### Blocage #5 — Captures Léa downscalées 800×500 envoyées au serveur **Fichier** : `agent_v0/agent_v1/core/executor.py:2895` **Défaut** : `_capture_screenshot_b64(max_width=800, quality=60)` **7 sites d'appel sans override** : `:633, :801, :824, :894, :935, :989, :1055, :1303`. Seuls `:1334` et `:2147` (resolve_target) passent `max_width=0` (full-res). **Impact produit** : - Matching template au serveur reçoit du 800×500 → compensé par multi-scale étendu côté serveur (`resolve_engine.py:130`, fix du 18 mai) - **Mais** les coords stockées dans la mémoire dépendent du chemin de projection (full-res vs downscale). Bruit imprécis sur le store. **Gravité** : P1 (workaround serveur tient, base mémoire bruitée) **Catégorie** : dette --- ### Blocage #6 — `_replay_active` flag mal géré pendant les pauses **Fichier** : `agent_v0/agent_v1/main.py:319-345` **Code problématique** : ```python if had_action: if not self._replay_active: self._replay_active = True ... else: if self._replay_active: print("[REPLAY] Replay termine — retour en mode capture") self._replay_active = False ``` **Bug** : si le serveur renvoie `action=null + replay_paused=true` (attente humaine), `had_action=False` → Léa interprète "fin du replay" → cleanup UI + bulle paused n'apparaît plus. Comportement déjà observé en démo (cf. handoff 19 mai bug P1 "Léa client interprète action=null + replay_paused=true comme fin du replay"). **Impact produit** : tracking de replay corrompu côté client pendant les pauses. Désaligne aussi le `ChatWindow` (bulle paused invisible après plusieurs replays). **Gravité** : P1 **Catégorie** : bug réel --- ### Blocage #7 — `core/learning/*` Phase 7 non branché au runtime serveur Léa **Fichiers** : - `core/learning/continuous_learner.py` (644 lignes) - `core/learning/feedback_processor.py` (176 lignes) - `core/learning/learning_manager.py` (180 lignes) - `core/learning/versioned_store.py` (592 lignes) **Consumers réels** (grep `from core.learning`) : | Caller | Statut | |---|---| | `core/execution/execution_loop.py:49, 71` | runtime alternatif, pas le serveur Léa | | `core/pipeline/workflow_pipeline.py:29` | pipeline batch GUI legacy | | `gui/orchestrator.py:52` | GUI PyQt5 legacy | | `visual_workflow_builder/backend/services/learning_integration.py:36` | service VWB | | `examples/test_phase7_*.py` | exemples | | `tests/unit/test_versioned_store.py` | tests | | `tests/test_correction_pack_integration.py` | tests | **Le runtime serveur Léa-first** (`api_stream.py` + `replay_engine.py` + `resolve_engine.py` + `stream_processor.py`) n'instancie **rien** de tout ça. Seul `TargetMemoryStore` est consommé via `replay_memory.py`. **Impact produit** : - Drift detection (`ContinuousLearner`) = mort en flux Léa-first - Versioned prototypes (`VersionedStore`) = morts - Retraitement feedback bus (`FeedbackProcessor`) = mort - Stats globales (`LearningManager`) = mortes **Gravité** : P1 **Catégorie** : branche non branchée --- ### Blocage #8 — Pas de signal "session enregistrée → workflow rejouable" exposé côté API **Fichier** : `agent_v0/server_v1/api_stream.py:1479-1525` (`POST /api/v1/traces/stream/finalize`) **Constat** : `finalize` marque la session, enqueue au worker VLM (`_enqueue_to_worker(session_id)`), rend la main. **Aucun endpoint** : - Pour savoir quand le workflow construit est prêt - Pour le déclencher en replay direct sur une cible (machine, agent) - Pour récupérer la liste des "workflows construits par Léa" disponibles La séquence "Léa fais ça → maintenant Léa, rejoue ça" n'a pas de surface API exposée — elle passe **implicitement** par VWB (qui lit les workflows en DB et orchestre). **Impact produit** : la voie nominale "capture → replay direct" n'a pas de point d'entrée client. C'est cohérent avec le blocage #2 (`build_workflow_replay` orphelin) : le pont produit n'existe pas non plus côté API. **Gravité** : P1 **Catégorie** : branche non branchée (au niveau orchestration) --- ## P2 — Bruit et observabilité ### Blocage #9 — Deux boucles heartbeat parallèles, persistance non scoped **Fichier** : `agent_v0/agent_v1/main.py:131, 349-393` **Constat** : 2 boucles tournent : - `_heartbeat_loop` (ligne 434) — actif seulement si `self.session_id` (= recording actif) - `_background_heartbeat_loop` (ligne 349) — actif **en permanence**, pousse sous `bg_` toutes les 5s même sans session Le serveur persiste ces sessions `bg_*` dans `data/streaming_sessions/`. Pas de purge automatique scoped (la purge générale tourne sur les sessions finalisées > 24h, mais `bg_*` ne se finalise jamais). **Impact produit** : pollution disque indépendante de l'usage. Croissance non maîtrisée. Bruit dans toute analyse a posteriori des sessions Léa réelles. **Gravité** : P2 **Catégorie** : dette --- ### Blocage #10 — Aucune métrique runtime sur la mémoire **Fichiers** : - `agent_v0/server_v1/replay_memory.py` (`memory_lookup`, `memory_record_*`) - `core/learning/target_memory_store.py` (`get_stats`) **Constat** : - Les hits/misses sont seulement `logger.info` (`replay_memory.py:191-196`). - `TargetMemoryStore.get_stats()` (`target_memory_store.py:440-479`) renvoie `total_entries, total_successes, total_failures, overall_confidence, jsonl_files_count, jsonl_total_size_mb` — **mais n'est branché à aucune route API**. - Pas de compteur Prometheus, pas d'endpoint `/api/v1/memory/stats`, pas de surface dashboard. **Impact produit** : impossible de répondre en runtime à "la mémoire travaille-t-elle aujourd'hui ?" ou "combien d'entrées sur ce workflow ?" sans grepper les logs ou ouvrir la DB SQLite à la main. Debugging et validation Léa-first **à l'aveugle**. **Gravité** : P2 **Catégorie** : dette (observabilité) --- ## Tableau récapitulatif | # | Sévérité | Catégorie | Fichier:fonction | 1-line | |---|---|---|---|---| | 1 | P0 | bug | `replay_learner.py:210` `record_human_correction` | Import inexistant + signature obsolète, apprentissage humain mort | | 2 | P0 | branche non branchée | `workflow_replay.py:29` `build_workflow_replay` | Orphelin, pas de pont capture→replay direct | | 3 | P0 | dette | `resolve_engine.py:1541` + `api_stream.py:3634` | Memory gated sur `window_title` souvent absent, silencieusement morte | | 4 | P0 | bug | `vision/capturer.py:107,150,247` | `mss.monitors[1]` aveugle, base mémoire empoisonnée | | 5 | P1 | dette | `executor.py:2895` (7 sites) | Captures 800×500 par défaut, store bruité | | 6 | P1 | bug | `main.py:319-345` `_replay_poll_loop` | `_replay_active` mal géré pendant pause, état UI désynchro | | 7 | P1 | branche non branchée | `core/learning/*` | Phase 7 non branchée au runtime serveur | | 8 | P1 | branche non branchée | `api_stream.py:1479` `/finalize` | Pas d'API "rejoue ce que tu viens d'enregistrer" | | 9 | P2 | dette | `main.py:131,349` | Heartbeat background pollue la persistance | | 10 | P2 | dette | `replay_memory.py` + `target_memory_store.py:440` | Aucune métrique runtime mémoire exposée | --- ## Recommandation de séquencement (si on devait choisir 4 fixes) Pour rendre la voie nominale `capture → replay direct → memory` opérationnelle avec un effort minimal : 1. **#4** d'abord — fixer `mss.monitors[1]` aveugle. Sinon tout ce qu'on stocke après est faux. 2. **#3** ensuite — exiger ou dériver `window_title` dans le `target_spec` à l'enregistrement Léa (la capture client a déjà cette info via `window_capture.title`, à propager). Sans ça, la mémoire reste vide. 3. **#1** — corriger `record_human_correction` (import + signature). Ouvre la boucle d'apprentissage supervisé. 4. **#2** + **#8** ensemble — soit rebrancher `build_workflow_replay` au worker VLM et exposer un endpoint client, soit assumer que VWB reste l'orchestrateur intermédiaire. Décision produit à arbitrer. **Pas dans le périmètre de cette mission** : proposer le design des fixes (la mission demandait l'audit, pas la refonte). --- ## Méthode d'audit - Lectures intégrales : `core/learning/__init__.py`, `core/learning/target_memory_store.py`, `replay_memory.py`, `replay_learner.py`, `live_session_manager.py`, `workflow_replay.py`, `core/captor.py`, `vision/capturer.py`, `network/streamer.py`, `main.py` - Lectures ciblées : `api_stream.py:1479-1525, 3600-3690`, `stream_processor.py:1700-1745, 2285-2345`, `resolve_engine.py:1525-1565` - Grep consumers : `build_workflow_replay`, `memory_lookup`, `memory_record_*`, `ReplayLearner`, `record_human_correction`, `to_raw_session`, `TargetMemoryStore(`, `ShadowLearningHook`, `from core.learning` - Croisement avec : handoffs 12-19 mai, `DETTE_TECHNIQUE.md`, `AUDIT_CONTROLES_DEBRANCHES_2026-05-08.md`