ADR 0017 — Блокировка инициализации workspace, Monitor dev agents и наследование пределов
- Статус: принят
- Дата: 2026-07-25
- Фаза: 4a (исправления после аудита)
Контекст
Аудит Фазы 4a по 7 пунктам исходной сессии и критерию приёмки. Четыре подтверждённых дефекта (один из них ломал критерий приёмки) и нестабильность ~50% в suite engine. Этот ADR фиксирует структурные изменения; исходные решения — в ADR 0011.
Решения
1. Workspace.ensure!/3 сериализует инициализацию по проекту (пересматривает 0011 §4)
ADR 0011 принял гонку рабочего дерева как "не являющуюся требованием критерия
приёмки". Являлась: при активации N dev agents проекта поднимаются вместе, и все
вызывают ensure!/3, видя рабочее дерево ещё несуществующим. git init/fetch
сталкивались в одном каталоге (could not lock config file, cannot copy .../hooks/*.sample). Измерено: при 8 агентах умирали 7; при 2 — числе
критерия приёмки — умирал 1, воспроизводимо в 6/6 запусков.
Инициализация начинает выполняться внутри :global.trans({{Workspace, project_id}, self()}), с повторной проверкой внутри критической секции (тот, кто ждал, находит рабочее
дерево готовым). Блокировка — на проект: разные проекты идут параллельно.
Горячий путь (рабочее дерево уже существует) даже не касается блокировки.
2. Workspace.ensure/3 не бросает исключение; сбой worktree возвращает task
ensure!/3 бросал MatchError, а DevAgentServer — restart: :temporary:
агент умирал окончательно. Хуже: task уже была забрана (in_progress,
blocked = false), оставаясь невидимой для claim (который берёт только todo) и вне
досягаемости unblock. Застрявшая task без пути восстановления через UI.
Новый ensure/3 возвращает {:ok, dir} | {:error, mensagem}, используемый
WorktreeManager.create/3; DevAgentServer фиксирует task_id в state до
сборки worktree и вызывает block_task/3 при сбое.
3. Engine.Dev.Monitor — dev agents получают Monitor, который был только у сессий
DevAgentState.delete/2 существовал без единого места вызова: строка переживала
процесс, а DevRehydrator при каждой загрузке воскрешал каждого dev agent, который когда-либо
существовал (включая убитых crash, которые возвращались без рабочего цикла и
удерживали agent_id в Registry, — делая WorktreeCleanupWorker безвредным,
поскольку у "живого" агента никогда нет осиротевшего worktree).
Новый Engine.Dev.Monitor повторяет Engine.Sessions.Monitor, с различием,
которого у монитора сессий нет: :shutdown сохраняет строку (это ровно тот случай,
который покрывает регидратация, — узел, уходящий вниз), любая другая причина её удаляет.
Оба Monitor — синглтоны: если они умирают, engine разом теряет
наблюдение за всеми наблюдаемыми процессами. delete в базе начинает быть
защищённым (rescue/catch :exit) в обоих — недоступность базы не
может иметь такого эффекта. Это была и корневая причина оставшейся нестабильности suite.
4. Субагент распараллеливания наследует пределы базового агента
parallelize поднимал dev-<modulo>-2 со значениями по умолчанию nil. Поскольку защита
ToolLoop — when is_integer(budget), nil означает без ограничений: принятие
одним кликом создавало агента без предела расходов. api не может служить источником — она не
сохраняет бюджет, выбранный при активации, — поэтому дополнительный агент наследует от долговечного
состояния базового агента; без базового агента engine отказывает с 409 вместо создания
агента без предела. AcceptParallelizationUseCase начинает вызывать engine
до записи события, поскольку event log неизменяем.
5. persist/1 явно записывает max_gate_corrections
Колонка есть в списке :replace в on_conflict; её опущение в upsert обнуляло
её в первом цикле task. Gates читают это поле из базы
(qa_agent_server/secops_agent_server), поэтому выбранный пользователем предел
молча становился DEFAULT_MAX_GATE_CORRECTIONS = 3 api.
Последствия
- Критерий приёмки двух параллельных dev начинает действовать на новом проекте.
- Новые тесты: 8 конкурентных
ensure/3на одном проекте;ensure/3, возвращающий ошибку без исключения; сбой worktree, возвращающий task; пределы, переживающиеpersist; Monitor, удаляющий при crash и сохраняющий при:shutdown; наследование пределов при распараллеливании и 409 без базового агента. - Suite engine: 173 теста, 12/12 зелёных запусков (базовая линия: 9 падений в 18 запусках).
workspace_test.exsиworkspace_files_test.exsстановятсяasync: false— это были единственные модулиasync: true, изменявшие глобальныйApplication.env:project_workspaces_root.
Не решено (аудит)
Зафиксировано то, что нашёл аудит и чего эта сессия не касается: предложение
распараллеливания по-прежнему count >= 2, а не граф зависимостей (в схеме нет зависимостей
между tasks); dev_agent_states.status никогда не выходит из "working";
ветка не сохраняется; замок merge не покрывает git_merge без targetBranch
в payload (сегодня недостижимо — ни один агент не предлагает git_merge); а
outbox опустошает только aggregate_type = "session".