16 KiB
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 :
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 :
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 :
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 :
$ 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 (tablestepsDB), 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 :
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 :
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 :
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 :
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 siself.session_id(= recording actif)_background_heartbeat_loop(ligne 349) — actif en permanence, pousse sousbg_<machine_id>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) renvoietotal_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 :
- #4 d'abord — fixer
mss.monitors[1]aveugle. Sinon tout ce qu'on stocke après est faux. - #3 ensuite — exiger ou dériver
window_titledans letarget_specà l'enregistrement Léa (la capture client a déjà cette info viawindow_capture.title, à propager). Sans ça, la mémoire reste vide. - #1 — corriger
record_human_correction(import + signature). Ouvre la boucle d'apprentissage supervisé. - #2 + #8 ensemble — soit rebrancher
build_workflow_replayau 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