diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 614b66926..f57f93e42 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -3230,7 +3230,7 @@ jobs: OPENCODE_RUN_TIMEOUT_SECONDS: "600" OPENCODE_EXPORT_TIMEOUT_SECONDS: "120" OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "3600" - OPENCODE_POOL_STEP_TIMEOUT_SECONDS: "3600" + OPENCODE_POOL_STEP_TIMEOUT_SECONDS: "2100" # Keep cycling through the high-sensitivity candidate catalog until # the retry budget or step timeout is exhausted; a single invalid # cycle can be all provider formatting noise rather than review @@ -3247,12 +3247,15 @@ jobs: OPENCODE_LARGE_CHANGE_TOTAL_BUDGET_SECONDS: "3600" OPENCODE_UNKNOWN_CHANGE_RUN_TIMEOUT_SECONDS: "600" OPENCODE_UNKNOWN_CHANGE_TOTAL_BUDGET_SECONDS: "3600" + OPENCODE_DYNAMIC_RUN_TIMEOUT_CAP_SECONDS: "600" + OPENCODE_DYNAMIC_TOTAL_BUDGET_CAP_SECONDS: "1800" + OPENCODE_DYNAMIC_MAX_CYCLES_CAP: "1" # This installation currently reports a 4k request-body limit for # GitHub Models GPT-5 endpoints even though the public catalog is # larger. Keep the exact runtime failure visible without spending a # full medium/large cadence slot after the long-context candidate. OPENCODE_GITHUB_GPT5_RUN_TIMEOUT_SECONDS: "45" - OPENCODE_DYNAMIC_MAX_CYCLES: "0" + 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' }} OPENCODE_CENTRAL_REVIEW_PROCESS_FALLBACK_RUN_TIMEOUT_SECONDS: "600" diff --git a/scripts/ci/run_opencode_review_model_pool.sh b/scripts/ci/run_opencode_review_model_pool.sh index 0e5122748..4ca47b1a5 100644 --- a/scripts/ci/run_opencode_review_model_pool.sh +++ b/scripts/ci/run_opencode_review_model_pool.sh @@ -253,6 +253,38 @@ env_integer_or_default() { fi } +cap_dynamic_cadence_for_queue() { + local timeout_cap budget_cap cycle_cap previous_run_timeout previous_budget_seconds previous_max_cycles + + timeout_cap="$(env_integer_or_default OPENCODE_DYNAMIC_RUN_TIMEOUT_CAP_SECONDS 600)" + budget_cap="$(env_integer_or_default OPENCODE_DYNAMIC_TOTAL_BUDGET_CAP_SECONDS 1800)" + cycle_cap="$(env_integer_or_default OPENCODE_DYNAMIC_MAX_CYCLES_CAP 1)" + previous_run_timeout="$original_run_timeout" + previous_budget_seconds="$budget_seconds" + previous_max_cycles="$max_cycles" + + if [ "$timeout_cap" -gt 0 ] && [ "$original_run_timeout" -gt "$timeout_cap" ]; then + original_run_timeout="$timeout_cap" + fi + if [ "$budget_cap" -gt 0 ] && [ "$budget_seconds" -gt "$budget_cap" ]; then + budget_seconds="$budget_cap" + fi + if [ "$cycle_cap" -gt 0 ]; then + if [ "$max_cycles" -eq 0 ] || [ "$max_cycles" -gt "$cycle_cap" ]; then + max_cycles="$cycle_cap" + fi + fi + + if [ "$original_run_timeout" != "$previous_run_timeout" ] || + [ "$budget_seconds" != "$previous_budget_seconds" ] || + [ "$max_cycles" != "$previous_max_cycles" ]; then + printf 'OpenCode dynamic review cadence queue cap applied: per-attempt %ss -> %ss, total budget %ss -> %ss, max-cycles %s -> %s; set OPENCODE_DYNAMIC_*_CAP_SECONDS or OPENCODE_DYNAMIC_MAX_CYCLES_CAP to 0 to disable a specific queue cap.\n' \ + "$previous_run_timeout" "$original_run_timeout" \ + "$previous_budget_seconds" "$budget_seconds" \ + "$previous_max_cycles" "$max_cycles" + fi +} + count_changed_files_for_cadence() { local changed_files_file="${OPENCODE_CHANGED_FILES_FILE:-}" @@ -646,12 +678,14 @@ main() { budget_seconds="$(env_integer_or_default OPENCODE_LARGE_CHANGE_TOTAL_BUDGET_SECONDS 7200)" fi max_cycles="$(env_integer_or_default OPENCODE_DYNAMIC_MAX_CYCLES 0)" + cap_dynamic_cadence_for_queue printf 'OpenCode dynamic review cadence selected %ss per attempt and %ss total budget for %s changed file(s); max-cycles=%s.\n' \ "$original_run_timeout" "$budget_seconds" "$changed_file_count" "$max_cycles" else original_run_timeout="$(env_integer_or_default OPENCODE_UNKNOWN_CHANGE_RUN_TIMEOUT_SECONDS 1800)" budget_seconds="$(env_integer_or_default OPENCODE_UNKNOWN_CHANGE_TOTAL_BUDGET_SECONDS 3900)" max_cycles="$(env_integer_or_default OPENCODE_DYNAMIC_MAX_CYCLES 0)" + cap_dynamic_cadence_for_queue printf 'OpenCode dynamic review cadence could not read OPENCODE_CHANGED_FILES_FILE; using %ss per attempt and %ss total budget; max-cycles=%s.\n' \ "$original_run_timeout" "$budget_seconds" "$max_cycles" fi diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 257ec8b04..0ce00ae39 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -722,7 +722,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert 'OPENCODE_UNKNOWN_CHANGE_TOTAL_BUDGET_SECONDS: "3600"' in workflow assert 'OPENCODE_RUN_TIMEOUT_SECONDS: "600"' in workflow assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "3600"' in workflow - assert 'OPENCODE_POOL_STEP_TIMEOUT_SECONDS: "3600"' in workflow + assert 'OPENCODE_POOL_STEP_TIMEOUT_SECONDS: "2100"' in workflow assert 'timeout --kill-after=30s "${OPENCODE_POOL_STEP_TIMEOUT_SECONDS:-3600}s"' in workflow assert "OpenCode model pool exceeded the outer" in workflow assert 'OPENCODE_POOL_MAX_CYCLES: "0"' in workflow @@ -750,7 +750,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert 'OPENCODE_RUN_TIMEOUT_SECONDS: "600"' in workflow assert 'OPENCODE_EXPORT_TIMEOUT_SECONDS: "120"' in workflow assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "3600"' in workflow - assert 'OPENCODE_POOL_STEP_TIMEOUT_SECONDS: "3600"' in workflow + assert 'OPENCODE_POOL_STEP_TIMEOUT_SECONDS: "2100"' in workflow assert 'OPENCODE_POOL_MAX_CYCLES: "0"' in workflow assert 'OPENCODE_DYNAMIC_REVIEW_CADENCE: "true"' in workflow assert 'OPENCODE_CHANGED_FILES_FILE: ${{ runner.temp }}/opencode-changed-files.txt' in workflow @@ -762,8 +762,11 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert 'OPENCODE_LARGE_CHANGE_TOTAL_BUDGET_SECONDS: "3600"' in workflow assert 'OPENCODE_UNKNOWN_CHANGE_RUN_TIMEOUT_SECONDS: "600"' in workflow assert 'OPENCODE_UNKNOWN_CHANGE_TOTAL_BUDGET_SECONDS: "3600"' in workflow + assert 'OPENCODE_DYNAMIC_RUN_TIMEOUT_CAP_SECONDS: "600"' in workflow + assert 'OPENCODE_DYNAMIC_TOTAL_BUDGET_CAP_SECONDS: "1800"' in workflow + assert 'OPENCODE_DYNAMIC_MAX_CYCLES_CAP: "1"' in workflow assert 'OPENCODE_GITHUB_GPT5_RUN_TIMEOUT_SECONDS: "45"' in workflow - assert 'OPENCODE_DYNAMIC_MAX_CYCLES: "0"' 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( " - name: Run merge scheduler after approval", 1 diff --git a/tests/test_opencode_model_pool_runner.py b/tests/test_opencode_model_pool_runner.py index 3a2b3c261..e32760e45 100644 --- a/tests/test_opencode_model_pool_runner.py +++ b/tests/test_opencode_model_pool_runner.py @@ -421,6 +421,33 @@ def test_dynamic_review_cadence_uses_small_change_timeout(tmp_path: Path) -> Non assert "retry budget remaining." in result.stdout +def test_dynamic_review_cadence_caps_large_change_queue_budget(tmp_path: Path) -> None: + """Large PR cadence logs queue caps instead of pinning review jobs for an hour.""" + changed_files = [f"backend/changed_{index}.py" for index in range(21)] + result = run_failed_model( + tmp_path, + changed_files=changed_files, + extra_env={ + "OPENCODE_DYNAMIC_REVIEW_CADENCE": "true", + "OPENCODE_DYNAMIC_MAX_CYCLES": "0", + "OPENCODE_LARGE_CHANGE_RUN_TIMEOUT_SECONDS": "3600", + "OPENCODE_LARGE_CHANGE_TOTAL_BUDGET_SECONDS": "7200", + }, + model_candidates="github-models/deepseek/deepseek-v3-0324", + ) + + assert result.returncode == 1 + assert ( + "OpenCode dynamic review cadence queue cap applied: per-attempt 3600s -> 600s, " + "total budget 7200s -> 1800s, max-cycles 0 -> 1" + ) in result.stdout + assert ( + "OpenCode dynamic review cadence selected 600s per attempt and 1800s total budget " + "for 21 changed file(s); max-cycles=1." + ) in result.stdout + assert "OpenCode model pool reached configured max cycle count 1" in result.stdout + + def test_github_gpt5_runtime_cap_preserves_queue_budget(tmp_path: Path) -> None: """Known constrained GitHub GPT-5 endpoints cannot consume a full cadence slot.""" result = run_failed_model(