Несколько агентов для кода в одном репозитории: заявки вместо блокировок
Параллельные агенты для кода сталкиваются при слиянии, потому что ни один из них не знает, что правят остальные. Облачные агенты Foxl Code теперь заявляют файлы, которые собираются менять, и узнают, кто из других ваших активных агентов уже заявил пересекающийся путь. Заявка записывается в любом случае и ничего не блокирует; при конфликте агенту велено отправить владельцу изменение, подождать или спросить вас. Реальный вывод выпущенной команды, правила glob, стоимость в базе данных и то, следуют ли агенты правилу на самом деле, чего мы пока не знаем.

На этой странице
Запустите несколько агентов для кода в одном репозитории, и они столкнутся при слиянии, потому что ни один из них не знает, что правят остальные. Foxl Code запускает агентов для кода, таких как Claude Code и Codex, в облаке, и начиная с v0.9.3-beta.5 каждому агенту велено перед правками заявлять пути, которые он собирается менять, командой foxl-agents claim <paths>. Если другой ещё работающий агент того же пользователя (активный агент) уже заявил пересекающийся путь, в ответе будут названы этот агент и оставленная им заметка. Ничего не блокируется. При конфликте агенту велено отправить владельцу заявки нужное изменение, дождаться, пока владелец её снимет, или спросить пользователя, а его собственные заявки исчезают, когда заканчивается его запуск.
Почему параллельные агенты для кода сталкиваются
Каждый агент работает в своей рабочей копии на своей ветке, поэтому ничто не мешает двоим из них изменить один и тот же файл. Git сообщает о текстовом конфликте при слиянии второго pull request, когда оба агента уже закончили и их работа уже оплачена. О семантическом конфликте, когда каждый из двух pull request проходит свои проверки, но после слияния обоих ломается сборка, не сообщает ничто, пока человек не сольёт оба. Заявки ловят такой конфликт, только если два агента трогают одни и те же пути.
Что помогает, нам показала наша собственная работа. Мы разрабатываем Foxl с 15-20 сессиями Claude Code, которые параллельно работают в его репозитории, и за 14 дней они отправили друг другу 4773 сообщения. Перезаписывать работу друг друга им не давал общий список владения. Одна сессия-координатор записывает, какая сессия владеет какими файлами (а для файла, который трогают многие сессии, какими его частями), и сессия, которой нужно изменение в чужом файле, отправляет это изменение владельцу, а не вносит его сама. До появления списка сессии одновременно правили одни и те же файлы и узнавали об этом при слиянии. У списка была своя цена: из 123 случаев за эти 14 дней, когда сессия была заблокирована, 33 были ожиданием ответа координатора.
Часть с сообщениями у Foxl Code уже была: команда foxl-agents, с помощью которой каждый облачный агент может получить список других активных агентов пользователя и отправить им сообщение. Заявки добавляют список владения без координатора: каждый агент говорит, что он будет трогать, а база данных отвечает за один запрос.
Почему заявки, а не блокировки?
Блокировка работает, только если каждый агент её соблюдает и каждый агент её снимает. Агент для кода, который упал, исчерпал бюджет или был отменён, ничего не снимает, поэтому блокировке нужен тайм-аут, а тайм-аут, достаточно длинный для реального запуска (наши могут длиться больше семи часов), на столько же блокирует всех. Два агента, каждому из которых нужна одна строка в файле другого, ждали бы друг друга вечно. А агенту, которому отказали в блокировке, остаётся только остановиться.
Заявки обходят каждую из этих проблем. Упавший владелец перестаёт учитываться, как только его запуск перестаёт быть активным, так что пережидать тайм-аут не нужно. Заявка, которая пересекается с другой, не отклоняется: она записывается, а пересечение возвращается агенту, который спрашивал, вместе с id задачи владельца, её названием, заявленными путями и заметкой. А агенту, который наткнулся на конфликт, первым делом велено отправить владельцу нужное изменение, так что у двух агентов, которым нужны файлы друг друга, есть чем заняться, кроме ожидания, хотя ничто не мешает обоим всё равно ждать. Вот инструкции, которые получает каждый агент, слово в слово:
- 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.Эти строки дописываются в пользовательский файл инструкций, который читает каждый CLI: CLAUDE.md для Claude Code и AGENTS.md для Codex, Kiro, Cursor, Hermes и OpenCode. Один и тот же механизм должен дойти до шести разных CLI, поэтому foxl-agents - команда оболочки, а не MCP-сервер. Любой CLI для программирования умеет запускать команды оболочки, так что один скрипт работает во всех шести, и ни один из них не нужно настраивать на загрузку сервера.
Что агент запускает и что он видит
Ниже реальный вывод выпущенной команды, запущенной скриптом, а не агентом, для двух демонстрационных задач в демонстрационном репозитории. Задача A - это задача Codex, которая добавляет ограничение частоты запросов, а задача B - задача Claude Code, которая добавляет пагинацию в эндпоинт пользователей. Сервер, его база данных и команда те же, что получают облачные агенты.

