/rollback a annoncé un succès — et a avalé en silence le travail non commité de votre dépôt imbriqué

Cela ressemblait à l’annulation la plus sûre de votre vie. Un refactor a mal tourné, vous avez consulté /rollback list, choisi un checkpoint vieux de deux heures et saisi son numéro. Le terminal a annoncé un succès. Puis vous faites un cd dans un sous-répertoire pour continuer, et le travail non commité du dépôt qui s’y trouve a tout simplement disparu — ni dans la corbeille, ni dans un stash.
C’est l’issue #127073. Les checkpoints photographient bien votre répertoire de travail, mais lorsque ce répertoire contient un autre dépôt git, le dépôt interne est enregistré comme un gitlink — une simple référence de commit — et ses fichiers n’entrent jamais dans l’instantané. Le rollback annonçait malgré tout un succès : vous perdiez le travail et vous receviez un faux feu vert. Le correctif (PR #127839, qui reprend le #127077 de @dskwe) a été fusionné sur main le 2026-09-29.
Ce que les checkpoints stockent — et ce qu’ils n’ont jamais stocké
Les détails d’implémentation déterminent quand vous vous faites piéger. Avant chaque appel à write_file, patch ou terminal, Hermes commite un instantané du répertoire de travail dans un dépôt bare partagé :
~/.hermes/checkpoints/
├── store/ # un dépôt bare partagé
│ ├── refs/hermes/<hash> # pointe de branche par projet
│ ├── indexes/<hash> # index git par projet
│ └── projects/<hash>.json
S’appuyer sur le modèle d’objets de git rend les instantanés légers et comparables — et c’est précisément là que naît la faille : git ne stocke pas le contenu d’un sous-dépôt dans son parent. Tout ce que l’arbre parent contient est un gitlink pointant vers un commit. Les modifications non commitées dans le dépôt interne n’existent tout simplement pas du point de vue du dépôt externe.
Cela produit la pire combinaison possible :
- les fichiers modifiés par l’agent dans l’arbre externe sont restaurés depuis le checkpoint ✅
- la même opération réinitialise le dépôt interne sur le commit nommé par son gitlink, en jetant le contenu non commité ❌
- et la commande renvoie un succès ✅ ← c’est là que ça devient vraiment dangereux
Après le correctif : refuser d’emblée au lieu de s’excuser après coup
La nouvelle règle est sans nuance : décider avant de toucher quoi que ce soit, et si le rollback devait atteindre un gitlink, ne rien modifier du tout.
Le comportement, point par point :
- Un rollback complet (
/rollback <N>) est refusé, parce que restaurer l’ensemble couvre nécessairement le gitlink. - Toute sélection qui touche un gitlink est refusée aussi — le gitlink lui-même, un fichier situé en dessous, ou un répertoire parent qui le contient. Le refus intervient avant que le moindre fichier, index, ref ou instantané de sécurité pré-rollback ne soit modifié, et le message d’erreur nomme les dépôts qui n’ont pas été capturés.
- Les sélections qui évitent les gitlinks restaurent toujours, y compris les pathspecs Git comme
*.txt,:(glob)…,:(exclude)tool. L’implémentation résout d’abord la sélection sur un index jetable : un joker qui atteindrait un gitlink est refusé, un joker qui ne l’atteint pas se comporte exactement comme avant. Ce détail compte — la première tentative de correctif rejetait tous les pathspecs sans distinction, ce qui supprimait la récupération*.txtqui, elle, fonctionne toujours. - La garde s’appuie sur le chemin enregistré dans le checkpoint, pas sur le système de fichiers du jour. Auparavant, un
notes.txtcapturé qui devenait ensuite un lien symbolique vers un dépôt imbriqué ne pouvait pas être restauré ; c’est désormais possible. À l’inverse, un gitlink enregistré et remplacé depuis par un lien symbolique sans rapport reste refusé. La validation de confinement des chemins (_validate_file_path) est inchangée.
Plus qu’un refus : les checkpoints le disent maintenant tout haut
Refuser ne répare pas ce qui s’est déjà produit, aussi le correctif ajoute-t-il un avertissement au moment de la capture. Tout checkpoint ayant enregistré un gitlink reçoit une annotation ajoutée à son propre message :
before write_file: app.py [nested git repos not captured: tool]
Au-delà de trois chemins, vous obtenez les trois premiers suivis de +N, par exemple [nested git repos not captured: a, b, c (+2)]. Comme les listes de /rollback affichent déjà la description d’un checkpoint, vous voyez l’avertissement au moment de choisir le checkpoint — et non après un rollback raté.
Le même correctif répare un bug qui masquait cette annotation : les surfaces rollback.list de la TUI et de Desktop lisaient une clé message alors que le manager l’appelle reason, si bien que ces listes n’affichaient jamais la moindre description de checkpoint. Le mapping est désormais en place, ce qui rend visibles à la fois les anciennes descriptions et le nouveau libellé.
Un détail multiplateforme : les composants de chemin sont désormais découpés avec le Path propre à la plateforme, de sorte que packages/nested et packages\nested sont tous deux correctement refusés sous Windows — le point de normalisation de chemins soulevé lors de la relecture de la PR précédente.
Qu’en faire
Une fois les mécanismes clairs, les conseils pratiques sont brefs, et chacun fait gagner des heures :
- Commitez dans le sous-dépôt avant de laisser un agent toucher au dépôt externe. Un gitlink pointe vers un commit ; seul le contenu commité est référencé. Si vous ne voulez pas encore de ce travail dans la branche principale, un
git stashou un commit jetable vaut mieux que d’espérer que le checkpoint le couvre. - Lisez la liste avant de lancer un rollback.
/rollback listaffiche maintenant[nested git repos not captured: …]. Voir cette annotation signifie que ce checkpoint ne peut restaurer que les fichiers externes — le dépôt interne, c’est à vous de le gérer. - Utilisez les pathspecs pour des restaurations chirurgicales.
/rollback 1 src/broken_file.pyest bien plus sûr qu’un rollback sur tout un répertoire, et les jokers fonctionnent de nouveau tant qu’ils ne touchent pas un gitlink. - Ne comptez pas là-dessus avec les backends conteneurisés. Avec
docker,singularity,modal,daytonaou un plugin de conteneur, les chemins appartiennent au sandbox et non à l’hôte : Hermes ne prend aucun checkpoint pour eux, et/rollbackliste les checkpoints existants de l’hôte tout en refusant le diff et la restauration, aussi bien dans la CLI que dans les chats de messagerie./diff sessiondonne la même raison. Les backends Local et SSH ne sont pas concernés.
Quatre garde-fous existants méritent d’être gardés en tête quand un parcours chaotique se transforme en pourquoi je ne peux rien restaurer : les répertoires de plus de 50 000 fichiers sont ignorés, un fichier dépassant max_file_size_mb (10 MB par défaut) reste hors de l’instantané, le store supprime le plus ancien commit de chaque projet en round-robin dès qu’il dépasse max_total_size_mb (500 MB par défaut), et les checkpoints sont désactivés de façon transparente quand git n’est pas dans le PATH.
Pour voir l’espace occupé par l’historique, ou pour le purger :
hermes checkpoints status # taille totale, nombre de projets, détail par projet
hermes checkpoints prune # supprime les checkpoints orphelins ou obsolètes et GC le store
hermes checkpoints clear # efface toute la base (tout l'historique /rollback)
Une condition préalable : les checkpoints sont opt-in. Depuis la v2, ils sont désactivés par défaut — activez-les pour une session avec hermes chat --checkpoints, ou globalement avec checkpoints.enabled: true dans ~/.hermes/config.yaml. Une fois activés, chaque projet conserve jusqu’à max_snapshots: 20 checkpoints, et la maintenance automatique s’exécute au plus une fois toutes les 24 heures, en supprimant les projets non touchés depuis 7 jours. Si vous ne les avez jamais activés, /rollback sera simplement vide — ce n’est pas un bug.
Gardez le bon modèle mental : un checkpoint est un undo de ce que l’agent vient de faire, pas une sauvegarde de chaque fichier présent dans votre arborescence. Nous avons rencontré la même catégorie de surprise dans l’article sur la corruption silencieuse de fichiers : faire confiance à un outil pour revenir en arrière ne fonctionne que si vous savez exactement ce qu’il a enregistré.
En bref
-
Un dépôt git imbriqué apparaît dans un checkpoint sous forme de gitlink ; ses modifications non commitées n’ont jamais été dans l’instantané.
-
Avant le correctif,
/rollbackécrasait le dépôt interne et annonçait un succès — la pire combinaison. -
Désormais, un rollback complet, ainsi que toute sélection touchant un gitlink (lui-même, un fichier en dessous ou son parent), est refusé avant toute modification, et le message d’erreur nomme les dépôts non capturés.
-
Les sélections qui évitent les gitlinks restaurent toujours, pathspecs Git compris, et la gestion des liens symboliques et des chemins Windows suit le chemin enregistré.
-
Les checkpoints qui ont manqué un dépôt imbriqué sont étiquetés
[nested git repos not captured: …]dès la capture, visible dans/rollback list, la TUI et Desktop. -
En pratique : commitez ou stashez dans le sous-dépôt d’abord, préférez
/rollback <N> <path>, et rappelez-vous que les backends conteneurisés n’ont pas de checkpoints côté hôte. -
Les checkpoints sont opt-in depuis la v2 (
hermes chat --checkpointsoucheckpoints.enabled: true) — une liste/rollbackvide signifie le plus souvent que le store n’a jamais été activé.
Référence complète des options sur hermes checkpoints ; pour les travaux d’intégrité voisins, voir cette série de correctifs de compaction.