La session que vous avez supprimée est revenue : deux bugs et un nouveau garde-fou

Vous supprimez une session qui a mal tourné dans l’application de bureau, vous rafraîchissez la liste, et la revoilà. Ou vous en supprimez une depuis le CLI et vous constatez ensuite que la session n’enregistre plus jamais de réponse : l’agent continue de répondre, l’historique reste vide, et la seule trace est une ligne dans le journal — Session DB append_message failed: FOREIGN KEY constraint failed. Les deux histoires sont réelles, et toutes deux viennent du même lot de correctifs.
La PR #125157, fusionnée le 2026-09-27, change le comportement lui-même : tant qu’un tour en cours détient une session, la suppression est refusée au lieu d’être discrètement annulée.
D’abord, deux vrais bugs
L’issue #123583 a documenté le premier en détail : la ligne de session disparaît du store alors que l’objet agent qui la possède est toujours vivant. Son indicateur interne _session_db_created croit encore que la ligne existe, si bien que le flush appelle une fois append_message, que la clé étrangère le rejette, et que l’échec est seulement journalisé — aucune nouvelle tentative, et rien d’observable ensuite, puisqu’un rollback est un rollback. Cela se répète à chaque tour. Le chat paraît parfaitement normal pendant que votre transcription durable cesse de grandir.
Le second, c’est son correctif (PR #124117, récupérée depuis la #123641) : une fois qu’une ligne est confirmée absente, le flush suivant recrée la session sous le même identifiant et rejoue tout le préfixe de la transcription, pas seulement le nouveau tour. La première tentative ne stockait que le nouveau tour, parce que les dicts d’historique paraissaient « déjà enregistrés » — le genre de détail toujours évident après coup. Le correctif insiste aussi sur un point : seule une ligne confirmée absente est réparée ; les autres échecs d’écriture ne déclenchent pas de reconstruction.
Le nouveau garde-fou : refuser la suppression, pas l’annuler
| Surface | Session détenue par un tour en cours | Inactive / fermée par compaction |
|---|---|---|
CLI sessions delete, export --delete-after-verified |
Refusée, code de sortie 1, Cannot delete active session |
Supprimée |
| Suppression unitaire sur le bureau | 409 | Supprimée |
| Suppression en masse sur le bureau | Racines actives ignorées et signalées dans skipped_active, avec un toast affichant les deux compteurs |
Supprimée |
DELETE du serveur API |
409 | Supprimée |
session.delete de la TUI |
4023 (le code existant « cannot delete an active session ») | Supprimée |
sessions prune |
Lignes actives épargnées (inchangé) | Élaguées |
Quelques détails d’implémentation valent la peine d’être connus :
- Le garde-fou est vérifié à l’intérieur de la transaction d’écriture de la suppression elle-même (le même chemin
_write_guards_reject/_guarded_idsquepruneutilisait déjà), donc rien ne peut changer entre la vérification et la suppression. - Si une session racine est inactive mais qu’un enfant délégué détient encore un bail actif, la suppression est refusée elle aussi — la suppression cascaderait dans cet enfant.
- Les sessions terminées (par exemple fermées par compaction) qu’aucun tour ne détient restent supprimables.
- La suppression en masse effectue une seule vérification groupée et ne retombe sur des vérifications par racine que si quelque chose est réellement protégé.
Ce que vous verrez
hermes sessions list # trouver la cible
hermes sessions delete 20260928_101500_ab12cd # active -> Cannot delete active session, exit 1
hermes sessions export 20260928_101500_ab12cd --delete-after-verified # exporter et vérifier, puis supprimer
hermes sessions prune --older-than 30d # les lignes actives sont toujours épargnées
Dans la TUI, une suppression sur une session active renvoie 4023 ; sur le bureau ou via l’API, c’est un 409. En pratique, le chemin le plus simple est de laisser le tour se terminer, ou de le /stop, puis de supprimer.
Alors, le cas du « retour » est-il encore un problème ?
Il est traité, des deux côtés. Le correctif de reconstruction (#124117) couvre « la ligne a disparu, mais pas la conversation » : la session revient avec tout son préfixe d’historique à la prochaine écriture réussie, donc une suppression en plein vol ne vous coûte plus les tours suivants. Le garde-fou (#125157) couvre l’autre côté : une suppression a lieu ou vous explique pourquoi, sans état intermédiaire où l’interface la montre disparue pendant que le backend continue d’écrire.
Si vous soupçonnez d’avoir déjà des résidus de cette classe de bug, regardez les statistiques du store lui-même avant de faire quoi que ce soit de radical. Les frontières de session expliquent quand les sessions sont découpées et quand elles se terminent, l’article sur state.db couvre la récupération au niveau de la base de données, et les options exactes se trouvent dans notre référence hermes sessions.
État de la publication
Les trois changements ont été fusionnés le 2026-09-27, après le tag de publication v0.21.5 (v2026.9.24), ils ne sont donc que sur main : hermes update les récupère.
À retenir
« La session est revenue » et « la session a disparu mais reçoit encore des tours » ressemblent à deux histoires différentes, mais elles n’en font qu’une : la suppression n’a jamais été alignée sur le tour qui utilisait la session. Le correctif rend la chose explicite — refuser tant que le tour est actif et dire à l’utilisateur pourquoi ; reconstruire une ligne réellement perdue à la prochaine écriture. La seule habitude à changer est petite : avant de supprimer une session, assurez-vous que son tour est réellement terminé.