fix(pipeline-status): MCP wrapper timeout 60s kills 5min CI poll cycle #67

Closed
opened 2026-08-12 23:10:59 +03:00 by slaid098 · 0 comments
Owner

Контекст

pipeline-status MCP tool используется оркестратором для определения фазы pipeline (ISSUE → IMPLEMENT → CI → REVIEW → MERGE → MEMORY) на каждой итерации /run-pipeline.

Tool заявляет в description: «Blocks up to 5 min while CI runs (polling CI). Returns DONE on green, NOT_DONE on failure, AMBIGUOUS on timeout/API error.». По факту MCP-обёртка убивает процесс через 60s, и пользователь видит ⚠️ pipeline_status failed (timed out (60s)): вместо verdict'а, пока CI ещё бежит (статус IN_PROGRESS). После завершения CI оракул отрабатывает нормально (<1s).

Корневая причина — несоответствие таймаутов:

  • .opencode/tools/pipeline-status.ts:14 — MCP-обёртка жёстко режет процесс через timeout: 60000 (60s) в spawnSync(..., { timeout: 60000 }).
  • .opencode/scripts/pipeline-status.py:61 — Python-оракул объявляет CI_WAIT_TIMEOUT = 300 (5 минут poll-цикл для CI в IN_PROGRESS).
  • Когда CI IN_PROGRESS, оракул уходит в poll-цикл _classify_rollup_with_poll (.opencode/scripts/pipeline-status.py:697, time.sleep(config.poll_interval) на строке 706, цикл до ~300s). MCP через 60s убивает процесс SIGTERM'ом → пользователь видит ⚠️ pipeline_status failed (timed out (60s)):.

Задача

Согласовать таймауты MCP-обёртки и Python-оракула, чтобы описание tool'а соответствовало реальному поведению. Один из вариантов (на усмотрение исполнителя):

  • Поднять .opencode/tools/pipeline-status.ts:14 timeout: 60000 → 310000 (5мин10с, чуть больше CI_WAIT_TIMEOUT=300) — соответствует заявленному description.
  • ИЛИ снизить .opencode/scripts/pipeline-status.py:61 CI_WAIT_TIMEOUT = 300 → значение, вписывающееся в 60s (минус: длинный CI будет prematurely AMBIGUOUS, нарушается обещание «up to 5 min» в description — потребует правки description).
  • ИЛИ гибрид: передавать --ci-wait-timeout аргумент из MCP-обёртки в скрипт, чтобы таймаут управлялся из одного места (MCP-обёртка = timeout + 10000 от переданного CI_WAIT_TIMEOUT).

Предпочтительный вариант — первый (поднять MCP-таймаут), т.к. он не нарушает внешний контракт description и не ухудшает UX при длинном CI.

Контракты

  • Внешний контракт tool'а (description в .opencode/opencode.json/tool({ description })) НЕ меняется — уже обещает «up to 5 min». При выборе варианта со снижением CI_WAIT_TIMEOUT — description нужно править под новое поведение.
  • Возвращаемое значение: DONE / NOT_DONE / AMBIGUOUS — без изменений.
  • Параметры: pr_number: number — без изменений.
  • CLI-контракт скрипта python3 scripts/pipeline-status.py <pr_number> — без изменений (если не выбран гибридный вариант с --ci-wait-timeout, тогда это новое опциональное расширение).

Инварианты

  • Оркестратор продолжает получать осмысленный verdict (DONE/NOT_DONE/AMBIGUOUS) вместо ⚠️ pipeline_status failed (timed out (60s)).
  • При CI длиннее 5 минут — AMBIGUOUS (не бесконечный poll). Гарантия верхнего предела сохраняется.
  • При CI < 60s — поведение не меняется (poll либо не запускается, либо успевает завершиться).
  • spawnSync остаётся синхронным блокирующим вызовом (оракул по дизайну блокирующий — это заявлено в description).

Граничные случаи

  • CI мгновенно завершён (success/failure) — poll не запускается, ответ <1s (как сейчас).
  • CI ровно 60s — сейчас SIGTERM после 60s, после фикса — нормальный verdict в рамках poll-цикла.
  • CI ровно 300s (граница CI_WAIT_TIMEOUT) — после фикса оракул корректно возвращает AMBIGUOUS, MCP не убивает процесс раньше.
  • CI > 5 минут — AMBIGUOUS по контракту (poll-цикл сам выходит по CI_WAIT_TIMEOUT).
  • Forgejo API недоступен / 5xx / rate-limit — отдельный кейс ретраев (FORGEJO_RETRY_INTERVAL_S), не этот баг.
  • Скрипт завершается с ненулевым exit code ДО poll-цикла (например, PR не найден) — MCP-обёртка уже корректно отрабатывает через r.status !== 0, этот путь не затрагивается.

