# nyora-notes-tt : elem_id OWA VOLATILE — le message_id natif par-message n'existe pas via scraping DOM **Date** : 08/08/2026 **Instance** : hermes-tt **Statut** : PISTE FERMÉE (elem_id abandonné) — conversation_id (data-convid) + fix retry /search conservés **Fichier** : `/volume1/docker/nyora-notes-tt/hermes_mail_client.py` (`_parse_item`, `search_folder`) ## Objectif initial Remplacer le `message_id` du pipeline mail RAG (jusqu'ici `msg_`) par un identifiant **natif par-message** stable, pour un ancrage propre (dédup, liaison RAG). `hermes-mail-browser` avait été patché pour exposer sur `/search` deux attributs DOM OWA en plus de `raw_text`/`aria_label` : `elem_id` (attribut `id` de la ligne) et `attrs` (string pipe-délimitée contenant notamment `data-convid=…`). ## Le faux positif (à ne pas reproduire) Un premier « test de stabilité » avait conclu que `elem_id` était **stable et unique par message** (« vérifié sur 3 appels /search séparés, requêtes différentes, même message → même elem_id »), avec un exemple de valeur **base64** type `AQAAAAAAAQwCAAAFYk58JQAAAAA=`. **Ce test était insuffisant** : un seul message, toujours en **position 0** de la liste, sur des appels **rapprochés**. OWA (SPA React) ne re-rend pas forcément la liste virtualisée entre deux appels rapprochés → le même DOM (donc le même `id` volatile) est réutilisé → **stabilité illusoire**. C'est le piège classique du test à un seul échantillon. ## Le test rigoureux (qui infirme) Deux appels `/search` **query identique** (`q="*"`), **même dossier** (`02 2024|DR Zone Sud|Tozeur`, 20 messages), appariement des messages par `raw_text` (identité stable pour du mail 2024) : | Mesure | Résultat | |---|---| | `elem_id` identique pour le même message entre 2 appels | **0 / 19** | | Recouvrement des ensembles d'`elem_id` (\|A ∩ B\|) | **0** (\|A\|=17, \|B\|=17, disjoints) | | Même **index** → même `elem_id` | **0 / 20** | | Même index → même `raw_text` (ordre de liste) | **20 / 20** (ordre stable) | | `aria-posinset` dans `attrs` | **toujours 0** (non peuplé par cette build OWA) | | `data-convid` (conversation_id) entre appels | **stable** (même ConversationId Exchange) | ## Cause `elem_id` = **id de rendu DOM volatile** : OWA/React génère un **GUID frais à chaque rendu** (format `1da682c2-7ada-f6a1-4cef-83fe279a6bcd`, ≠ le base64 du faux positif → l'attribut/la build a changé). Il **n'encode ni l'identité du message ni sa position** (l'ordre de liste est pourtant stable, mais l'id ne l'est pas ; `aria-posinset` reste à 0). **Inutilisable comme `message_id`.** Le seul identifiant natif stable exposé au niveau **ligne** est `data-convid` — mais il est de **niveau THREAD** (plusieurs mails d'un même fil le partagent), pas par-message. → Conclusion : **aucun identifiant natif par-message stable n'est disponible via le scraping des attributs de ligne OWA** dans l'état actuel. Cohérent avec la conclusion générale « les handles du SPA OWA ne sont pas stables ». Un vrai ItemId Exchange par-message nécessiterait une autre voie (ouverture du message, Graph API, autre attribut DOM) — hors périmètre, non fait. ## Décision finale (appliquée le 08/08/2026) 1. **`message_id` reste le hash SHA256** `msg_` — déterministe et STABLE (le hash n'était pas cassé, juste « non natif »). La branche `elem_id` est **retirée entièrement** (pas juste mise en fallback : `elem_id` n'est jamais falsy, le fallback `if not native_id` ne se serait jamais déclenché → il aurait toujours servi l'id volatile). Au passage, l'ancien code mort `item.get("id")` (attribut jamais renvoyé par le bridge) est supprimé aussi. 2. **`conversation_id` conservé** : extrait de `attrs` par **regex ciblée** `r'data-convid=([^|]+)'`. **NE JAMAIS** faire de `split('|')` global sur `attrs` : `aria-label` y est réinjecté en fin de chaîne, texte libre non échappé pouvant contenir des `|` et des sauts de ligne. `data-convid` est du base64 sans `|`, donc `[^|]+` est sûr même si le reste est pollué. (Non persisté pour l'instant : la table `notes` n'a pas de colonne `conversation_id` — enhancement futur possible.) 3. **Fix fiabilité `/search` conservé** : voir section suivante. ## Bug annexe corrigé : /search count:0 transitoire Deux appels `/search` à < ~10 s d'écart : le 2ᵉ renvoie parfois `count:0` (liste virtualisée OWA pas encore stabilisée côté Playwright). Fix dans `search_folder` : si `items` est vide de façon inattendue, **retry unique après `time.sleep(4)`** avant d'abandonner (simple, pas de backoff). ## Garde-fou dédup (vérifié — pas de doublons) - `message_id` **EST** la clé de dédup à l'ingestion (`pipeline_processor.py`, `SELECT id FROM notes WHERE message_id = ?`). - Au moment du chantier : **399 notes, 399 au format `msg_`**, toutes dans des dossiers de checkpoint **`done`**. `pending_folders` (`mcp_server.py`) ne resélectionne jamais un dossier `done` → **pas de re-scan → pas de risque de doublon**. Les checkpoints non-`done` (138 `error_404`, 5 `error_409`) ont 0 note. - Fenêtre de risque « code elem_id déployé » : le conteneur a tourné le code dégradé (message_id = id volatile) entre deux rebuilds. Vérifié après coup : **0 note insérée** dans cette fenêtre (toujours 399, aucune au format non-hash) → aucun nettoyage nécessaire. - **Caveat** documenté : si un jour un checkpoint `done` est remis manuellement en `pending`, les anciennes notes ne seraient pas reconnues par la dédup `message_id` et retomberaient sur le filet vectoriel `find_duplicate_in_folder` (distance L2 < 0.05 + sujet identique → `duplicate_count++`). ## Voir aussi - `hermes-tt/mail-o365-recherche-scroll-virtualisation.md` — virtualisation de la liste OWA (même origine que le count:0 transitoire). - `hermes-tt/mail-o365-hermes-mail-browser-inbox-staleness.md` — état de session OWA / staleness. - `hermes-tt/mail-o365-16-dossiers-error-etat-session-owa.md` — 502 « Racine du compte introuvable dans le panneau de dossiers » = état OWA dégradé → clear cache + `docker restart hermes-mail-browser`. ## Leçon transférable Un attribut DOM `id=`/`data-*` d'un SPA n'est PAS présumé stable. Le tester sur **≥ 2 rendus réels distincts** (appels espacés OU requêtes forçant un re-render), sur **plusieurs messages**, en appariant par une identité externe stable (`raw_text`), et vérifier le **recouvrement d'ensembles** — pas un seul échantillon en position fixe. Ajouté à `PROTOCOL-INFRA.md`.