Plusieurs agents de codage, un seul dépôt : des revendications plutôt que des verrous
Des agents de codage en parallèle entrent en collision au moment de la fusion, parce qu’aucun ne sait ce que les autres modifient. Les agents cloud de Foxl Code revendiquent désormais les fichiers qu’ils s’apprêtent à modifier et apprennent lesquels de vos autres agents actifs ont déjà revendiqué un chemin qui chevauche le leur. Une revendication est enregistrée dans tous les cas et ne verrouille rien ; en cas de conflit, l’agent a pour consigne d’envoyer la modification au détenteur, d’attendre ou de vous demander. La sortie réelle de la commande livrée, les règles des globs, le coût en base de données, et la question de savoir si les agents suivent vraiment la règle, à laquelle nous n’avons pas encore de réponse.

Sur cette page
Faites travailler plusieurs agents de codage dans un même dépôt et ils entrent en collision au moment de la fusion, parce qu’aucun ne sait ce que les autres modifient. Foxl Code exécute dans le cloud des agents de codage comme Claude Code et Codex, et depuis la v0.9.3-beta.5, chaque agent a pour consigne de revendiquer les chemins qu’il s’apprête à modifier en lançant foxl-agents claim <paths> avant d’éditer. Si un autre agent du même utilisateur, encore en cours d’exécution (un agent actif), a déjà revendiqué un chemin qui chevauche le sien, la réponse nomme cet agent et la note qu’il a laissée. Rien n’est verrouillé. En cas de conflit, l’agent a pour consigne d’envoyer au détenteur la modification dont il a besoin, d’attendre que le détenteur libère sa revendication, ou de demander à l’utilisateur, et ses propres revendications disparaissent à la fin de son exécution.
Pourquoi les agents de codage en parallèle entrent en collision
Chaque agent travaille dans sa propre copie du dépôt, sur sa propre branche : rien n’empêche donc deux d’entre eux de modifier le même fichier. Git signale un conflit textuel quand la seconde pull request est fusionnée, une fois que les deux agents ont terminé et ont été payés. Un conflit sémantique, où deux pull requests passent chacune leurs vérifications mais cassent le build une fois fusionnées toutes les deux, n’est signalé par rien tant qu’une personne n’a pas fusionné les deux. Les revendications ne détectent ce type de conflit que lorsque les deux agents touchent aux mêmes chemins.
Notre propre organisation nous a montré ce qui aide. Nous développons Foxl avec 15 à 20 sessions Claude Code qui tournent en parallèle sur son dépôt, et en 14 jours elles se sont envoyé 4 773 messages. Ce qui les empêchait d’écraser le travail les unes des autres, c’était une liste de propriété partagée. Une session coordinatrice note quelle session possède quels fichiers (et, pour un fichier que beaucoup de sessions touchent, quelles parties de ce fichier), et une session qui a besoin d’une modification dans un fichier qui ne lui appartient pas envoie la modification au propriétaire au lieu de la faire. Avant cette liste, des sessions modifiaient les mêmes fichiers en même temps et ne s’en apercevaient qu’à la fusion. La liste avait elle aussi un coût : sur les 123 fois où une session a été bloquée pendant ces 14 jours, 33 étaient des attentes de la réponse du coordinateur.
Foxl Code avait déjà la partie messagerie : une commande foxl-agents que chaque agent cloud peut utiliser pour lister les autres agents actifs de l’utilisateur et leur envoyer un message. Les revendications ajoutent la liste de propriété sans coordinateur : chaque agent dit ce qu’il va toucher, et la base de données répond en une seule requête.
Pourquoi des revendications plutôt que des verrous ?
Un verrou ne fonctionne que si chaque agent le respecte et le libère. Un agent de codage qui plante, épuise son budget ou est annulé ne libère rien : un verrou a donc besoin d’un délai d’expiration, et un délai assez long pour une vraie exécution (les nôtres peuvent durer plus de sept heures) bloque tout le monde aussi longtemps. Deux agents qui ont chacun besoin d’une ligne du fichier de l’autre s’attendraient mutuellement à l’infini. Et un agent dont le verrou est refusé n’a plus rien d’autre à faire que s’arrêter.
Les revendications contournent chacun de ces problèmes. Un détenteur qui a planté cesse de compter dès que son exécution n’est plus active : il n’y a pas de délai d’expiration à attendre. Une revendication qui en chevauche une autre n’est pas refusée ; elle est enregistrée, et le chevauchement est renvoyé à l’agent qui a demandé, avec l’identifiant de tâche du détenteur, son titre, les chemins revendiqués et sa note. Et un agent qui tombe sur un conflit a d’abord pour consigne d’envoyer au détenteur la modification dont il a besoin : deux agents qui ont besoin des fichiers l’un de l’autre ont donc autre chose à faire qu’attendre, même si rien n’empêche les deux d’attendre quand même. Voici, mot pour mot, les instructions que reçoit chaque agent :
- Before you start editing, claim the files and directories you expect to
change, in one command: `foxl-agents claim <path>... --note "<why>"`. A
directory claims everything under it; `*`, `?` and `**` globs work. Claim
what you will change, not the whole repository, and claim again if your
plan grows.
- The answer names any other live agent that already claimed an overlapping
path. On a CONFLICT, do not edit those files: send the holder the exact
change you need with `foxl-agents send <task-id> "<the change>"`, wait until
it releases them (check `foxl-agents claims` at a natural checkpoint, never
in a loop or behind a `sleep`), or tell the user. A claim is information,
not a lock.
- `foxl-agents release <path>...` drops a claim you no longer need. Every
claim you hold is dropped when your run ends.Ces lignes sont ajoutées au fichier d’instructions au niveau utilisateur que lit chaque CLI : CLAUDE.md pour Claude Code, et AGENTS.md pour Codex, Kiro, Cursor, Hermes et OpenCode. Le même mécanisme doit atteindre six CLI différentes, et c’est pourquoi foxl-agents est une commande shell plutôt qu’un serveur MCP. Toutes les CLI de code savent exécuter une commande shell : un seul script fonctionne donc dans les six, sans configurer chacune pour qu’elle charge un serveur.
Ce qu’un agent exécute et ce qu’il voit
Voici la sortie réelle de la commande livrée, lancée par un script et non par un agent, pour deux tâches de démonstration dans un dépôt de démonstration. La tâche A est une tâche Codex qui ajoute une limitation de débit, et la tâche B une tâche Claude Code qui ajoute la pagination à l’endpoint des utilisateurs. Le serveur, sa base de données et la commande sont ceux-là mêmes que reçoivent les agents cloud.

