Hermes a corrigé 4 bugs sournois en une journée : pièces jointes, API Keys, mises à jour, recherche


Vous avez déjà vécu ce moment où « quelque chose cloche » ? Vous demandez à Hermes d’envoyer un fichier sur Telegram, il répond « envoyé » — mais la pièce jointe n’arrive jamais. Ou vous faites tourner plusieurs profiles sur la même machine, changez de modèle, et vous vous rendez compte que les identifiants utilisés ne ressemblent pas aux vôtres. Ou vous cherchez dans votre historique de sessions une phrase que vous avez pourtant bien dite, et vous n’obtenez rien. Ce n’est pas votre imagination : ce sont quatre bugs profonds d’Hermes Agent, tous corrigés le 9 août 2026. Cet article décortique chacun d’eux : à quoi il ressemblait, pourquoi il se produisait, et comment le correctif fonctionne.

Bug 1 : les pièces jointes disparaissent silencieusement — il a dit « envoyé », mais rien n’est arrivé

Le cas : Vous demandez à Hermes sur Telegram de générer un rapport PDF. Il répond « rapport généré, envoi en cours. » Mais votre conversation n’affiche qu’une étrange ligne de texte : MEDIA:/path/to/report.pdf — pas de fichier, juste le chemin littéral. Pire, parfois même pas ça : il journalise « livraison confirmée » et il ne se passe rien du tout.

Cause racine : Le bug vivait dans le chemin de livraison en file d’attente. Quand une réponse devait être mise en file d’attente (pendant que le tour précédent tournait encore, lors des déclassements subagent/compression, des rafales de photos, ou en mode file d’attente), la réponse du premier tour partait par un chemin latéral qui contournait entièrement le traitement MEDIA : les réponses non streamées étaient envoyées via adapter.send() brut avec le tag MEDIA: littéral comme texte et sans fichier ; les réponses déjà streamées journalisaient « livraison confirmée » et abandonnaient silencieusement les pièces jointes. Hermes vous disait qu’un fichier avait été produit — le fichier n’arrivait jamais.

Le correctif : Une nouvelle fonction _deliver_queued_first_response() gère désormais les livraisons en file d’attente de façon uniforme : elle sépare le texte des pièces jointes à l’aide du mécanisme existant extract_media + _deliver_media_from_response (en préservant le filtrage de sécurité des chemins), et achemine le tout via le flux standard de livraison des pièces jointes. Il y a aussi une garde judicieuse : si le premier tour a réellement échoué, elle délivre le texte d’échec normalisé mais n’uploade jamais de pièces jointes — fini de déguiser les échecs en succès.

Ce qu’il faut retenir : Si vous avez déjà reçu une ligne de texte MEDIA: ou une pièce jointe manquante sur Telegram, Discord, ou toute autre plateforme gateway, c’était ça. Après le correctif, les pièces jointes arrivent correctement ; avant de mettre à jour, vous pouvez utiliser le navigateur de fichiers ou web_extract pour inspecter les fichiers produits.

Bug 2 : des clés API qui traversent les profiles — le changement de modèle lisait la clé d’un autre

Le cas : Vous faites tourner plusieurs profiles sur la même machine (disons, travail et perso), chacun avec ses propres clés API de fournisseur de modèle. Un jour, vous passez sur votre profile perso et ouvrez le sélecteur de modèles — la liste des « fournisseurs authentifiés » affiche des fournisseurs authentifiés avec les clés de votre profile travail. Vous étiez à un clic d’envoyer une requête avec la mauvaise clé.

Cause racine : Avec les profiles multiplexés, les lectures d’identifiants pendant le changement de modèle contournaient la portée des secrets propre à chaque profile. Deux endroits étaient touchés : la liste « quels fournisseurs sont authentifiés » du sélecteur, et la résolution effective de la clé dans switch_model — cette dernière lisait en brut l’environnement du processus (expansion ${VAR} et fallback key_env) et passait le résultat à la résolution runtime comme clé API explicite. Un profile pouvait donc voir — ou adopter — les clés API d’un autre profile.

Le correctif : Un nouveau helper _scoped_key_env() achemine les deux lectures via agent.secret_scope.get_secret. Multiplexage désactivé, le comportement est octet pour octet identique à os.getenv (les utilisateurs mono-profile ne sont pas affectés). Multiplexage activé, les lectures sont strictement confinées à la portée de secrets du profile courant. La décision de conception clé : fail-closed (échec par défaut). Si une UnscopedSecretError est levée, cela provoque une erreur plutôt qu’un repli sur les variables d’environnement — les clés ne fuient jamais silencieusement entre profiles.

Ce qu’il faut retenir : C’est le seul problème type/security des quatre — il s’agit d’isolation des clés, donc les utilisateurs multi-profile devraient mettre à jour dès que possible. Le comportement mono-profile est totalement inchangé.

Bug 3 : des processus orphelins après les mises à jour — les mises à jour du desktop Windows bloquées

