docs: add POC specs, handoffs, and research notes
This commit is contained in:
320
docs/AUDIT_LEA_FIRST_RUNTIME_2026-05-19.md
Normal file
320
docs/AUDIT_LEA_FIRST_RUNTIME_2026-05-19.md
Normal 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`
|
||||
Reference in New Issue
Block a user