Перейти к основному содержимому

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".