hermes update a arrêté les gateways sans jamais les relancer : le .git que git lui-même refuse de lire

Sur une machine Windows, hermes update se termine et Telegram comme WhatsApp cessent de répondre. Trois lignes dans le log : votre gateway nommé hermes-2 est « en pause », puis le fatal: not a git repository de git, puis une phrase rassurante — « Paused gateway(s) left stopped: the checkout HEAD is unreadable », avec la promesse que la prochaine commande hermes ramènerait le gateway.
Vous en avez lancé une. Rien n’est revenu. Vous avez relancé hermes update : les trois mêmes lignes, mot pour mot.
La PR #142609, fusionnée le 21 octobre 2026, casse cette boucle. Elle vaut la lecture parce que le bug n’est pas « une fonctionnalité a cessé de marcher » — c’est la gestion d’erreur qui scelle le chemin de récupération. Cette catégorie de bug reste invisible jusqu’au jour où elle mord, et ce jour-là vous êtes persuadé d’avoir cassé quelque chose.
Cause racine : un .git que git lui-même rejette
La prémisse d’abord : ce .git n’est pas absent, il est présent et illisible pour git. Deux formes typiques :
.git/HEADest vide, ou le fichier HEAD a complètement disparu..gitest un fichier (pas un répertoire) contenantgitdir: <some path>, et ce chemin n’existe plus — un résidu courant d’un worktree ou d’une migration.
L’updater de Hermes ne fetch que si .git existe, donc il fetch allègrement contre ce .git. Git répond not a git repository, la mise à jour échoue. Jusque-là, rien d’anormal.
Le vrai problème, c’est l’enregistrement de pause. Avant de mettre à jour, Hermes met en pause les gateways actifs (pour que rien ne lise l’arbre pendant que le code bouge) et enregistre l’état de cet arbre. Cet enregistrement comporte une vérification nommée tree_is_whole, et dès que HEAD était illisible elle retenait l’ensemble en pause indéfiniment. Autrement dit :
- Première mise à jour : pause du gateway, le fetch échoue, HEAD illisible, ensemble retenu, le gateway reste à terre.
- Deuxième mise à jour : même HEAD illisible, ensemble retenu de nouveau, gateway toujours à terre.
- Pour toujours.
Une précondition cassée (HEAD illisible) était traitée comme une raison de ne pas récupérer — et elle ne se réparerait jamais toute seule.
Correctif un : l’enregistrement de pause lâche prise
La nouvelle règle : si chaque pause de l’ensemble avait déjà trouvé HEAD illisible au moment d’être enregistrée, et qu’aucune étape n’a lié un déplacement d’arbre ensuite, l’ensemble reprend et les gateways reviennent.
Le raisonnement est simple. Les gateways servaient exactement cet arbre, et rien n’y a écrit — sans état à moitié écrit à protéger, il n’y a aucune raison de continuer à retenir.
Deux cas retiennent encore l’ensemble, et ça compte si vous croyez qu’il se libère désormais sans condition :
- un déplacement d’arbre lié est enregistré (
_checkout_movenote les cibles et les chemins avant que git n’écrive quoi que ce soit) ; - HEAD était lisible au moment de la pause et ne l’est plus — quelque chose d’autre a endommagé l’arbre entre-temps, et un humain doit regarder.
Correctif deux : l’erreur nomme le dommage et la réparation
Le second changement est dans le diagnostic du fetch. Avant, toutes les causes produisaient Failed to fetch updates from origin., vous laissant deviner.
Désormais, quand git lui-même affiche fatal: not a git repository et que le checkout échoue à rev-parse --show-toplevel, Hermes affiche :
This Hermes checkout's git data is damaged ...
suivi d’une commande d’installeur pointée vers ce checkout — avec le chemin :
curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash -s -- --dir <path>
Côté PowerShell, c’est -InstallDir <path>. Ça compte pour les installations dans un répertoire personnalisé : elles sont réparées sur place au lieu d’être ramenées à l’emplacement par défaut.
Pourquoi la vérification rev-parse en plus ? Parce qu’un dépôt distant avec un .git fantôme affiche la même ligne fatal: not a git repository. Un simple rapprochement de texte diagnostiquerait à tort une casse locale, donc le verdict dépend de la capacité à analyser le checkout courant. hermes update --check rapporte le même diagnostic.
Correctif trois : l’installeur gère un fichier .git fantôme
Les deux installeurs savaient déjà mettre de côté un checkout dont HEAD est illisible sous .broken-<stamp> et cloner à neuf. Mais install.sh ne le faisait que pour un .git répertoire ; devant un fichier .git fantôme, il refusait avec « not a Hermes git checkout ».
Il teste maintenant avec -e, comme le Test-Path de install.ps1. Les deux avertissements parlent aussi de « damaged .git » désormais, au lieu de seulement « interrupted clone » — les deux cas étaient jusque-là indistinguables.
Pourquoi le repli ZIP refuse toujours
L’updater dispose d’un repli ZIP pour quand git est inutilisable : télécharger une archive et écraser. Dans ce scénario, il refuse encore.
La raison est dans la doc : sans git status, il ne peut pas distinguer vos modifications locales d’un dommage, et écraser supprimerait le travail de quelqu’un. Cette règle fail-closed (#87304) tient — mieux vaut une réparation manuelle qu’un écrasement deviné.
Si vous êtes bloqué là, maintenant
Si vous fixez un gateway arrêté, avancez dans cet ordre (tout est le comportement de commandes livrées) :
# 1. voir ce que le gateway croit faire
hermes gateway list
hermes gateway status
# 2. s'il est seulement en pause et que rien d'autre ne le retient, le relancer
hermes gateway restart
# 3. si .git est réellement endommagé, réparer sur place avec l'installeur
# (remplacez <path> par le chemin de votre checkout)
curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash -s -- --dir <path>
Une fois sur une build qui embarque ce correctif, l’étape 3 cesse d’être votre affaire — hermes update affiche la commande pour vous. Ça enterre aussi une idée fausse liée : la promesse que « la prochaine commande hermes redémarrera les gateways » n’était pas toujours vraie, parce qu’elle dépendait de la libération de l’enregistrement de pause, et l’ancien code ne le faisait jamais.
Statut de release et rayon d’impact
La PR a été fusionnée tard le 21 octobre, après la publication de la v0.22.4 du jour (18:49 UTC), donc elle n’est ni dans la v0.22.4 ni dans un tag — main uniquement pour l’instant.
Les retours de terrain viennent de Windows (.git/HEAD supprimé alors que l’arbre de travail restait), mais la cause racine est indépendante de la plateforme : tout ce qui rend HEAD illisible — un .git nettoyé à la main, un clone interrompu, une migration qui n’a copié que le worktree — déclenche le même chemin sous Linux et macOS. Windows produit simplement cet état plus facilement, à cause des verrous de fichiers et de bizarreries de permissions.
La leçon est petite et générale : quand la logique de récupération dépend d’une précondition qui doit se réparer elle-même, et que cette précondition est justement ce qui est cassé, le système se bloque en silence. Maintenant, soit il récupère, soit il vous dit exactement où réparer.