Files
orchestrator/docs/history/BUGFIXES_2026-06-02.md
Dev Agent 7c68d1d812
All checks were successful
CI / test (pull_request) Successful in 9s
docs(orchestrator): adopt enduro doc canon + CLAUDE.md + ADR (ORCH-9)
2026-06-05 12:33:55 +03:00

8.4 KiB
Raw Blame History

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_verdict frontmatter 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)