8.4 KiB
Bugfixes 2026-06-02 — устранение багов оркестратора
Источник: tasks/multi-agent/AUDIT_2026-06-02.md
Цель: вернуть автономность мультиагентного pipeline (ET-009: 0/6 этапов были автономны).
Исполнитель: Dev-агент (Opus 4.8 Tokenator).
Что починено
B-1 — запись .task-*.md без docker
Было: launcher._write_task_file() писал файл через docker run --rm -i python:3.12-slim bash -c "cat > ...". Бинарника docker в контейнере НЕТ → запись падала молча → агент читал старый task-файл.
Стало: прямая запись в смонтированный volume /repos/<repo>/<task_file> обычным open(..., "w"). При ошибке записи — RuntimeError (не молчит).
Файл: src/agents/launcher.py (_write_task_file, вызов в launch).
Проверка:
docker exec orchestrator python3 -c "
import sys; sys.path.insert(0,'/repos/orchestrator')
from src.agents.launcher import launcher
launcher._write_task_file('enduro-trails', '.task-test-write.md', 'hello-from-fix')
print(open('/repos/enduro-trails/.task-test-write.md').read())"
# => hello-from-fix (без docker)
✅ Verified: READBACK = hello-from-fix.
B-2 — Popen stdout → файл, убран PIPE-поток (зомби, потеря exit_code)
Было: Popen(stdout=PIPE) + daemon-поток с select/readline + startup-timeout 120с. → PIPE-deadlock, зомби при рестарте, exit_code=None в БД (все прогоны ET-009).
Стало: log_fh = open(output_path, "w"); Popen(stdout=log_fh, stderr=STDOUT). _monitor_agent упрощён до proc.wait() + log_fh.close(). PIPE-поток и startup-timeout удалены. Watchdog по pid (AGENT_TIMEOUT) сохранён.
Файл: src/agents/launcher.py (launch, _monitor_agent).
Проверка: после прогона SELECT exit_code FROM agent_runs ORDER BY id DESC LIMIT 1 != NULL; ps aux | grep defunct — пусто.
B-3 — .task-*.md в .gitignore, не коммитятся
Было: task-файлы трекались в git (.task-arch.md, .task-dev.md, .task-review.md, .task.md) и тащились между задачами.
Стало: в enduro-trails/.gitignore добавлено .task*.md; трекаемые файлы убраны из индекса (git rm --cached).
Файл: enduro-trails/.gitignore (+ untrack). Ветка main protected → изменения в PR #19 (chore/gitignore-task-files).
Проверка: git check-ignore .task.md .task-arch.md → matched. git add docs/ src/ tests/ (scoped) не цепляют task-файлы.
S-5 — машиночитаемый verdict ревьюера
Было: check_reviewer_verdict искал подстроки APPROVED/REQUEST_CHANGES во всём тексте (5000 байт) → ложные срабатывания на таблицах.
Стало: читается ТОЛЬКО verdict: из YAML-frontmatter 12-review.md (через yaml.safe_load). Нет verdict / нет frontmatter → not-approved. reviewer.md обновлён: требование frontmatter verdict: APPROVED|REQUEST_CHANGES.
Файлы: src/qg/checks.py (check_reviewer_verdict), enduro-trails/.openclaw/agents/reviewer.md (PR #19; рабочая копия применена сразу).
Проверка: ET-009 12-review.md (frontmatter verdict: APPROVED) → (True, 'Reviewer verdict: APPROVED'). Unit-тесты покрывают APPROVED/REQUEST_CHANGES/no-verdict/no-frontmatter/таблица-в-теле.
S-1 — QG тестов гоняет сам оркестратор (не Gitea CI)
Было: development → review QG = check_ci_green (Gitea status). CI не настроен → всегда false → автопереход не происходил + ложные «CI failed» алерты.
Стало: новый QG check_tests_local — оркестратор делает git fetch/checkout <branch> + make test в /repos/<repo>, judge по exit-code. stages.py: development QG → check_tests_local. Dispatch добавлен в launcher._try_advance_stage и webhooks/plane._try_advance_stage (args (repo, branch)). webhooks/gitea.handle_ci_status: failure → debug-лог, без notify_error.
Файлы: src/qg/checks.py, src/stages.py, src/agents/launcher.py, src/webhooks/plane.py, src/webhooks/gitea.py.
Грабля (известное ограничение): check_tests_local делает checkout в shared /repos — небезопасно при параллельных задачах (S-4 worktree — отдельно).
M-1 — нормальный orphan-recovery
Было: UPDATE agent_runs SET exit_code=-1 WHERE finished_at IS NULL AND started_at < now-35min — молча списывал зомби.
Стало: перечисляем каждый orphan-run, помечаем exit=-1, логируем per-run warning («manual check needed»), отправляем Telegram-уведомление. Не автоперезапускаем (риск зацикливания). Killing по pid невозможен — pid не персистится в БД (задокументировано).
Файл: src/main.py (lifespan).
Что НЕ входило (отдельные задачи)
- S-2/S-3 (rollback деплоера в shared-репо), S-4 (git worktree per task), M-3 (единый stage-engine), F-2b (очередь задач), M-7 (идемпотентность webhook).
_auto_merge_pr— мёртвый код оставлен (отдельная чистка).
Тесты
- Новый файл
tests/test_launcher.py: 10 тестов (_write_task_fileпишет/raise/без docker;check_reviewer_verdictfrontmatter cases). tests/test_qg.py: 16 passed.tests/test_launcher.py: 10 passed.- ⚠️ Pre-existing:
tests/test_webhooks.pyимеет падения (401/signature + cross-file env pollution) — НЕ связаны с этими фиксами, существовали до правок. Запуск в изоляции part-passes; в общем прогоне больше падений из-за общего env/DB между тест-файлами. Гигиена test_webhooks — отдельная задача.
Деплой
Оркестратор пересобран: cd /home/slin/repos/orchestrator && docker compose up -d --build. Health: {"status":"ok"}.
Дополнительно найдено и починено в ходе теста автономности
git safe.directory (launcher commit/push)
В ходе теста выяснилось: git внутри контейнера (root) над bind-mounted /repos падал с "dubious ownership" → авто-commit/push агента не проходил. Фикс: git config --system --add safe.directory "*" в Dockerfile. Теперь _monitor_agent commit+push работает автономно (проверено: analyst(ET): auto-commit run_id=47 запушен в origin).
init:true (PID-1 reaper) — добиваем B-2
Прямой child (bash) reap-ался корректно через proc.wait(), НО claude (node) порождает свои дочерние процессы; при выходе bash они реparent-ились на PID 1 (uvicorn), который их НЕ reap-ал → grandchild-зомби. Фикс: init: true в docker-compose.yml — Docker внедряет docker-init(tini) как PID 1. Проверено: после реального прогона агента ZOMBIE_COUNT_AFTER=0.
Тест автономности (Task 9) — РЕЗУЛЬТАТ
Запуск через launcher.launch("analyst", ...) (НЕ base64). Подтверждено автономно:
- B-1: свежий
.task.mdзаписан без docker (which docker = NO_DOCKER_BINARY) - B-2:
exit_code=0вagent_runs(run 46/47/48) - зомби: 0 после прогона (tini reaper)
- git: auto-commit + push в origin отработал
- M-1: при рестарте orphan-recovery залогировал per-run + Telegram (runs 42/43/44 ET-009)