Hermes a « oublié » 798 messages après un crash — le bug d'amnésie de session, expliqué et corrigé


Lundi matin. Vous ouvrez Telegram pour reprendre la conversation laissée avec Hermes la veille au soir — et il se met à faire la conversation sur un sujet qui aurait dû être clos trois jours plus tôt. « Ce PR a été mergé ? »

Ce n’est ni le modèle qui perd la tête, ni votre imagination. C’est exactement ce qui est arrivé à Teknium, le lead du projet Hermes Agent, début août 2026 : après un crash et un redémarrage du gateway, son DM Telegram a silencieusement repris un contexte vieux de 2,7 jours, et les 798 messages réels échangés entre-temps semblaient avoir disparu. Rien n’avait été perdu — Hermes ne les retrouvait tout simplement pas.

Ce qui s’est passé : comment un seul message a envoyé une session dans le passé

Le ticket officiel de suivi (#82616) contient des preuves issues de la base de données de production. L’incident s’est déroulé en quatre étapes :

Quand Ce qui s’est passé
6 août, 16h18 Un message entrant déclenche un chemin de récupération qui crée silencieusement une nouvelle ligne de session sans identité
6 août → 8 août Les 798 messages réels atterrissent tous dans cette ligne orpheline ; le mappage en mémoire maintient l’illusion d’une conversation parfaitement normale — le fork est invisible
8 août, 17h03 Le gateway plante puis redémarre ; le mappage en mémoire « chat → session » est jeté comme obsolète
9 août, 09h40 Le message suivant arrive : Hermes résout le chat par session key, et la seule ligne portant la clé est l’ancienne session du 3 août — il reprend donc un contexte vieux de trois jours et discute d’un PR mergé plusieurs jours plus tôt

Les preuves de la base montrent deux lignes : l’« original » du 3 août (session key complète, mais ended_at vide et aucun nouveau message), et l’orpheline du 6 août (session_key, chat_id tous NULL — mais avec les 798 messages accrochés à elle).

En clair : Hermes a silencieusement « forké » une copie sans nom de la session, tous les messages sont allés dans la copie, un redémarrage a effacé le carnet d’adresses en mémoire, et quand Hermes a cherché la session à nouveau, il n’a reconnu que la ligne portant une étiquette — il est donc revenu en arrière dans le temps.

Cause racine : trois défaillances empilées, toutes silencieuses

L’analyse approfondie (l’analyse complète au niveau du code se trouve dans les commentaires du ticket) a convergé vers une seule chaîne de causes :

  1. Les écritures en base de la rotation de session ont échoué, et les échecs ont été avalés. L’auto-reset d’inactivité d’Hermes clôt l’ancienne session et en crée une nouvelle — les deux écritures ont échoué : l’une journalisée au niveau debug, l’autre un simple print dans la console. Personne n’a rien remarqué.
  2. Pourtant, la table de routage était déjà passée à la nouvelle session. L’ancienne session est devenue un « zombie » (jamais marquée comme clôturée, détenant toujours la session key), tandis que la nouvelle session n’existait pas du tout sur le disque.
  3. Un writer paresseux a matérialisé la ligne fantôme. Une écriture en arrière-plan ultérieure (par exemple la comptabilité de consommation de tokens) a constaté que la ligne cible manquait et l’a créée à la volée — mais avec tous les champs d’identité (session_key, chat_id, …) vides. L’orpheline était née, et aucun chemin de code ne lui a jamais réapposé d’identité.
  4. Après le crash, la résolution au redémarrage ne voyait pas l’orpheline. Elle cherche les sessions par clé et par infos de chat — l’orpheline ne correspond à rien, le seul candidat est donc le « zombie » original. Il gagne, et l’ancien contexte revient.

Il y a une péripétie fascinante : les logs répétaient possible FTS write corruption et disk=0 (relecture disque de 0 lignes), si bien que tout le monde a d’abord suspecté une corruption de la base de données. L’enquête a prouvé que le chemin de lecture ne touche jamais l’index full-text — le vrai problème était le routage par ID de session : le writer suivait une carte de reroutage, le reader interrogeait l’ancien ID et obtenait 0 ligne. La base était saine du début à la fin, ce qui explique précisément pourquoi aucun message n’a été perdu.

Rien n’a été perdu : les 798 messages vivent dans une ligne « cachée »

Le plus rassurant dans tout cet incident : les 798 messages sont intacts — ils reposent simplement dans une ligne sans étiquette d’identité, invisible pour le resolver. Pour les utilisateurs concernés, le ticket documente une procédure de récupération manuelle (ci-dessous).

Le correctif : prévention + traitement, mergé sur main le 9 août 2026

Le correctif arrive sous la forme de deux PR — l’un pour que cela ne se reproduise plus jamais, l’autre pour les dégâts déjà existants :

#82633 — durcissement côté écriture (prévention)

  • Identité écrite de façon atomique : la session key, le chat ID et l’origine font désormais partie de l’INSERT de création de ligne lui-même (ON CONFLICT remplit les manques via COALESCE), au lieu d’un UPDATE post-création au mieux. L’identité et la ligne vivent ou meurent désormais ensemble.
  • Chaque refresh est une occasion de réparation : le gateway rafraîchit les infos de session à chaque tour ; si une ligne manque, il insère désormais l’identité complète au lieu de ne rien faire en silence.
  • Résolution sensible à la récence : après un crash/redémarrage, les sessions candidates sont classées par last_activity_at, les lignes contenant des messages en premier, et un nouvel ID n’est jamais créé tant qu’une ligne avec clé existe.
  • Les erreurs cessent d’être silencieuses : les échecs d’écriture de clôture/création sont journalisés en WARNING avec la conséquence de routage explicitée ; les exceptions de lecture de transcript ne renvoient plus silencieusement une liste vide.
  • Corrige aussi #12857 : les resets de session ne perdent plus l’ID de session parent (la lignée).

La validation a été brutale : 11 tests de régression lancés contre un main non corrigé ont produit 6 échecs ; après le correctif, tout passe. Une relecture de bout en bout de l’incident (zombie de 3 jours + orpheline de 798 messages sur une vraie base de données) aboutit de nouveau à la conversation en cours.

#82712 — hermes sessions repair-routing (le traitement)

Une nouvelle commande qui « sauve » les orphan sessions, conçue autour d’un principe fail-closed :

hermes sessions repair-routing              # dry-run d'abord : signale les constats, ne change rien
hermes sessions repair-routing --apply      # répare réellement, après confirmation
  • Détection : scanne les lignes de session gateway sans clé mais avec de vrais messages (les lignes branch/delegate/tool sont sans clé par conception et exclues, donc pas de faux positifs).
  • Verrouillé sur les preuves : il n’agit que lorsque les preuves sont sans ambiguïté — soit une lignée enregistrée (parent_session_id pointant vers une ligne avec clé de la même source), soit exactement un prédécesseur avec clé qui s’est tu dans les 15 minutes suivant le début de l’orpheline. Deux prédécesseurs candidats, ou deux orphelines revendiquant un même prédécesseur, sont refusés avec une raison — une mauvaise adoption coudrait la conversation d’une personne dans le chat d’une autre, alors il refuse de deviner.
  • La réparation : appose sur l’orpheline l’identité du prédécesseur via COALESCE (n’écrase jamais une valeur existante), enregistre la lignée, et met le prédécesseur à la retraite avec end_reason='superseded_by_repair' — volontairement pas la raison de reset normale, pour que le prochain redémarrage ne puisse pas dériver en arrière.

Êtes-vous concerné ?

Vérifiez le profil avant de paniquer :

  • Uniquement les sessions gateway : Telegram, Discord, Slack, WhatsApp et autres sessions de plateformes de messagerie. Les utilisateurs 100 % CLI sont structurellement immunisés — le CLI ne possède aucun de ces mécanismes de routage de session.
  • Risque le plus élevé : les sessions de longue durée, quiconque redémarre/met à jour/fait crasher le gateway, et les conversations très multimodales.
  • Pas un cas isolé : la même installation montre 5 incidents d’orphan session depuis juin (42, 34, 5, 798 et 2 messages) — c’était simplement le plus gros, et il est arrivé au lead developer.
  • Symptôme typique : après un redémarrage du gateway, Hermes parle de vieux sujets, ne se souvient pas des jours récents et référence des choses terminées depuis longtemps.

Mettre à jour et récupérer vos données

Important : le correctif est sur main (mergé le 2026-08-09) mais n’est encore dans aucune release — la dernière release est toujours v0.20.0 (2026-08-03). Les utilisateurs de longue date sont invités à mettre à jour, par l’un de ces deux chemins :

  1. Attendre la prochaine release, ou
  2. Tourner depuis main pour une couverture immédiate (voir le guide d’installation et la référence de la commande de mise à jour).

Déjà concerné et vous voulez récupérer votre conversation :

  • Préféré : mettez à jour vers un build contenant le correctif, puis lancez hermes sessions repair-routing (dry-run d’abord, puis --apply).
  • Procédure manuelle (tirée du ticket officiel, quand l’outil n’est pas disponible) :
    1. Arrêtez le gateway ;
    2. Sauvegardez d’abord : cp state.db state.db.bak ;
    3. Copiez la session_key, le chat_id, le chat_type, l’origin_json etc. de l’ancienne ligne « originale » vers la ligne orpheline (ne remplissez que les vides, n’écrasez jamais) ;
    4. Réglez ended_at et end_reason='superseded_by_repair' sur l’ancienne ligne ;
    5. Redémarrez le gateway et laissez la résolution trouver la vraie session.

Sauvegardez state.db avant toute étape manuelle, et assurez-vous que le gateway est complètement arrêté. En cas de doute, attendez la release du correctif et utilisez repair-routing — c’est plus sûr qu’une édition à la main.

Ce que cet incident nous apprend

Le plus effrayant n’est pas le bug en lui-même — c’est le silence. Le fork de session s’est produit discrètement en arrière-plan ; la continuité en mémoire entretenait l’illusion que tout allait bien, jusqu’à ce qu’un redémarrage le révèle. Deux enseignements pour quiconque fait tourner un assistant IA de longue durée : d’abord, vérifiez la continuité des sessions après les redémarrages et les mises à jour ; ensuite, les données d’Hermes disparaissent rarement — une conversation « perdue » est généralement un problème de routage, pas un problème de données.

Pour approfondir la gestion des sessions, consultez la référence de la commande hermes sessions ; pour la dernière version majeure, voir les notes de release v0.20.0.