Files
rpa_vision_v3/docs/AUDIT_WINDOW_TITLE_MEMORY_PATH_2026-05-19.md

11 KiB

Audit — Perte de window_title dans le pipeline mémoire

Date : 2026-05-19 Mission : Claude 3 (lecture seule) Périmètre : agent_v0/server_v1/stream_processor.py, api_stream.py, replay_engine.py, workflow_replay.py, replay_memory.py Branche : backup/post-demo-2026-05-19 (HEAD 5ea4960e6) Référence : blocage P0 #3 de docs/AUDIT_LEA_FIRST_RUNTIME_2026-05-19.md

Constat

La mémoire persistante (TargetMemoryStore) repose sur une signature d'écran sha256(window_title) (cf. replay_memory.py:94-103). Sans window_title, la signature est vide → ni memory_lookup ni memory_record_* ne se déclenchent. Le code utilise try/except permissif et un if _window_title silencieux — aucun signal n'est émis quand la mémoire est skip.

L'audit identifie deux problèmes simultanés :

  1. Asymétrie écriture/lecture : plusieurs chemins de production écrivent window_title sur l'action top-level (action["window_title"] ou action["expected_window_before"]), mais la lecture mémoire (api_stream.py:3634-3639) cherche uniquement dans target_spec. Conséquence : la fallback or _mem_target_spec.get("expected_window_before", "") ne peut jamais réussir car ce champ n'est posé que sur l'action top-level.

  2. Producteurs incomplets : plusieurs constructeurs de target_spec n'injectent pas window_title même quand l'information est disponible dans le contexte.

Résultat net : sur le chemin Léa-first natif (capture → workflow construit par build_replay_from_raw_events), la mémoire ne se déclenche jamais bien que window_title soit présent sur l'action.

Tableau — cartographie des chemins

Producteurs target_spec / actions click

Fichier Fonction / site Chemin window_title dans target_spec Impact mémoire
stream_processor.py:1532-1601 build_replay_from_raw_events (raw events Léa → actions) Léa-first natif JAMAIS posé dans target_spec — écrit top-level ligne 1545 🔴 lookup + record toujours skip
stream_processor.py:1590-1601 branche enrich post-Léa (anchor + window_capture) Léa-first natif non — propage enrichment (by_text, anchor) + window_capture.rect, jamais window_title 🔴 même chemin que 1545
stream_processor.py:4396-4443 _create_edge_action (worker VLM offline, GraphBuilder edges) workflow construit hors session OK ligne 4402 : if window_title: target_spec['window_title'] = window_title 🟢 mémoire active si node metadata contient window_title
replay_engine.py:534-548 _generate_setup_env_actions clic Démarrer replay-session bootstrap AUCUN window_title posé (légitime : fenêtre Bureau Windows) 🟡 dette assumée — clic système
replay_engine.py:563-578 _generate_setup_env_actions clic Rechercher replay-session bootstrap AUCUN window_title posé (légitime : menu Démarrer) 🟡 dette assumée
replay_engine.py:611-625 _generate_setup_env_actions clic résultat app replay-session bootstrap AUCUN window_title posé (résultats recherche volatils) 🟡 dette assumée
replay_engine.py:966-979 _normalize_action (action depuis objet Target) normalisation chemin Target API by_role, by_text, context_hints posés — PAS window_title 🔴 lookup + record skip
replay_engine.py:1804-1807 _create_replay_state slim copy tous chemins (post-construction) conserve si présent ; strip uniquement anchor_image_base64 🟢 transparent
workflow_replay.py:119-128 build_workflow_replay (orphelin) branche non branchée OK ligne 123 : "window_title": node_title posé correctement code mort, 0 caller runtime

Lecteurs window_title (sites memory)

Fichier Fonction / site Cherche dans Statut
replay_memory.py:142-206 memory_lookup target_spec.get("window_title", "") 🔴 silencieusement skip si vide
api_stream.py:3634-3639 memory_record_success/failure source _mem_target_spec.get("window_title", "") puis _mem_target_spec.get("expected_window_before", "") 🔴 deuxième fallback inopéranteexpected_window_before n'est jamais dans target_spec
resolve_engine.py:1541 déclenche memory_lookup target_spec.get("window_title", "") 🔴 propagation du silence
api_stream.py:3278-3281 log REPLAY (observabilité) action.get("expected_window_before") or _tspec.get("window_title", "") 🟢 correct — preuve que les 2 endroits existent et qu'au moins ce code le sait
api_stream.py:3599 audit_trail target_app _target_spec.get("window_title", "") 🔴 audit incomplet sur chemins où window_title est top-level
stream_processor.py:1149, 1176 _enrich_actions_with_intentions (user-facing) action.get("target_spec", {}).get("window_title", "") 🟡 affiche '?' ou 'inconnue' quand absent

Producteurs top-level action["window_title"] ou expected_window_before

Fichier Site Champ posé Présent dans target_spec ?
stream_processor.py:1545 build_replay_from_raw_events action["window_title"] = window["title"] non
replay_engine.py:1797-1798 _create_replay_state slim copy a_copy["expected_window_before"], a_copy["expected_window_title"] non

Cas critiques (chemins où la mémoire skip silencieusement)

Cas A — Session Léa fraîchement enregistrée, workflow direct

