Apprendre à un daemon à se relever tout seul

Un programme autonome qui se trompe ne se trompe pas une fois, il se trompe en boucle, toute la nuit. Récit des garde-fous que j'ai posés sur Forge pour qu'il borne ses emballements, reprenne ses agents morts, range ses propres carcasses et m'alerte avant le matin, le tout sans moi. Avec, en dernier recours, un agent dont le seul travail est de réparer la machine à agents.

Projet rattaché Granit Golem Sur Azure, le prix d'un health-check explose : de quelques centimes à plusieurs euros pièce. Aucun outil self-hosted ne coche mes cases, alors je l'écris moi-même. Au passage, je lâche Claude Code à fond pour voir : le projet 'rikiki' vire en plateforme SaaS industrielle, et je passe un mois à éponger le bazar généré.

Un matin de mai, j’ouvre mon tableau de bord, café à la main. Un ticket. Relancé cent soixante-seize fois dans la nuit. Toujours le même, toujours pour le même échec, cent soixante-seize fois de suite pendant que je dormais.

Cent soixante-seize fois, et pas un regard posé dessus.

Un programme autonome qui se trompe ne se trompe pas une fois. Il se trompe en boucle, vite, et toute la nuit. C’est la rançon de l’autonomie : le jour où j’ai retiré le dernier humain de la boucle, moi, j’ai aussi retiré celui qui débranchait la prise quand ça partait en vrille.

Forge, c’est le daemon qui fait coder ma plateforme par des agents Claude pendant que je dors. J’ai raconté ailleurs comment je lui ai confié ma backlog et pourquoi son ordonnanceur a besoin d’une machine à états. Ici, c’est l’envers du décor : ce qui se passe quand ça casse, et comment je lui ai appris à se relever sans moi.

Une précision d’abord, la même que pour le reste de cette série. Tout ça reste de la R&D, sur mon propre outil. Pas une chaîne que je brancherais sur la prod d’un client : c’est justement un terrain où je m’autorise à le laisser tomber, pour apprendre à le faire se relever.

Parce que la vraie question, quand un programme écrit du code à votre place sans personne pour surveiller, ce n’est pas de savoir s’il va se tromper. Il va se tromper. C’est : qu’est-ce qui se passe ensuite, à trois heures du matin, sans moi ?

Un programme autonome se trompe en boucle

Mes pires nuits ont toutes la même forme : une répétition que plus rien n’arrête.

Les cent soixante-seize relances, c’était un agent qui repoussait à chaque fois exactement le même commit, et l’ordonnanceur qui le reprenait, encore, et encore. Une autre fois, c’est un ticket déjà livré que le daemon s’est entêté à vouloir reprendre, recyclé en zombie neuf cent trente cycles d’affilée. Une autre nuit encore, la branche principale est passée au rouge et y est restée une heure et demie, pendant que Forge continuait tranquillement de lancer des agents par-dessus, sans une alerte.

Aucun de ces incidents n’est un bug de logique au sens classique. Le code faisait exactement ce qu’on lui demandait. On avait juste oublié de lui dire quand s’arrêter.

Border l’emballement

La première parade n’a pas été d’ajouter de l’intelligence. Plutôt l’inverse : poser des plafonds bêtes. Un budget d’essais par ticket, et passé le budget, on abandonne.

Les bornes qui empêchent l'emballement8 gates

Budget par ticket5 essais
  • Cinq lancements maximum, reprises comprises
  • Au-delà, le ticket part en needs-humanavec les preuves, en commentaire GitLab
Attente qui s'allongebackoff
  • 5 min, puis 10, puis 20, puis 40 entre deux essais
  • Une tempête serrée devient une pluie finela vieille recette des réseaux qui se télescopent
Reprises sur le même point3 max
  • Un agent rejoué qui ne pousse aucun nouveau commit tourne en rond
  • Trois fois sur le même état, puis on arrêteun nouveau commit remet le compteur à zéro