La tâche B a revendiqué src/api/ avec une barre oblique finale, qui désigne un répertoire, et le serveur l’a stocké sous la forme src/api/**. Sans la barre oblique, la commande aurait ajouté /** elle-même : elle s’exécute dans la copie de travail, le seul endroit capable de distinguer un fichier d’un répertoire. La commande réécrit aussi chaque chemin par rapport à la racine du dépôt, quel que soit le répertoire de travail, et refuse un chemin situé hors de la copie de travail avant d’envoyer quoi que ce soit.
Le conflit nomme les deux fichiers de la tâche A et sa note, et c’est d’après la note que la tâche B décide. Ici, elle pourrait laisser le middleware tranquille et envoyer à la tâche A la seule modification dont elle a besoin à cet endroit, ou commencer par le handler propre à l’endpoint et revenir vérifier une fois que la tâche A aura libéré.
Comment savoir si deux globs se chevauchent ?
Deux revendications se chevauchent quand au moins un chemin correspond aux deux. La grammaire est volontairement réduite :
- un chemin simple est un glob sans caractère générique ;
*correspond à des caractères au sein d’un seul segment de chemin ;?correspond à exactement un caractère ;- un segment qui vaut exactement
**correspond à un nombre quelconque de segments entiers ; - tout autre caractère est littéral :
[id].tsxest donc un nom de fichier, pas une classe de caractères.
Voici ce que renvoie le comparateur livré pour quelques paires :
| revendication A | revendication B | chevauchement |
|---|---|---|
src/api/** | src/api/users.ts | oui |
src/*.ts | src/api/users.ts | non, * reste dans un seul segment |
src/**/*.test.ts | src/api/*.ts | oui, les deux correspondent à src/api/x.test.ts |
docs/** | src/** | non |
src/app/[id]/page.tsx | src/app/a/page.tsx | non, [id] est littéral |
**/*.md | docs/guide.md | oui |
Un chevauchement ne fait jamais refuser une revendication, mais quelques formes le sont :
- un glob composé uniquement de caractères génériques qui inclut
**, comme**ou**/*, parce qu’il revendiquerait tout le dépôt ; - un segment de chemin
.., une barre oblique inverse ou un caractère de contrôle ; - un
!en tête, parce que ce n’est pas une négation ici et que cela ne doit pas en avoir l’air.
Une tâche détient au plus 50 revendications, de 200 caractères au plus chacune, avec des notes de 280 caractères au maximum.
La comparaison est plafonnée, pour qu’un ensemble inhabituel de revendications ne puisse pas coûter beaucoup de CPU au serveur. Deux revendications dont les premiers segments littéraux diffèrent (docs/** et src/**) sont écartées d’emblée. Sinon, le serveur effectue un parcours mémoïsé sur des paires de segments et, au sein d’un segment, sur des paires de caractères. Toutes les comparaisons d’une même requête partagent un budget de 50 000 étapes, et une paire que le budget n’atteint jamais est signalée comme chevauchement possible plutôt qu’ignorée : une requête qui épuise son budget prévient donc quand même l’agent.
Quand une revendication prend fin
Une revendication appartient à une exécution. Quand l’exécution se termine, la tâche abandonne toutes les revendications qu’elle a faites, dans l’étape même qui enregistre l’exécution comme terminée. Deux vérifications au moment de la lecture couvrent une exécution qui n’a jamais atteint cette étape : une revendication dont le détenteur n’est plus actif est ignorée, de même qu’une revendication plus ancienne que la durée maximale d’une exécution, 7 heures 20 minutes. Les revendications ont la même visibilité que les messages : seuls les agents actifs d’un même utilisateur, qui passent par la même installation de la GitHub App sur le même dépôt, peuvent voir les revendications les uns des autres.

Ce que cela coûte sur une offre de base de données gratuite
Les revendications vivent dans une seule table, avec un seul index sur (utilisateur, dépôt, date de création), et rien ne l’interroge en boucle. Chaque lecture ne demande que les revendications créées depuis moins de 7 heures 20 minutes, et la date de création est la dernière colonne de l’index : les lignes plus anciennes tombent donc hors de la plage que la base de données parcourt. Des revendications périmées que personne n’a libérées ajoutent une ligne à une lecture, qu’il y en ait 1 000 ou 2 000 ; nous avons mesuré les deux cas.
Nous mesurons cela parce que Foxl tourne sur l’offre gratuite de Cloudflare, où toutes les bases D1 d’un compte se partagent 5 millions de lignes lues et 100 000 lignes écrites par jour, et où le compte a droit à 100 000 requêtes Worker par jour. Les chiffres ci-dessous viennent de D1 exécuté en local dans workerd, le runtime de Cloudflare, et n’ont pas encore été confirmés par les métriques propres de D1 en production :
| appel | lignes lues | lignes écrites |
|---|---|---|
| revendiquer N chemins | environ 2 par revendication active sur le dépôt, plus environ 1 par chemin et quelques lectures fixes | 2 par chemin |
| lister les revendications | 1 + 2 par revendication active | 0 |
| libérer | environ 1 par chemin | 1 par ligne libérée |
| fin d’une exécution qui n’a rien revendiqué | 0 | 0 |
Avec environ 50 tâches cloud par jour, tous utilisateurs confondus, et deux appels de revendication chacune, cela fait 100 appels par jour. Chaque appel représente deux requêtes Worker, une sur code.foxl.ai et une via un saut interne vers le serveur, soit environ 200 requêtes, ou 0,2 % des 100 000 de l’offre gratuite. Même si chacun de ces appels coûtait ce que coûte, au pire, celui d’un utilisateur très actif (dix agents détenant cinq revendications chacun sur un même dépôt), la journée lirait environ 11 000 lignes (0,2 % des 5 millions) et en écrirait environ 830 (0,8 % des 100 000).
Les agents suivent-ils vraiment la règle ?
Nous ne le savons pas encore, et cette phase est conçue pour le découvrir. Nos propres sessions locales avaient besoin d’un coordinateur pour faire respecter la même règle : nous ne partons donc pas du principe que six CLI différentes revendiqueront avant d’éditer et reculeront en cas de conflit simplement parce que leurs instructions le leur disent. Chaque conflit écrit un enregistrement d’audit (chaque instance du serveur ignore une répétition pour la même tâche et les mêmes détenteurs dans les dix minutes), tout comme chaque message qu’un agent envoie à un autre. Le journal d’audit montre si un agent a écrit au détenteur, la table des revendications montre s’il a libéré sa revendication, et le diff de la pull request montre s’il a modifié le fichier malgré tout.
La phase suivante dépend de ce que cela montrera. Si les agents respectent bien les revendications, le plan est que la partie de Foxl Code qui répartit une demande entre plusieurs agents donne à chaque sous-tâche une liste de fichiers, et exécute les sous-tâches qui se chevauchent l’une après l’autre plutôt qu’en même temps. Au 23 octobre 2026, la table de production est vide, car aucune tâche cloud n’a démarré depuis le déploiement de la fonctionnalité.
Pour qui construit quelque chose de similaire, les trois propriétés que nous garderions sont qu’une revendication est toujours enregistrée, qu’un chevauchement revient avec un détenteur et une note, et qu’une revendication ne dure que le temps de son exécution. Notre article du lancement de Foxl Code présente la boucle de l’agent elle-même, qui converge vers un objectif au lieu de faire confiance à chaque étape (en anglais).
Références et lectures complémentaires
- Tarifs de Cloudflare D1 et limites de l’offre gratuiteDocumentation
- Version Foxl v0.9.3-beta.5Version