# nyora-notes-tt / download_attachments_phase1.py -- timeout traite comme "vide" (07/08/2026) **Instance auteur** : hermes-tt (diagnostic + fix Claude, session chantier PJ mail O365 2026) **Date** : 2026-08-07 **Tags** : nyora-notes-tt, hermes-mail-browser, owa, timeout, attachments **Statut** : valide --- ## Probleme Le sweep des PJ 2026 (`download_attachments_phase1.py`) degradait progressivement la session OWA partagee au fil du run (timeouts croissants), jusqu'a marquer des notes `attachments_scanned=1` (traitees, 0 PJ) alors qu'elles n'avaient jamais ete reellement verifiees. Symptome observe en test direct : sur un lot de 5 notes, 2 ont declenche `Erreur listing PJ index N : timed out` sans que le run ne s'en trouve affecte -- les 5 notes finissaient quand meme marquees scannees. --- ## Contexte et contraintes `download_attachments_phase1.py` boucle sur les notes `attachments_scanned=0`, triees par `folder_path`. Pour chaque dossier non encore vu, un seul appel `search_folder()` (cache en memoire pour tout le run) resout l'index courant des messages ; `list_attachments()` va ensuite chercher la liste des PJ pour le message matche. Les deux appels HTTP passent par `hermes_mail_client.py`, qui pilote hermes-mail-browser (Edge reel via CDP sur session OWA partagee). Code source a `/volume1/docker/nyora-notes-tt/`, **cuit dans l'image** (`COPY . .` dans le Dockerfile, pas de volume mount sur les `.py`) -- toute modif exige rebuild + recreate. --- ## Ce qui NE fonctionnait PAS | Mecanisme | Comportement observe | Raison | |-----------|----------------------|--------| | `search_folder()` : `except HermesMailClientError` generique dans `process_note()` | Un timeout (60s) sur le dossier etait catche exactement comme un 404 (dossier reellement introuvable) -- `folder_items_cache[folder_path] = []` | `HermesFolderNotFoundError` (404) et `HermesFolderAmbiguousError` (409) heritent toutes deux de `HermesMailClientError` ; le `except` ne distinguait pas un vrai "dossier absent" d'un simple timeout de transport | | Resultat vide mis en cache pour tout le run | Chaque note suivante du meme dossier tombait directement en `NO-MATCH` -> `attachments_scanned=1`, sans meme retenter l'appel | Le cache par `folder_path` ne stocke qu'une seule fois par run, sans TTL ni distinction succes/echec | | `HermesMailClient.list_attachments()` : `except Exception` generique | Un timeout sur le listing PJ d'une note precise renvoyait aussi `[]` (silencieux), note marquee "0 PJ" a tort | Meme pattern que ci-dessus mais a l'echelle d'une note plutot que d'un dossier -- le client officiel ne fait pas la distinction 404 vs timeout, choix delibere pour son autre consommateur (backfill texte) mais dangereux tel quel pour ce script | --- ## Solution validee **`_fetch_folder_or_none()`** (nouvelle fonction, remplace l'appel direct a `client.search_folder()` dans `process_note`) : distingue 404 (`[]`, dossier reellement absent), 409 ambigu (`None`, necessite resolution manuelle), et toute autre erreur (`HermesMailClientError` generique = timeout/transport) avec **une retentative** apres `RETRY_DELAY_ON_TRANSIENT_ERROR = 5s` avant d'abandonner (`None`). **`_list_attachments_or_none()`** (nouvelle fonction, bypasse `client.list_attachments()` avec le meme appel HTTP brut) : meme principe a l'echelle d'une note -- seul un vrai 404 devient `[]`, tout le reste (timeout inclus) devient `None`. **Contrat `None` propage jusqu'a `process_note()`** : `None` != `attachments_scanned=1`. Une note dont le dossier ou le listing PJ n'a pas pu etre verifie n'est **jamais** marquee scannee -- elle reste `attachments_scanned=0` et sera retentee au prochain passage. **Circuit-breaker dans `main()`** : `MAX_CONSECUTIVE_TRANSIENT_FAILURES = 3`. Des que 3 notes d'affilee renvoient `None` (session probablement en train de se degrader), le run s'arrete proprement (`break`) plutot que de continuer a empoisonner des dossiers pour rien. Le run logue vu/telecharge/skippe separement au lieu d'un seul total ambigu. **Filtre `year_bucket` parametrable** : `YEAR_BUCKET = os.environ.get("ATTACH_YEAR_BUCKET", "2026")`. Le SQL de `main()` n'avait aucun filtre annee avant ce fix (bug distinct, meme session) -- `WHERE attachments_scanned = 0 AND year_bucket = ?` desormais, avec `ATTACH_YEAR_BUCKET=ALL` pour lever le filtre une fois 2026 termine et enchainer sur 2024/2025 sans retoucher le code. **Orchestration en lots** (script wrapper separe, hors image) : lots de 20 notes, redemarrage de hermes-mail-browser (cache Edge vide, cf ci-dessous) entre chaque lot, pause de quelques minutes. Le run precedent avait deja mis en evidence qu'un profil Edge de hermes-mail-browser peut accumuler plusieurs Go de cache disque (2,9 Go constate le 07/08 avant nettoyage) sans impact fonctionnel direct connu, mais un redemarrage periodique repart sur un process Edge/CDP propre plutot que de laisser une session tourner des heures. --- **Addendum (meme session, ~1h plus tard)** : le circuit-breaker fonctionnait mais se faisait piegier par 3 notes precises du dossier `DR Zone Sud|Gabes` (index 7, 8, 14), qui timeoutaient de facon **reproductible** sur `list_attachments` (pas transitoire -- 2 lots de suite, memes 3 notes, meme resultat, y compris apres redemarrage confirme healthy du container). Diagnostic direct avec un timeout large (90s) : la reponse arrive en ~23,5s et ne fait que 28 octets -- donc pas un volume de PJ important, plutot une latence fixe de rendu OWA pour ces messages precis, juste au-dessus de l'ancien seuil de 20s. Fix : `LIST_ATTACHMENTS_TIMEOUT = 35` (constante dediee, au lieu du `20` en dur). Sweep relance apres ce fix : les 3 notes passent du premier coup, aucun `[LIST-FAILED]` sur le lot suivant, telechargements de PJ reelles confirmes (PDF recus, tailles coherentes). Lecon : un circuit-breaker sur echecs consecutifs protege bien contre la corruption de donnees, mais ne distingue pas a lui seul "degradation de session" de "seuil de timeout trop serre pour un sous-ensemble reproductible de messages" -- verifier les deux angles avant de conclure sur la cause quand le meme point de blocage revient identique. ## Verification ```bash docker exec nyora-notes-tt python3 download_attachments_phase1.py 5 ``` Log attendu : `[START] N notes a scanner (year_bucket=2026, limit=5)`, puis en fin de run `[DONE] N notes vues, X PJ telechargees, Y non verifiees`. Verifier en base que les notes marquees `attachments_scanned=1` ont bien un contenu `attachments` (`[]` ou liste reelle), pas juste le flag pose : ```python import sqlean as sqlite3 c = sqlite3.connect('/app/nyora-notes-tt.db') c.execute("SELECT id, attachments, attachments_scanned FROM notes WHERE folder_path=?", (folder,)).fetchall() ``` --- ## Pieges specifiques - **Le bug touchait deux endpoints differents avec le meme pattern** (`search_folder` a l'echelle dossier, `list_attachments` a l'echelle note) -- en chercher d'autres occurrences si `hermes_mail_client.py` est reutilise ailleurs avec un `except Exception` qui renvoie `[]`/`None` sans distinguer 404 (vrai negatif) d'un timeout (inconnu). - Le contrat `None` vs `[]` doit rester strict dans tout code qui consomme ces deux fonctions -- `None` ne doit jamais etre traite comme une liste vide par accident (ex. `fresh_attachments or []` effacerait la distinction et referait apparaitre le bug). - Rebuild : `docker compose build` en SSH synchrone depuis mcp-nas fait tomber la connexion (NAT TT) -- lancer en nohup + tail du log (`nohup docker compose build > build.log 2>&1 < /dev/null &`, bien rediriger stdin sinon la commande backgroundee bloque quand meme la session SSH parente). - Sauvegarde systematique de l'ancien script avant remplacement (`download_attachments_phase1.py.bak-20260807`) avant tout remplacement de fichier cuit dans l'image. --- ## References - Runbook associe : `hermes-tt/nyora-notes-tt-roadmap-pieces-jointes.md` (architecture 2 phases) - Runbook associe : `hermes-tt/mail-o365-16-dossiers-error-etat-session-owa.md` (degradation de session OWA, angle different -- degradation cote navigation plutot que cote script PJ) - Note NyoraNotes : "Chantier PJ 2026 -- fix timeout search_folder/list_attachments + lots de 20" (07/08/2026)