Corrections d'une MR2 max
  • Une proposition recalée est rejouée deux fois
  • Ensuite, BLOCKEDle budget de cette MR est épuisé

Aucune intelligence là-dedans, rien que des plafonds. Passé la limite, Forge abandonne proprement au lieu de s'acharner.

Le cœur, c’est le budget : cinq lancements pour un ticket, reprises comprises. Entre deux essais, j’allonge l’attente, cinq minutes, puis dix, vingt, quarante (le backoff exponentiel, la vieille recette des réseaux : quand deux signaux se télescopent, chacun attend de plus en plus longtemps avant de réessayer). Une tempête serrée devient une pluie fine.

Et quand un ticket épuise son budget, Forge ne s’acharne pas. Il le bascule en needs-human, écrit un commentaire sur GitLab avec les preuves de ce qui a coincé, et passe au suivant. Le matin, c’est moi qui tranche.

Reprendre où ça s’est arrêté

Vous avez déjà perdu une heure de travail parce qu’un outil a planté, et qu’il a fallu tout refaire depuis le début ? Pour un agent, c’est pareil, sauf que l’heure se paie en tokens.

Un agent qui meurt en plein vol (un délai dépassé, un plantage, ou moi qui reprends la main sur son atelier) ne repart donc pas de zéro. Forge garde sa session ouverte pendant vingt-quatre heures et la rejoue : « reprends là où tu t’es arrêté ». Il rouvre le worktree, l’agent relit son propre git status, et continue son ouvrage au lieu de le recommencer.

Le chemin d'une panne, du plantage à la reprise

Panne

  • agent morttimeout, plantage, ou main reprise
  • cycle interrompule daemon coupé en plein travail

Détection

  • health-checkagent vivant ? cycle qui avance ?
  • seuilsinactif 30 min, bloqué 90 min

Reprise

  • rejoué depuis son étatou nettoyé, ou escaladé
  • needs-humansi le budget est épuisé
Un agent meurt rarement proprement. Forge le détecte, décide s'il vaut la peine d'être rejoué, et sinon range ou escalade vers moi.

Sauf que reprendre à l’aveugle, c’est rouvrir grand la porte à la boucle. D’où le garde-fou : si un agent relancé ne produit aucun nouveau commit, c’est qu’il patine sur le même point. Au bout de trois reprises sur le même état, Forge arrête de le ressusciter. Un seul nouveau commit, et le compteur repart à zéro : tant qu’il avance, je le laisse avancer.

Ranger derrière soi

Un daemon qui tourne en continu sème des traces. Des ateliers ouverts pour des propositions déjà fusionnées. Des tickets marqués « en cours » sans personne dedans. Des étiquettes qui auraient dû tomber depuis longtemps. Si rien ne range, ça s’accumule jusqu’au jour où la file entière se grippe.

C’est arrivé. Le 5 juin, quarante-trois tickets se sont retrouvés gelés en « en cours » sans le moindre atelier sur le disque : un blocage circulaire, chacun attendant un agent qui n’existait plus. Le lendemain, douze autres coincés derrière une étiquette « à relire » que rien ne venait retirer.

Ce que le daemon range derrière lui4 gates

Ateliers terminésMR fusionnée ou fermée
  • Le worktree est supprimé, jamais sans vérifier qu'aucun agent n'y travaille encore
Tickets orphelinsen cours, sans atelier
  • Remis en file, prêts à redévelopperle deadlock des 43 tickets gelés du 5 juin
Étiquettes périméesà relire, sans MR
  • Retirées, le ticket repart12 tickets coincés derrière, le 6 juin
Fichiers inconnusquarantaine
  • Mis de côté, jamais supprimésgitignorés, pour que je regarde avant de jeter

À chaque tour, Forge fait le ménage de ses propres traces. Sinon les carcasses s'empilent jusqu'au blocage.

Une règle me tient particulièrement à cœur, là-dedans : jamais de rm aveugle. Un fichier que Forge ne reconnaît pas dans un atelier ne part pas à la poubelle, il part en quarantaine, dans un coin gitignoré, pour que je puisse regarder avant de jeter. J’ai trop vu de scripts de nettoyage emporter ce qu’ils auraient dû épargner.

