6.9 KiB
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.
Verification
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 :
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_foldera l'echelle dossier,list_attachmentsa l'echelle note) -- en chercher d'autres occurrences sihermes_mail_client.pyest reutilise ailleurs avec unexcept Exceptionqui renvoie[]/Nonesans distinguer 404 (vrai negatif) d'un timeout (inconnu). - Le contrat
Nonevs[]doit rester strict dans tout code qui consomme ces deux fonctions --Nonene 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 builden 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)