Влияние на связанные компоненты

  • run-pipeline skill — полагается на pipeline-status на каждой итерации (особенно фаза CI). Сейчас pipeline может стопориться / выдавать ложный негатив, когда CI бежит 1-5 минут: оркестратор видит timed out (60s) вместо NOT_DONE и не может корректно дождаться зелёного CI.
  • Все PR-pipeline'ы в slaid098 репо, использующие /run-pipeline — страдают от этого бага, пока CI бежит дольше 60s. Это особенно актуально для репо с медленным CI (fullstack с Playwright/pytest).
  • pipeline-status tool также используется вручную оркестратором на фазах REVIEW/MERGE для проверки, что CI зелёный — те же симптомы.
  • spec-status tool — НЕ затрагивается (другой скрипт, нет poll-цикла на CI).

Вне scope

  • Логика классификации verdict'ов (_classify_rollup, _classify_rollup_with_poll, _classify_rollup_in_progress) — не трогаем. Только таймауты.
  • Forgejo API endpoints и retry-логика (_env_int("FORGEJO_RETRY_INTERVAL_S", ...), CI_NO_RUNS_INTERVAL) — не трогаем.
  • Другие tools (merge-pr, post-review, project-status, spec-status) — не трогаем.
  • Формат вывода verdict'а (строка с DONE/NOT_DONE/AMBIGUOUS + NEXT:) — не трогаем.
  • Перевод spawnSync на асинхронную модель — не трогаем (оракул намеренно блокирующий).

Критерии приемки

  • pipeline-status отдаёт verdict (DONE/NOT_DONE/AMBIGUOUS) когда CI бежит 1-5 минут, без ⚠️ pipeline_status failed (timed out (60s)).
  • Description tool'а (в tool({ description }) и в .opencode/opencode.json если дублирует) соответствует реальному поведению — «up to 5 min» действительно означает до ~5 минут poll, а не 60s.
  • Ручной запуск python3 .opencode/scripts/pipeline-status.py <pr> на PR с IN_PROGRESS CI — завершается естественным образом verdict'ом, не SIGTERM'ом от MCP-обёртки.
  • При CI > 5 минут — AMBIGUOUS по контракту (верхний предел сохраняется, не бесконечный poll).
  • Существующие тесты pipeline-status (если есть в tests/) — проходят. Если тестов нет — добавить минимальный тест на таймаут-контракт (опционально, на усмотрение исполнителя).
  • Согласованность значений: MCP timeout >= CI_WAIT_TIMEOUT + buffer (чтобы скрипт успел вернуть verdict до того, как MCP его убьёт).