Voir dans le noir

Tout ce qui précède suppose une chose : savoir que quelque chose ne va pas. Et mon pire ennemi, là, n’a pas été la panne. C’est le silence.

Début juin, le dispatch est resté gelé deux jours pleins. Zéro ticket lancé, zéro alerte. Forge n’était pas en panne, il était poliment bloqué, et son silence ressemblait à s’y méprendre au calme d’une file enfin vide.

J’ai donc posté des sentinelles qui s’alarment du silence. Le dispatch figé depuis plus d’une heure, ça sonne. Le pool d’agents plein depuis trois quarts d’heure avec du travail qui attend derrière, ça sonne. Des tickets ouverts mais aucun fermé depuis une heure et demie, ça sonne aussi.

Chacune avec un délai de carence, pour ne pas refaire le coup des quatre-vingt-dix-neuf notifications identiques que j’ai déjà racontées. Une fois prévenu, je ne le suis plus avant plusieurs heures, le temps d’agir.

Et un principe que j’ai fini par graver : dans le doute, une sentinelle laisse passer. Une vanne mal réglée qui bloque tout est pire que la panne qu’elle surveille. Quand un garde-fou hésite, il ouvre.

Le dernier recours : un agent pour réparer la machine à agents

Parfois, rien de tout ça ne suffit. Le daemon est coincé, les sentinelles crient, et la cause est trop emmêlée pour une règle écrite d’avance.

Alors Forge fait une chose que j’ai mis du temps à m’autoriser : il lance un agent Claude dont le seul travail est de le débloquer, lui.

Un agent pour réparer la machine à agents.

Un agent lâché sur sa propre infrastructure, c’est pourtant le genre d’idée qui finit en catastrophe. Son mandat est donc verrouillé : il diagnostique, et il n’a droit qu’à des gestes sûrs (fermer un ticket mort, retirer une étiquette de travers, balayer une carcasse). Interdiction formelle de toucher au code, de lancer un git reset ou de redémarrer un service. Il range, il ne répare jamais à coups de masse.

Et il ne se déclenche pas à la moindre alarme. Il faut un blocage installé, plus de trois heures, signalé plusieurs fois, et qu’aucune autre tentative de déblocage n’ait eu lieu dans les quatre dernières heures. Le dernier recours doit rester rare, sinon ce n’est qu’une boucle de plus déguisée en sauvetage.

Le tester en le cassant

Comment je sais que tout ça tient ? Pas en l’espérant.

Je ne fais pas confiance à un filet que je n’ai jamais éprouvé. Alors je casse Forge exprès, par à-coups, dans ce que j’appelle des « game days » (des journées où je provoque la panne pour vérifier qu’il s’en relève). Je tue le daemon en plein déblocage, et je regarde s’il récupère son verrou au redémarrage sans laisser de cadavre derrière. Je l’inonde de fausses alertes, et je vérifie que ses quotas tiennent le choc. Je lui colle une proposition volontairement cassée, et j’observe : il la détecte, la corrige deux fois, puis abandonne proprement, exactement comme prévu.

Une sauvegarde qu’on n’a jamais restaurée n’est pas une sauvegarde, c’est un espoir. Un filet, ça se vérifie en sautant dedans, pas en le regardant de loin.

Ce que j’en retiens

L’autonomie, je la croyais affaire d’intelligence. Six semaines plus tard, je la vois surtout comme une affaire de rattrapage. Forge n’est pas plus malin que le premier soir où j’ai lancé quatorze agents d’un coup pour me retrouver avec une cohue. Il est juste mieux entouré de filets.

Rien de tout ça n’est terminé, et je n’irais pas jurer que la prochaine nuit se passera bien. C’est une mouture, sur mon propre terrain. Mais je dors mieux qu’au temps des cent soixante-seize relances (ce qui, vu d’où je partais, ne place pas la barre très haut).