190 appels gaspillés et aucun garde-fou n'a bronché

Vous demandez à l’agent de lancer 10 subagents en arrière-plan et de faire une synthèse quand ils ont terminé. Au lieu de clore le tour et d’attendre la notification, il maintient le tour en vie avec un no-op après l’autre — des appels execute_code qui affichent quelque chose et ne changent rien. 84 minutes plus tard, vous abandonnez à la main. En relisant le journal ensuite, il n’y a pas une seule alerte « you appear to be repeating yourself ». Le garde-fou est resté muet du début à la fin.
Cela n’a rien à voir avec un modèle stupide. C’est un détecteur de boucle incapable de voir ce type particulier de tournage à vide. L’issue #124072 a chiffré tout l’incident, et la PR #124724, fusionnée le 2026-09-27, en a corrigé la partie essentielle.
Le bilan de l’incident : 84 minutes, 190 appels
Les chiffres de l’issue viennent des propres enregistrements de la session (la fenêtre de boucle, 22:31:05–23:20:12) :
| Métrique | Valeur |
|---|---|
| Appels API du modèle principal | 190 |
| Tokens d’entrée (cachés inclus) | 31.365.131 (dont 31.272.448 de lectures de cache) |
| Tokens de sortie | 25.896 |
| Somme de la latence par appel | 1.869,8 s — 63 % de cette fenêtre de 49 minutes |
| Appels du modèle auxiliaire d’approbation | 193 (environ un par itération gaspillée) |
La session entière (22:09–23:33) est encore plus parlante sur la ligne principale : 267 appels, 40.377.600 tokens de lecture de cache, 73.059 tokens de sortie. Autrement dit, 71 % des appels du modèle principal ont été dépensés à l’intérieur de la fenêtre d’attente, et le garde-fou ne s’est jamais déclenché une seule fois.
Pourquoi la détection de boucle est restée muette
Le garde-fou d’appels répétés de Hermes se base sur un triplet : (nom de l'outil, arguments, hachage du résultat). Des appels identiques doivent s’aligner en série avant que l’avertissement « Nième appel identique » (ou l’arrêt net) puisse se produire.
Le problème venait du hachage du résultat lui-même :
- Les résultats d’
execute_codetransportent deux champs de comptabilité par appel qui changent à chaque fois :duration_seconds(temps d’horloge) etkernel.execution_count(un compteur monotone de cellules du kernel). - Des rejeux identiques octet pour octet se hâchaient donc différemment à chaque fois, et la série d’appels identiques restait bloquée à 1.
- La seconde moitié du problème est la plus tranchante :
after_call()retourne tôt pour les outils mutants, etexecute_codeen est un — donc pour cet outil, ce seul chemin de garde-fou peut se déclencher. Au total : zéro alerte. Le commentaire du code note « a model re-ran one empty execute_code call 147 times unflagged » ; le corps de la PR note zéro alerte sur 186 appels pendant l’incident.
Le correctif : supprimer exactement ces deux champs, pour execute_code uniquement
Le patch est volontairement étroit :
def _without_execute_code_metadata(parsed):
if not isinstance(parsed, dict):
return parsed
cleaned = {k: v for k, v in parsed.items() if k != "duration_seconds"}
kernel = cleaned.get("kernel")
if isinstance(kernel, dict):
cleaned["kernel"] = {k: v for k, v in kernel.items() if k != "execution_count"}
return cleaned
Le commentaire explique la portée : pour tout autre outil, ces mêmes noms de clés peuvent être une vraie sortie, donc y toucher corromprait les comparaisons d’« identique » ; les résultats non-JSON se hâchent exactement comme avant. Avec ce changement, la détection de rejeu fonctionne de nouveau — une alerte apparaît à partir du 3e appel identique (les seuils exacts sont configurables, voir ci-dessous).
Une précision utile : l’issue #124072 liste quatre défauts. Cette PR en corrige un — les champs volatils qui aveuglent les détecteurs de rejeu. Les trois autres, y compris la mécanique d’attente sur des subagents en arrière-plan, sortent du cadre ici.
Les réglages dont vous disposez déjà
Ces comportements sont configurables, et les valeurs par défaut diffèrent entre les exécutions interactives et non surveillées :
# ~/.hermes/config.yaml
tool_loop_guardrails:
warnings_enabled: true
hard_stop_enabled: false # les sessions interactives avertissent par défaut
non_interactive_hard_stop_enabled: true # gateway / cron s'arrêtent net par défaut
warn_after:
exact_failure: 2
same_tool_failure: 3
idempotent_no_progress: 2
hard_stop_after:
exact_failure: 5
same_tool_failure: 8
idempotent_no_progress: 5
loop_caps:
max_web_searches: 50 # appels web_search par tour
max_subagents: 50 # subagents lancés par tour
Le raisonnement se suit facilement : personne ne peut appuyer sur /stop dans une exécution non surveillée, donc ces plateformes s’arrêtent net par défaut, tandis que les sessions interactives reçoivent d’abord un avertissement pour que le modèle puisse se rattraper. Les loop_caps sont des plafonds par tour, réinitialisés à chaque tour et appliqués indépendamment des seuils ci-dessus — des dizaines de recherches ou de subagents en un seul tour étaient déjà pathologiques.
Pour inspecter les valeurs effectives, hermes config get tool_loop_guardrails fonctionne ; les sous-commandes sont show|edit|get|set|unset|path|env-path|check|migrate (il n’y a pas de reload).
Ce que cela signifie si vous lancez de longues tâches
- N’attendez pas en tournant à vide. Laissez le tour se terminer et revenez sur la notification d’arrière-plan. C’est bien moins coûteux que de garder un tour en vie avec des sondes vides — celui-ci a brûlé 31 M de tokens de lecture de cache.
- La boucle coûte plus qu’un seul modèle. Ces 193 appels au modèle auxiliaire d’approbation correspondent presque un pour un aux itérations gaspillées ; vous payez deux fois.
- Les arrêts nets ne sont pas un filet de sécurité universel. Même en fonctionnant correctement, ce garde-fou ne détecte que les appels identiques. Un modèle qui reformule chaque appel vide y échappe, et il ne reste que les plafonds par tour. Pour la machinerie voisine, voir le contrôle des subagents en direct et ce qui se passe quand le plafond de tours est levé.
- Le sens complet de ces clés se trouve dans notre référence de configuration hermes.
État de la publication
La PR #124724 a été fusionnée le 2026-09-27, après le tag de publication v0.21.5 (v2026.9.24), elle n’est donc que sur main : hermes update la récupérera.
À retenir
Un horodatage qui change à chaque appel n’est normalement qu’un bruit. Il ne devient coûteux que lorsqu’il sert à décider si cet appel était identique au précédent — et à ce moment, le garde-fou devient aveugle sur tout l’outil. Le correctif n’ajoute pas un garde-fou de plus ; il fait en sorte que le hachage ne regarde que la partie du résultat qui représente réellement ce qui s’est passé. Quelques dizaines de lignes, en échange de l’arrêt bien plus précoce de la plupart de ces 190 appels.