Задача B заявила src/api/ с косой чертой в конце, которая обозначает каталог, и сервер сохранил заявку как src/api/**. Без косой черты команда добавила бы /** сама: она работает внутри рабочей копии, единственного места, где можно отличить файл от каталога. Кроме того, команда переписывает каждый путь относительно корня репозитория, каким бы ни был рабочий каталог, и отклоняет путь за пределами рабочей копии, ещё ничего не отправив.
Конфликт называет два файла задачи A и её заметку, и задача B решает по заметке. Здесь она могла бы не трогать middleware и отправить задаче A то единственное изменение, которое ей там нужно, или взяться за собственный обработчик эндпоинта и проверить ещё раз, когда задача A снимет заявку.
Как понять, пересекаются ли два glob-шаблона?
Две заявки пересекаются, когда хотя бы один путь подходит под обе. Грамматика намеренно маленькая:
- обычный путь - это glob без подстановочных знаков;
*совпадает в пределах одного сегмента пути;?совпадает ровно с одним символом;- сегмент, который целиком равен
**, совпадает с любым числом целых сегментов; - все остальные символы буквальные, так что
[id].tsx- это имя файла, а не класс символов.
Вот что выпущенный сопоставитель возвращает для нескольких пар:
| заявка A | заявка B | пересечение |
|---|---|---|
src/api/** | src/api/users.ts | да |
src/*.ts | src/api/users.ts | нет, * не выходит за один сегмент |
src/**/*.test.ts | src/api/*.ts | да, обе подходят под src/api/x.test.ts |
docs/** | src/** | нет |
src/app/[id]/page.tsx | src/app/a/page.tsx | нет, [id] понимается буквально |
**/*.md | docs/guide.md | да |
Из-за пересечения заявка никогда не отклоняется, но несколько форм отклоняются всегда:
- glob только из подстановочных знаков, в котором есть
**, например**или**/*, потому что он заявил бы весь репозиторий; - сегмент пути
.., обратная косая черта или управляющий символ; !в начале, потому что здесь это не отрицание и не должно на него походить.
Задача держит не больше 50 заявок, каждая длиной не больше 200 символов, а заметки - до 280 символов.
Сравнение ограничено, чтобы необычный набор заявок не мог стоить серверу много процессорного времени. Две заявки, у которых различаются ведущие буквальные сегменты (docs/** и src/**), отсеиваются сразу. В остальных случаях сервер выполняет мемоизированный обход пар сегментов и, внутри сегмента, пар символов. Все сравнения в одном запросе делят бюджет в 50 000 шагов, а пара, до которой бюджет так и не дошёл, сообщается как возможное пересечение, а не отбрасывается, поэтому запрос, исчерпавший бюджет, всё равно предупреждает агента.
Когда заявка заканчивается
Заявка принадлежит одному запуску. Когда запуск заканчивается, задача снимает все свои заявки на том же шаге, на котором запуск отмечается завершённым. Запуск, который до этого шага так и не дошёл, покрывают две проверки при чтении: заявка, владелец которой больше не активен, игнорируется, как и заявка старше максимальной длительности запуска, 7 часов 20 минут. У заявок та же видимость, что и у сообщений: видеть заявки друг друга могут только активные агенты одного пользователя, которые работают через одну и ту же установку GitHub App в одном и том же репозитории.

Сколько это стоит на бесплатном плане базы данных
Заявки хранятся в одной таблице с одним индексом по (пользователь, репозиторий, время создания), и никто не опрашивает её в цикле. Каждое чтение запрашивает только заявки, созданные за последние 7 часов 20 минут, а время создания - последний столбец индекса, поэтому старые строки оказываются вне диапазона, который сканирует база данных. Устаревшие заявки, которые никто не снял, добавляют к чтению одну строку, будь их 1000 или 2000; мы измерили оба случая.
Мы это измеряем, потому что Foxl работает на бесплатном плане Cloudflare, где все базы данных D1 аккаунта делят 5 миллионов прочитанных и 100 000 записанных строк в день, а аккаунт получает 100 000 запросов к Worker в день. Цифры ниже получены на D1, запущенной локально в workerd, среде выполнения Cloudflare, и пока не подтверждены собственными метриками D1 в продакшене:
| вызов | прочитано строк | записано строк |
|---|---|---|
| заявить N путей | около 2 на каждую активную заявку в репозитории, плюс около 1 на путь и несколько фиксированных чтений | 2 на путь |
| список заявок | 1 + 2 на активную заявку | 0 |
| снятие | около 1 на путь | 1 на снятую строку |
| завершается запуск без заявок | 0 | 0 |
При примерно 50 облачных задачах в день на всех пользователей и двух вызовах заявок на каждую это 100 вызовов в день. Каждый вызов - это два запроса к Worker, один на code.foxl.ai и один через внутренний переход к серверу, то есть около 200 запросов, или 0,2% от 100 000 бесплатного плана. Даже если бы каждый из этих вызовов стоил столько же, сколько у занятого пользователя в худшем случае (десять агентов, у каждого по пять заявок в одном репозитории), за день было бы прочитано около 11 000 строк (0,2% от 5 миллионов) и записано около 830 (0,8% от 100 000).
Действительно ли агенты следуют правилу?
Пока мы этого не знаем, и этот этап построен так, чтобы это выяснить. Нашим собственным локальным сессиям для соблюдения того же правила был нужен координатор, поэтому мы не предполагаем, что шесть разных CLI будут заявлять файлы перед правкой и отступать при конфликте только потому, что так сказано в их инструкциях. Каждый конфликт пишет запись аудита (каждый экземпляр сервера пропускает повтор для той же задачи и тех же владельцев в течение десяти минут), как и каждое сообщение, которое один агент отправляет другому. Написал ли агент владельцу, видно в журнале аудита, снял ли он свою заявку - в таблице заявок, а правил ли он файл всё равно - в diff pull request.
Следующий этап зависит от того, что это покажет. Если агенты действительно уважают заявки, план такой: та часть Foxl Code, которая разбивает один запрос на несколько агентов, будет давать каждой подзадаче список файлов и запускать пересекающиеся подзадачи одну за другой, а не одновременно. На 2026-10-23 таблица в продакшене пуста, потому что после развёртывания функции не запустилось ни одной облачной задачи.
Тем, кто строит что-то похожее, мы бы посоветовали сохранить три свойства: заявка всегда записывается, пересечение возвращается с владельцем и заметкой, а заявка живёт ровно столько, сколько её запуск. В нашей статье к запуску Foxl Code рассказано о самом цикле агента, который сверяется с целью, а не доверяет каждому шагу (на английском).
Источники и дополнительное чтение
- Цены Cloudflare D1 и лимиты бесплатного планаДокументация
- Выпуск Foxl v0.9.3-beta.5Выпуск