docs: add POC specs, handoffs, and research notes

This commit is contained in:
Dom
2026-06-02 16:28:34 +02:00
parent 18ed6cb751
commit f2e9aac6b7
86 changed files with 27615 additions and 25 deletions

View File

@@ -0,0 +1,320 @@
# 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_<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`) 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`