Trigger : utilisateur enregistre une session, finalize enqueue, build_replay_from_raw_events produit les actions. Chemin : stream_processor.py:1532-1601. Symptôme : action["window_title"] est posé au top-level (ligne 1545), mais le target_spec (lignes 1590-1601) ne contient que enrichment + window_capture. Au replay, memory_record_success lit _mem_target_spec.get("window_title", "") → vide → compute_screen_sig"" → skip silencieux. Conséquence produit : la voie nominale Léa-first n'alimente jamais la mémoire. Aucune leçon stockée.

Cas B — Action via API Target normalisé

Trigger : un client appelle une API qui passe par _normalize_action (replay_engine.py:966). Chemin : replay_engine.py:966-979. Symptôme : target_spec construit avec by_role, by_text, context_hints uniquement. window_title jamais posé même si disponible dans le contexte appelant. Conséquence produit : tout client qui passe par cette API perd la mémoire.

Cas C — Workflow setup_env autogénéré (ouvrir une app via Démarrer)

Trigger : un workflow démarre par ouvrir une app, génération automatique de 3 clics (Démarrer → Recherche → Résultat). Chemin : replay_engine.py:534-625. Symptôme : aucun des 3 clics n'a window_title dans target_spec. C'est intentionnel (fenêtres système volatiles), mais la mémoire ne s'active pas non plus sur ces clics. Conséquence produit : ces 3 clics ne bénéficieront jamais de l'apprentissage par répétition, alors qu'ils sont parmi les plus stables visuellement (bouton Démarrer toujours en bas-gauche).

Cas D — Fallback expected_window_before codée mais inopérante

Trigger : action a expected_window_before posé top-level (par _create_replay_state ou un workflow VWB qui le renseigne). Chemin : api_stream.py:3634-3639. Symptôme : le code tente le fallback _mem_target_spec.get("expected_window_before", ""). Mais _mem_target_spec est l'objet target_spec ; expected_window_before n'est jamais dans target_spec, il est sur action top-level (cf. replay_engine.py:1797). La fallback est toujours vide. Conséquence produit : même quand l'information existe au top-level de l'action, le memory_record ne la voit pas. Preuve qu'il existait une intention de cohérence : api_stream.py:3278-3281 (log REPLAY) lit correctement action.get("expected_window_before") or _tspec.get("window_title"). Le code mémoire a copié la mauvaise moitié de l'expression.

Cas E — Workflow VWB édité à la main sans window_title

Trigger : workflow construit/édité dans VWB (table steps), target_spec.window_title souvent omis. Chemin : consommé tel quel par _create_replay_state. Symptôme : aucun warning, aucune erreur, aucun signal. La mémoire reste vide pour ce workflow. Conséquence produit : sur Demo_urgence_3_db (46 steps), à vérifier combien de steps ont effectivement target_spec.window_title non vide — probablement minoritaire.

Conclusion

Bug réel (P0) :

  • Cas Dapi_stream.py:3634-3639 : la fallback or _mem_target_spec.get("expected_window_before", "") cherche dans target_spec au lieu de l'action top-level. Code mort par erreur de copy-paste depuis api_stream.py:3278-3281 qui faisait correctement action.get(...) or _tspec.get(...). Une ligne à corriger une fois la décision produit prise.
  • Cas Astream_processor.py:1545 vs :1590-1601 : asymétrie de contrat dans le même fichier, sur le chemin Léa-first nominal. window_title est connu (posé top-level) mais non propagé dans target_spec où la mémoire le cherche.

Dette de contrat (P1) :

  • Cas Breplay_engine.py:966-979 : _normalize_action n'inclut pas window_title dans target_spec. Contrat implicite "tous les producteurs de target_spec doivent injecter window_title quand disponible" non documenté ni appliqué.
  • Cas E — workflows VWB sans window_title : pas de validation côté serveur, pas de warning. La forme du contrat n'est jamais vérifiée à la création.
  • Cas C — clics setup_env : exclusion légitime mais devrait être documentée. Une mémoire "setup_env" pourrait utiliser une signature d'écran différente (cf. replay_learner.py:213 qui utilise "human_correction" comme signature constante — pattern réutilisable).

Branche non branchée :

  • workflow_replay.py:119-128 : code mort qui pose pourtant window_title correctement. Cohérent avec son orphelinat global (blocage P0 #2 de l'audit Léa-first). Pas de valeur tant qu'il n'est pas câblé.

Synthèse 1-ligne : la mémoire est branchée mais le contrat window_title in target_spec n'est respecté que par le chemin GraphBuilder (worker VLM offline) ; le chemin Léa-first nominal et la normalisation API perdent l'info, et la fallback prévue pour rattraper est inopérante par bug de lecture. Le résultat observable : TargetMemoryStore reste vide sur les sessions Léa réelles.

Méthode d'audit

  • Grep cibles : target_spec.*=, "window_title", window_title=, "type": "click" sur les 5 fichiers du périmètre.
  • Lectures ciblées : stream_processor.py:1140-1180, 1530-1605, 4380-4445, replay_engine.py:525-625, 955-980, 1780-1810, api_stream.py:3270-3290, 3540-3670, workflow_replay.py (intégral), replay_memory.py (intégral).
  • Croisement écritures/lectures pour identifier les asymétries.
  • Pas de modification de code, pas d'exploration VWB, pas de proposition de refonte — conformément aux interdits de la mission.