From 4f52825932ddccb3f9a5db39659f4a3645a6c7b5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 13 Jul 2026 15:17:20 +0900 Subject: [PATCH] fix(opencode): raise review cadence so the direct-OpenAI slot can finish substantive reviews Since the GitHub Models org budget cap, gpt-5.6-luna is the only model-pool candidate producing verdicts, and the dynamic review cadence tightened in #495 (small 300s/600s, medium 420s/900s, large 420s/1080s) no longer fits it: on 2026-07-13 (~04:10Z onward) every substantive PR across the org failed opencode-review with MODEL_OUTPUT_UNAVAILABLE because luna timed out mid-review (exit 124 after producing hundreds of KB of events) while only one-line dependabot diffs passed. The org merge queue stalled. - raise per-attempt/total budgets: small 900s/1500s, medium 1500s/2400s, large 1800s/2700s, unknown 1500s/2400s - raise the pool step shell watchdog 1080s -> 2880s, the pool step cap 20 -> 50 minutes, and the review job cap 45 -> 75 minutes so the shell watchdog still fires before the runner kills the step - keep the queue-protection structure of #495 intact: one attempt per model, single catalog cycle, outer watchdog, continue-on-error - update the contract pins in tests/test_opencode_agent_contract.py and scripts/ci/test_strix_quick_gate.sh in lockstep Verified: pytest tests/ -> 377 passed, 9 skipped on this branch. Co-Authored-By: Claude Fable 5 --- .github/workflows/opencode-review.yml | 37 +++++++++++++++------------ scripts/ci/test_strix_quick_gate.sh | 2 +- tests/test_opencode_agent_contract.py | 34 ++++++++++++------------ 3 files changed, 39 insertions(+), 34 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 928e1ddc7..1e3d1c384 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -1529,10 +1529,10 @@ jobs: ) runs-on: ubuntu-latest # Coverage and current-head evidence are prepared before the model pool. - # The model pool has a 25-minute retry budget and its publish gate has an - # 8-minute cap, so 45 minutes releases genuinely hung runners without - # truncating the bounded multi-provider review path. - timeout-minutes: 45 + # The model pool step is capped at 50 minutes (2880s shell watchdog) and + # its publish gate has an 8-minute cap, so 75 minutes releases genuinely + # hung runners without truncating the bounded multi-provider review path. + timeout-minutes: 75 permissions: actions: read checks: read @@ -3251,7 +3251,7 @@ jobs: - name: Run OpenCode PR Review model pool id: opencode_review_model_pool if: needs.coverage-evidence.result == 'success' - timeout-minutes: 20 + timeout-minutes: 50 continue-on-error: true env: STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }} @@ -3285,7 +3285,7 @@ jobs: OPENCODE_RUN_TIMEOUT_SECONDS: "300" OPENCODE_EXPORT_TIMEOUT_SECONDS: "120" OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "600" - OPENCODE_POOL_STEP_TIMEOUT_SECONDS: "1080" + OPENCODE_POOL_STEP_TIMEOUT_SECONDS: "2880" # Visit the high-sensitivity candidate catalog once. Per-provider # failures and timeouts are logged before the publish gate evaluates # the current-head result and fails closed when no verdict exists. @@ -3293,14 +3293,19 @@ jobs: OPENCODE_DYNAMIC_REVIEW_CADENCE: "true" OPENCODE_SMALL_CHANGE_FILE_THRESHOLD: "3" OPENCODE_MEDIUM_CHANGE_FILE_THRESHOLD: "20" - OPENCODE_SMALL_CHANGE_RUN_TIMEOUT_SECONDS: "300" - OPENCODE_SMALL_CHANGE_TOTAL_BUDGET_SECONDS: "600" - OPENCODE_MEDIUM_CHANGE_RUN_TIMEOUT_SECONDS: "420" - OPENCODE_MEDIUM_CHANGE_TOTAL_BUDGET_SECONDS: "900" - OPENCODE_LARGE_CHANGE_RUN_TIMEOUT_SECONDS: "420" - OPENCODE_LARGE_CHANGE_TOTAL_BUDGET_SECONDS: "1080" - OPENCODE_UNKNOWN_CHANGE_RUN_TIMEOUT_SECONDS: "420" - OPENCODE_UNKNOWN_CHANGE_TOTAL_BUDGET_SECONDS: "900" + # Cadence sized for the direct-OpenAI slot (gpt-5.6-luna), the only + # candidate producing verdicts while the GitHub Models org budget is + # capped: at the previous 420s/900s a substantive multi-file review + # timed out mid-run and the exhausted pool stalled the org merge + # queue (2026-07-13). The outer watchdog still bounds the step. + OPENCODE_SMALL_CHANGE_RUN_TIMEOUT_SECONDS: "900" + OPENCODE_SMALL_CHANGE_TOTAL_BUDGET_SECONDS: "1500" + OPENCODE_MEDIUM_CHANGE_RUN_TIMEOUT_SECONDS: "1500" + OPENCODE_MEDIUM_CHANGE_TOTAL_BUDGET_SECONDS: "2400" + OPENCODE_LARGE_CHANGE_RUN_TIMEOUT_SECONDS: "1800" + OPENCODE_LARGE_CHANGE_TOTAL_BUDGET_SECONDS: "2700" + OPENCODE_UNKNOWN_CHANGE_RUN_TIMEOUT_SECONDS: "1500" + OPENCODE_UNKNOWN_CHANGE_TOTAL_BUDGET_SECONDS: "2400" OPENCODE_DYNAMIC_MAX_CYCLES: "1" CENTRAL_REVIEW_PROCESS_FALLBACK_ELIGIBLE: ${{ steps.central_review_process_fallback_scope.outputs.eligible || 'false' }} CENTRAL_REVIEW_PROCESS_FALLBACK_SCOPE_LABEL: ${{ steps.central_review_process_fallback_scope.outputs.scope_label || 'unsupported' }} @@ -3326,13 +3331,13 @@ jobs: run: | set -euo pipefail set +e - timeout --kill-after=30s "${OPENCODE_POOL_STEP_TIMEOUT_SECONDS:-1080}s" \ + timeout --kill-after=30s "${OPENCODE_POOL_STEP_TIMEOUT_SECONDS:-2880}s" \ bash "$GITHUB_WORKSPACE/scripts/ci/run_opencode_review_model_pool.sh" pool_status=$? set -e if [ "$pool_status" -eq 124 ] || [ "$pool_status" -eq 137 ] || [ "$pool_status" -eq 143 ]; then printf 'OpenCode model pool exceeded the outer %ss step budget; marking the pool exhausted so current-head evidence fallback can publish a bounded reason instead of blocking the org queue.\n' \ - "${OPENCODE_POOL_STEP_TIMEOUT_SECONDS:-1080}" + "${OPENCODE_POOL_STEP_TIMEOUT_SECONDS:-2880}" { printf 'review_model=\n' printf 'review_status=exhausted\n' diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index ab61094b9..df67cdd94 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -569,7 +569,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "not a generic model-exhaustion message" "opencode review tells models to return concrete missing-evidence findings instead of progress-only output" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "tokens_limit_reached" "opencode review detects provider context-window overflow" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "skipping remaining attempts for this model" "opencode review skips same-model retries after context-window overflow" - assert_file_contains "$workflow_file" 'timeout-minutes: 45' "opencode review target releases stalled review runners within the bounded queue budget" + assert_file_contains "$workflow_file" 'timeout-minutes: 75' "opencode review target releases stalled review runners within the bounded queue budget" assert_file_contains "$workflow_file" 'timeout-minutes: 12' "opencode evidence preparation fails closed before it ties up the review queue" assert_file_contains "$workflow_file" 'timeout-minutes: 12' "opencode model pool gives multiple candidates a bounded review window while capping stalled model attempts" assert_file_contains "$workflow_file" 'timeout-minutes: 8' "opencode approval publication is bounded so a stalled review releases queue capacity promptly" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index f478459eb..c297b70a3 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -670,17 +670,17 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert '"## Check outcome"' not in workflow assert "publish REQUEST_CHANGES when coverage-evidence blocker states" in workflow assert re.search(r"Prepare bounded OpenCode review evidence[\s\S]{0,120}timeout-minutes: 12", workflow) - assert re.search(r"opencode-review-target:[\s\S]*?timeout-minutes: 45", workflow) + assert re.search(r"opencode-review-target:[\s\S]*?timeout-minutes: 75", workflow) assert 'timeout-minutes: 12' in workflow - assert re.search(r"Run OpenCode PR Review model pool[\s\S]{0,240}timeout-minutes: 20", workflow) - assert 'OPENCODE_SMALL_CHANGE_TOTAL_BUDGET_SECONDS: "600"' in workflow - assert 'OPENCODE_MEDIUM_CHANGE_TOTAL_BUDGET_SECONDS: "900"' in workflow - assert 'OPENCODE_LARGE_CHANGE_TOTAL_BUDGET_SECONDS: "1080"' in workflow - assert 'OPENCODE_UNKNOWN_CHANGE_TOTAL_BUDGET_SECONDS: "900"' in workflow + assert re.search(r"Run OpenCode PR Review model pool[\s\S]{0,240}timeout-minutes: 50", workflow) + assert 'OPENCODE_SMALL_CHANGE_TOTAL_BUDGET_SECONDS: "1500"' in workflow + assert 'OPENCODE_MEDIUM_CHANGE_TOTAL_BUDGET_SECONDS: "2400"' in workflow + assert 'OPENCODE_LARGE_CHANGE_TOTAL_BUDGET_SECONDS: "2700"' in workflow + assert 'OPENCODE_UNKNOWN_CHANGE_TOTAL_BUDGET_SECONDS: "2400"' in workflow assert 'OPENCODE_RUN_TIMEOUT_SECONDS: "300"' in workflow assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "600"' in workflow - assert 'OPENCODE_POOL_STEP_TIMEOUT_SECONDS: "1080"' in workflow - assert 'timeout --kill-after=30s "${OPENCODE_POOL_STEP_TIMEOUT_SECONDS:-1080}s"' in workflow + assert 'OPENCODE_POOL_STEP_TIMEOUT_SECONDS: "2880"' in workflow + assert 'timeout --kill-after=30s "${OPENCODE_POOL_STEP_TIMEOUT_SECONDS:-2880}s"' in workflow assert "OpenCode model pool exceeded the outer" in workflow assert 'OPENCODE_POOL_MAX_CYCLES: "1"' in workflow assert re.search(r"Run OpenCode PR Review model pool[\s\S]{0,280}continue-on-error: true", workflow) @@ -703,18 +703,18 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert 'OPENCODE_RUN_TIMEOUT_SECONDS: "300"' in workflow assert 'OPENCODE_EXPORT_TIMEOUT_SECONDS: "120"' in workflow assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "600"' in workflow - assert 'OPENCODE_POOL_STEP_TIMEOUT_SECONDS: "1080"' in workflow + assert 'OPENCODE_POOL_STEP_TIMEOUT_SECONDS: "2880"' in workflow assert 'OPENCODE_POOL_MAX_CYCLES: "1"' in workflow assert 'OPENCODE_DYNAMIC_REVIEW_CADENCE: "true"' in workflow assert 'OPENCODE_CHANGED_FILES_FILE: ${{ runner.temp }}/opencode-changed-files.txt' in workflow - assert 'OPENCODE_SMALL_CHANGE_RUN_TIMEOUT_SECONDS: "300"' in workflow - assert 'OPENCODE_SMALL_CHANGE_TOTAL_BUDGET_SECONDS: "600"' in workflow - assert 'OPENCODE_MEDIUM_CHANGE_RUN_TIMEOUT_SECONDS: "420"' in workflow - assert 'OPENCODE_MEDIUM_CHANGE_TOTAL_BUDGET_SECONDS: "900"' in workflow - assert 'OPENCODE_LARGE_CHANGE_RUN_TIMEOUT_SECONDS: "420"' in workflow - assert 'OPENCODE_LARGE_CHANGE_TOTAL_BUDGET_SECONDS: "1080"' in workflow - assert 'OPENCODE_UNKNOWN_CHANGE_RUN_TIMEOUT_SECONDS: "420"' in workflow - assert 'OPENCODE_UNKNOWN_CHANGE_TOTAL_BUDGET_SECONDS: "900"' in workflow + assert 'OPENCODE_SMALL_CHANGE_RUN_TIMEOUT_SECONDS: "900"' in workflow + assert 'OPENCODE_SMALL_CHANGE_TOTAL_BUDGET_SECONDS: "1500"' in workflow + assert 'OPENCODE_MEDIUM_CHANGE_RUN_TIMEOUT_SECONDS: "1500"' in workflow + assert 'OPENCODE_MEDIUM_CHANGE_TOTAL_BUDGET_SECONDS: "2400"' in workflow + assert 'OPENCODE_LARGE_CHANGE_RUN_TIMEOUT_SECONDS: "1800"' in workflow + assert 'OPENCODE_LARGE_CHANGE_TOTAL_BUDGET_SECONDS: "2700"' in workflow + assert 'OPENCODE_UNKNOWN_CHANGE_RUN_TIMEOUT_SECONDS: "1500"' in workflow + assert 'OPENCODE_UNKNOWN_CHANGE_TOTAL_BUDGET_SECONDS: "2400"' in workflow assert 'OPENCODE_DYNAMIC_MAX_CYCLES: "1"' in workflow assert 'OPENCODE_BACKOFF_MAX_SECONDS: "30"' in workflow publish_step = workflow.split(" - name: Publish OpenCode review outcome", 1)[1].split(