Le cas : Vous êtes sous Windows, avec l’application desktop Hermes. Vous cliquez sur mise à jour, mais la nouvelle version ne démarre pas — les anciens processus tiennent toujours l’environnement virtuel, les fichiers sont verrouillés, et la mise à jour échoue sans cesse. Le Gestionnaire des tâches montre un cimetière de processus backend « sans parent » qui refusent de mourir.

Cause racine : Pendant les mises à jour du desktop, releaseBackendLock() envoyait SIGTERM au backend principal avant que forceKillProcessTree() ne s’exécute. Sur Windows, cet ordre est fatal : si le launcher se termine avant que taskkill /T ne s’exécute, Windows ne peut plus énumérer ses descendants — les processus enfants survivent, tiennent le venv, et deviennent des orphelins.

Le correctif : Une nouvelle fonction stopBackendTreesForUpdate() tue l’arbre en commençant par la racine vivante (sans pré-signalisation), puis gère le démontage du pool. La classification des orphelins est aussi devenue consciente des arbres : les racines orphelines détectées par le scanner sont renvoyées avec leurs descendants (y compris le worker interpréteur uv-managed ré-exécuté, dont le parent vivant est la racine orpheline elle-même) — taskkill /T récolte alors tout le sous-arbre d’un seul coup. Les backends avec un parent réellement vivant sont laissés tranquilles.

Ce qu’il faut retenir : Ce correctif n’affecte que le flux de mise à jour du desktop Windows. Les utilisateurs Linux/macOS ne sont pas touchés ; si vous avez connu « mise à jour bloquée / processus résiduels » sous Windows, cela devrait s’améliorer considérablement.

Bug 4 : une recherche qui échoue en silence — les requêtes courantes ne renvoient rien

Le cas : Vous cherchez gateway/run.py dans votre historique de sessions — le fichier dont vous avez pourtant parlé — et vous obtenez « aucun résultat ». Vous essayez it's, user@host, 50%. Tout est vide. Vous commencez à douter de votre mémoire. Les mots y étaient ; c’est la recherche qui était cassée.

Cause racine : La recherche de sessions utilise la recherche plein texte FTS5 de SQLite, mais le sanitiseur de requêtes ne supprimait que six caractères (+{}():"^). La grammaire de FTS5 en rejette beaucoup d’autres — apostrophes, slashes, @, virgules, points d’interrogation, signes égaux, points-virgules, points d’exclamation, barres verticales, tildes, dièses, signes dollar, crochets, chevrons, antislashs — et n’importe lequel d’entre eux atteignant MATCH brut levait une OperationalError, que le site de requête avalait en zéro résultat. Vous ne cherchiez pas « rien » ; c’est la requête qui plantait.

Le correctif : La classe de suppression du sanitiseur a été reconstruite (via re.escape, ce qui a aussi corrigé une perte d’antislash littéral) pour couvrir l’ensemble complet des rejets, avec des cas spéciaux intelligents : les phrases exactes entre guillemets ("exact phrase") survivent grâce à une extraction par placeholder, les termes avec points ou tirets ne sont pas touchés, et % n’est supprimé que pour les requêtes non-CJK — car % est un caractère réservé pour le repli LIKE des CJK, et les requêtes CJK n’atteignent de toute façon jamais le chemin d’erreur FTS5.

Ce qu’il faut retenir : Après le correctif, it's, gateway/run.py, user@host et 50% se parsent et matchent tous correctement ; les requêtes CJK sont totalement inchangées. Si la recherche Hermes vous a déjà paru capricieuse, ce bug en était probablement la cause.

Résumé : une journée de merge, quatre leçons

Bug Symptôme Sévérité Affecte
Pièces jointes qui disparaissent Texte MEDIA: littéral / fichiers abandonnés en silence Fonctionnel Toutes les plateformes gateway (Telegram, Discord, …)
Fuite entre clés API Le changement de modèle lit la clé d’un autre profile Sécurité Utilisateurs de profiles multiplexés
Orphelins de mise à jour Les anciens processus verrouillent le venv après les mises à jour Windows Fonctionnel Desktop Windows
Échecs silencieux de recherche Les requêtes avec caractères spéciaux renvoient du vide Fonctionnel Tout le monde

Le fil conducteur des quatre : les échecs étaient silencieux. Des pièces jointes abandonnées mais annoncées comme envoyées ; des requêtes qui plantaient mais s’affichaient comme « aucun résultat » ; des clés mal lues sans la moindre erreur. C’est exactement pour cela qu’ils étaient si difficiles à trouver — la première réaction naturelle est de douter de soi, pas du logiciel. Les quatre sont corrigés maintenant ; si l’un d’eux vous a mordu avant, cela vaut la peine de réessayer ces opérations après la mise à jour.

Note : Ces correctifs ont atterri sur main d’Hermes Agent le 2026-08-09 et ne sont pas encore dans un tag de release (la dernière release est toujours v0.20.0). Pour les utiliser dès aujourd’hui, exécutez depuis la source — ou attendez la prochaine release.