## Контекст `pipeline-status` MCP tool используется оркестратором для определения фазы pipeline (ISSUE → IMPLEMENT → CI → REVIEW → MERGE → MEMORY) на каждой итерации `/run-pipeline`. Tool заявляет в description: «Blocks up to 5 min while CI runs (polling CI). Returns DONE on green, NOT_DONE on failure, AMBIGUOUS on timeout/API error.». По факту MCP-обёртка убивает процесс через 60s, и пользователь видит `⚠️ pipeline_status failed (timed out (60s)):` вместо verdict'а, пока CI ещё бежит (статус IN_PROGRESS). После завершения CI оракул отрабатывает нормально (<1s). Корневая причина — несоответствие таймаутов: - `.opencode/tools/pipeline-status.ts:14` — MCP-обёртка жёстко режет процесс через `timeout: 60000` (60s) в `spawnSync(..., { timeout: 60000 })`. - `.opencode/scripts/pipeline-status.py:61` — Python-оракул объявляет `CI_WAIT_TIMEOUT = 300` (5 минут poll-цикл для CI в IN_PROGRESS). - Когда CI IN_PROGRESS, оракул уходит в poll-цикл `_classify_rollup_with_poll` (`.opencode/scripts/pipeline-status.py:697`, `time.sleep(config.poll_interval)` на строке 706, цикл до ~300s). MCP через 60s убивает процесс SIGTERM'ом → пользователь видит `⚠️ pipeline_status failed (timed out (60s)):`. ## Задача Согласовать таймауты MCP-обёртки и Python-оракула, чтобы описание tool'а соответствовало реальному поведению. Один из вариантов (на усмотрение исполнителя): - Поднять `.opencode/tools/pipeline-status.ts:14` `timeout: 60000` → `310000` (5мин10с, чуть больше `CI_WAIT_TIMEOUT=300`) — соответствует заявленному description. - ИЛИ снизить `.opencode/scripts/pipeline-status.py:61` `CI_WAIT_TIMEOUT = 300` → значение, вписывающееся в 60s (минус: длинный CI будет prematurely AMBIGUOUS, нарушается обещание «up to 5 min» в description — потребует правки description). - ИЛИ гибрид: передавать `--ci-wait-timeout` аргумент из MCP-обёртки в скрипт, чтобы таймаут управлялся из одного места (MCP-обёртка = `timeout + 10000` от переданного `CI_WAIT_TIMEOUT`). Предпочтительный вариант — первый (поднять MCP-таймаут), т.к. он не нарушает внешний контракт description и не ухудшает UX при длинном CI. ## Контракты - Внешний контракт tool'а (description в `.opencode/opencode.json`/`tool({ description })`) НЕ меняется — уже обещает «up to 5 min». При выборе варианта со снижением `CI_WAIT_TIMEOUT` — description нужно править под новое поведение. - Возвращаемое значение: `DONE` / `NOT_DONE` / `AMBIGUOUS` — без изменений. - Параметры: `pr_number: number` — без изменений. - CLI-контракт скрипта `python3 scripts/pipeline-status.py <pr_number>` — без изменений (если не выбран гибридный вариант с `--ci-wait-timeout`, тогда это новое опциональное расширение). ## Инварианты - Оркестратор продолжает получать осмысленный verdict (`DONE`/`NOT_DONE`/`AMBIGUOUS`) вместо `⚠️ pipeline_status failed (timed out (60s))`. - При CI длиннее 5 минут — `AMBIGUOUS` (не бесконечный poll). Гарантия верхнего предела сохраняется. - При CI < 60s — поведение не меняется (poll либо не запускается, либо успевает завершиться). - `spawnSync` остаётся синхронным блокирующим вызовом (оракул по дизайну блокирующий — это заявлено в description). ## Граничные случаи - CI мгновенно завершён (success/failure) — poll не запускается, ответ <1s (как сейчас). - CI ровно 60s — сейчас SIGTERM после 60s, после фикса — нормальный verdict в рамках poll-цикла. - CI ровно 300s (граница `CI_WAIT_TIMEOUT`) — после фикса оракул корректно возвращает `AMBIGUOUS`, MCP не убивает процесс раньше. - CI > 5 минут — `AMBIGUOUS` по контракту (poll-цикл сам выходит по `CI_WAIT_TIMEOUT`). - Forgejo API недоступен / 5xx / rate-limit — отдельный кейс ретраев (`FORGEJO_RETRY_INTERVAL_S`), не этот баг. - Скрипт завершается с ненулевым exit code ДО poll-цикла (например, PR не найден) — MCP-обёртка уже корректно отрабатывает через `r.status !== 0`, этот путь не затрагивается. ## Влияние на связанные компоненты - `run-pipeline` skill — полагается на `pipeline-status` на каждой итерации (особенно фаза CI). Сейчас pipeline может стопориться / выдавать ложный негатив, когда CI бежит 1-5 минут: оркестратор видит `timed out (60s)` вместо `NOT_DONE` и не может корректно дождаться зелёного CI. - Все PR-pipeline'ы в slaid098 репо, использующие `/run-pipeline` — страдают от этого бага, пока CI бежит дольше 60s. Это особенно актуально для репо с медленным CI (fullstack с Playwright/pytest). - `pipeline-status` tool также используется вручную оркестратором на фазах REVIEW/MERGE для проверки, что CI зелёный — те же симптомы. - `spec-status` tool — НЕ затрагивается (другой скрипт, нет poll-цикла на CI). ## Вне scope - Логика классификации verdict'ов (`_classify_rollup`, `_classify_rollup_with_poll`, `_classify_rollup_in_progress`) — не трогаем. Только таймауты. - Forgejo API endpoints и retry-логика (`_env_int("FORGEJO_RETRY_INTERVAL_S", ...)`, `CI_NO_RUNS_INTERVAL`) — не трогаем. - Другие tools (`merge-pr`, `post-review`, `project-status`, `spec-status`) — не трогаем. - Формат вывода verdict'а (строка с `DONE`/`NOT_DONE`/`AMBIGUOUS` + `NEXT:`) — не трогаем. - Перевод `spawnSync` на асинхронную модель — не трогаем (оракул намеренно блокирующий). ## Критерии приемки - [ ] `pipeline-status` отдаёт verdict (`DONE`/`NOT_DONE`/`AMBIGUOUS`) когда CI бежит 1-5 минут, без `⚠️ pipeline_status failed (timed out (60s))`. - [ ] Description tool'а (в `tool({ description })` и в `.opencode/opencode.json` если дублирует) соответствует реальному поведению — «up to 5 min» действительно означает до ~5 минут poll, а не 60s. - [ ] Ручной запуск `python3 .opencode/scripts/pipeline-status.py <pr>` на PR с IN_PROGRESS CI — завершается естественным образом verdict'ом, не SIGTERM'ом от MCP-обёртки. - [ ] При CI > 5 минут — `AMBIGUOUS` по контракту (верхний предел сохраняется, не бесконечный poll). - [ ] Существующие тесты `pipeline-status` (если есть в `tests/`) — проходят. Если тестов нет — добавить минимальный тест на таймаут-контракт (опционально, на усмотрение исполнителя). - [ ] Согласованность значений: MCP `timeout` >= `CI_WAIT_TIMEOUT + buffer` (чтобы скрипт успел вернуть verdict до того, как MCP его убьёт).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
slaid098/opencode-config#67
No description provided.