From 992e279c86320d6c699b4829b03b4548adf5c404 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Fri, 18 Sep 2026 02:01:36 +0800 Subject: [PATCH] fix(ai): keep quota deferred from failing OIDC canary jobs Recognize service-shaped Codex quota deferrals without requiring failure_category, stamp the category on gateway defer bodies, and treat daily-summary deferred as exit 0 so Actions stays green with artifacts. Co-authored-by: Cursor --- scripts/consume_daily_briefing.py | 4 +++- scripts/run_global_etf_research_codegen.py | 14 ++++++++++++-- service/ai_gateway_service.py | 8 +++++++- tests/test_daily_briefing_summary.py | 14 ++++++++++++++ tests/test_global_etf_research_codegen.py | 21 +++++++++++++++++++++ 5 files changed, 57 insertions(+), 4 deletions(-) diff --git a/scripts/consume_daily_briefing.py b/scripts/consume_daily_briefing.py index 78d5261b..86a0a46c 100644 --- a/scripts/consume_daily_briefing.py +++ b/scripts/consume_daily_briefing.py @@ -67,7 +67,9 @@ def main(argv: list[str] | None = None) -> int: except (OSError, ValueError, TypeError, KeyError, OverflowError): summary = {"status": "unavailable", "reason": "briefing_input_unavailable", "advisory_only": True} print(json.dumps({"day": args.day, "ai_summary": summary}, ensure_ascii=False, indent=2)) - return 0 if summary["status"] in {"available", "dry_run"} else 3 + # deferred = trusted capacity/scheduling decision with artifact; do not + # fail the OIDC Actions job (red CI) the way unavailable/input errors do. + return 0 if summary["status"] in {"available", "dry_run", "deferred"} else 3 result = consume_briefing_dir(report_dir, day=args.day) payload: dict = result.to_dict() if args.dispatch: diff --git a/scripts/run_global_etf_research_codegen.py b/scripts/run_global_etf_research_codegen.py index 64af4965..25f8af4f 100644 --- a/scripts/run_global_etf_research_codegen.py +++ b/scripts/run_global_etf_research_codegen.py @@ -234,15 +234,25 @@ def _read_json(path: Path, reason: str) -> dict[str, Any]: def _trusted_deferred_retry_at(raw: Any, *, now: float | None = None) -> int | None: - """Return the bounded retry time for the one recognized pre-execution deferral.""" + """Return the bounded retry time for the one recognized pre-execution deferral. + + Service-side 429 defer bodies may omit ``failure_category``; the gateway + client injects ``quota_or_capacity_failure`` when present. Accept either + the exact category or a missing/empty category when the other quota fields + already match, so deferred is not mis-labeled ``global_codegen_gateway_failed``. + """ if not isinstance(raw, Mapping): return None retry_at = raw.get("retry_at") + failure_category = raw.get("failure_category") if ( raw.get("status") != "deferred" or raw.get("execution_started") is not False or raw.get("error") != _DEFERRED_QUOTA_ERROR - or raw.get("failure_category") != _DEFERRED_FAILURE_CATEGORY + or ( + failure_category not in (None, "") + and failure_category != _DEFERRED_FAILURE_CATEGORY + ) or isinstance(retry_at, bool) or not isinstance(retry_at, (int, float)) or not math.isfinite(retry_at) diff --git a/service/ai_gateway_service.py b/service/ai_gateway_service.py index 8cb27366..dff5d0ee 100644 --- a/service/ai_gateway_service.py +++ b/service/ai_gateway_service.py @@ -334,7 +334,13 @@ def _admit_codex_execute(quota: Any, repo: str, payload: dict[str, Any]) -> dict and route["reason"] in {"codex_quota_reserved", "codex_account_unavailable", "codex_account_stale"} and str(payload.get("model") or "") in {"", "auto"}): return _admit_cursor_execute(quota, repo, payload) - return {"status": "deferred", "error": route["reason"], "retry_at": route["retry_at"], "execution_started": False} + return { + "status": "deferred", + "error": route["reason"], + "retry_at": route["retry_at"], + "execution_started": False, + "failure_category": "quota_or_capacity_failure", + } payload.update(model=route["model"], reasoning_effort=route["reasoning_effort"]) return None result = quota.check(repo, "codex-cli", str(payload.get("prompt") or ""), codex_account=True) diff --git a/tests/test_daily_briefing_summary.py b/tests/test_daily_briefing_summary.py index 6481bbb7..cda89126 100644 --- a/tests/test_daily_briefing_summary.py +++ b/tests/test_daily_briefing_summary.py @@ -264,6 +264,20 @@ def test_real_sdk_unavailable_or_deferred_does_not_fallback_or_leak(report, kind assert result["retry_at"] == 9000 +def test_summary_only_deferred_exits_zero_for_oidc_actions(report, capsys): + path, _ = report + with patch( + "scripts.consume_daily_briefing.summarize_briefing", + return_value={"status": "deferred", "advisory_only": True, "retry_at": 9000}, + ), patch( + "scripts.consume_daily_briefing.consume_briefing_dir", + return_value=object(), + ): + assert main(["--report-dir", str(path), "--day", "2026-09-09", "--summary-only"]) == 0 + payload = json.loads(capsys.readouterr().out) + assert payload["ai_summary"]["status"] == "deferred" + + @pytest.mark.parametrize("field,value", [ ("as_of", None), ("as_of", "invalid"), ("as_of", "2026-09-09T22:00:00"), ("as_of", "9999-12-31T23:59:59+00:00"), ("as_of", "2026-09-08T00:00:00+00:00"), diff --git a/tests/test_global_etf_research_codegen.py b/tests/test_global_etf_research_codegen.py index 567e792b..4d1d1563 100644 --- a/tests/test_global_etf_research_codegen.py +++ b/tests/test_global_etf_research_codegen.py @@ -349,6 +349,27 @@ def test_trusted_quota_deferral_preserves_completed_test_summary_and_replays_wit ) self.assertEqual(replay, result) + def test_service_shape_quota_deferral_without_failure_category_is_deferred(self): + retry_at = int(codegen.datetime.now(codegen.timezone.utc).timestamp()) + 60 + response = SimpleNamespace( + success=False, provider="codex", model="", output="", + raw={ + "status": "deferred", "execution_started": False, + "retry_at": retry_at, "error": "codex_quota_reserved", + }, + ) + with TemporaryDirectory() as tmp, patch.object(codegen, "_docker_preflight"), patch.object( + codegen, "_read_global_base", return_value=(codegen.GLOBAL_ETF_RESEARCH_CODEGEN_UES_COMMIT, {})), patch.dict( + sys.modules, {"scripts.run_new_research": SimpleNamespace(_archive_codegen_base=lambda *a, **k: None)}): + result = codegen.run_global_etf_research_codegen_case( + ues_repo_root=tmp, run_root=tmp, source_ref="a" * 40, fetch_source=_source, + candidate_test_runner=lambda *args, **kwargs: {"status": "passed"}, + execute=lambda prompt: response, + ) + self.assertEqual(result["status"], "deferred") + self.assertEqual(result["reason"], "global_codegen_quota_deferred") + self.assertEqual(result["retry_at"], retry_at) + def test_legacy_gateway_failure_is_deferred_only_for_the_exact_saved_quota_shape(self): source = _source() identity = codegen._identity(source=source, source_commit=codegen.GLOBAL_ETF_RESEARCH_CODEGEN_UES_COMMIT)