fix(sidecar): keep transient-rejected preflight routes as deferred failover instead of discarding them - #1947
Conversation
…ilover The review sidecar's preflight discarded a route the moment its 16-token probe raised, including on 429 and 5xx, while the serving gateway treats exactly those statuses as transient: it retries the route and fails over across it (provider_errors.PROVIDER_STATUS_SURFACES marks 429 retryable; orchestrator.TRANSIENT_HTTP_STATUS). Under concurrent CI load the probes spend the per-key budgets themselves: noema-review run 33993637015 (.github#1687, 2026-09-05) rejected 11 of 12 routes -- six with 429, three of them on NVIDIA keys whose sibling routes were ready -- served the single ready route for 542 s and returned 502. Routes whose probe answered with a status in the gateway's transient set are now kept as deferred, ranked after every ready route by a catalog priority penalty, so failover has somewhere to go. A probe that timed out records no http_status and stays rejected, so the silent route whose request costs the full two-layer retry budget is never admitted on that evidence. ready_count is unchanged, deferred_count is reported, and with no ready route the stage still fails, so ADR-0005's priced-catalog fallback contract is untouched. The escalation path is not touched. The stream sanitizer admits preflight_route_deferred next to preflight_route_rejected. Tests: deferral order and priorities, all-transient still fails, frozen and plain agent demotion, deferred log line, sanitizer pass/drop/scrub; all five fail against main's launcher and sanitizer. Gate on this tree: 2909 passed, coverage 100%, interrogate 100% (final commit differs from the gated tree by one comment; the touched module and interrogate re-run). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Warning Review limit reachedNext included review available in 20 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (4)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Sanitizer side checked on |
The review request is served by the passthrough path (one attempt per candidate via proxy_send_once, stream=False), so a silent route costs one socket-silence timeout, not a multi-layer retry budget. Comment-only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… budget The first noema-sidecar-evidence trace (.github#1661 run 33995553859) shows the review path is _invoke: a silent route costs two rounds of three 90 s timeouts. Word the constant's comment on that basis instead of the single-attempt passthrough wording of the previous commit. Comment-only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Follow-up stacked on this branch: |
…ransient-routes # Conflicts: # CHANGELOG.md
Lane jan rebuilt the real 2026-09-06 candidate order from #1938's Strix artifact (comment on #1949): under the plain sixteen-probe walk the served set is about five ready + five deferred and the readiness target of eight is unreachable, because the tier round-robin spends five probes on an account whose every free route has answered 429 in every artifact since 21:00Z and four on the two dead gemma-3 entries per key, while the routes that were ready in every pre-#1939 artifact (llama-3.2-11b/90b, muse-glimmer-30b) sit past the cap. A 429 at preflight is a per-key answer. Once one credential account has answered 429 to REVIEW_PREFLIGHT_ACCOUNT_SKIP_AFTER_429 = 2 consecutive probes, its remaining candidates are skipped without a probe and the walk continues with the other accounts' next candidates; the two probed routes are still deferred (#1947). Under jan's order the same sixteen probes now reach both keys' llama routes and the target; a fully rate-limited hour costs two probes per account instead of the whole budget. - The production free pool lists the full 24 candidates again (probe cap 16): the tail past the cap is reachable exactly when an account is skipped, and the report separates skipped_count from the unreached remainder, which answers the earlier unreachable-tail objection. - The deferral pass pairs rows with the agents actually probed (a new `probed` list), not positionally with `agents` -- with skips those no longer line up; the artifact-order test alone missed this because its skips fell after its deferred rows, so a dedicated ordering test pins it. - Report gains skipped_count and account_skip_after_429. ADR-0029 and the CHANGELOG record the rule and the projection; jan's family-interleave layer is recorded as the reserve alternative. Tests: dead-hour budget test now uses 404s (24 -> 16 probes, no skips); three accounts x 8 all 429 -> 6 probes, 18 skipped; jan's order -> a llama route on each key, ready 8 within 16 probes, deferred = the two probed OpenRouter routes, skipped >= 3; row/agent pairing after skips. Negative control: the new tests fail on this PR's previous head; the pairing test fails on the pre-fix walk. Gate: 2916 passed, 1 skipped, coverage 100% (0 missed), interrogate 100%. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Head-tree verification and merge under the chicken-and-egg clause (host 1, 2026-09-06 14:05 KST). The merge procedure was requested from host 2 at 10:40 KST and repeated at 13:08 and 13:35 KST by session name; no reply on either channel by 14:05 KST, so I am performing it, with the evidence here.
|
|
First post-merge sample (host 1). The run then failed inside Strix itself, not at the gateway: after |
Brings the branch current with protected main (24 commits, including the contextual-orchestrator pin advance in efb8926 and the sidecar preflight changes in #1947/#1949/#1950) so the gap-baseline entry this PR carries can be extended with the retry-stacking root cause and its first post-pin production measurement. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPmJErfkcHer4UVEgrQxUX
Brings the branch current with protected main (24 commits since 6f8c51d). main did not touch .github/workflows/opencode-review-dispatch.yml in that range, so this branch's REVIEW_DISPATCH_BLOB_SHA pin (0a39def) still matches the merged workflow; the only textual merge was CHANGELOG.md, which auto-merged. The merged main carries the contextual-orchestrator pin advance (efb8926) and the sidecar preflight repairs (#1947, #1949, #1950), so this head's required reviews run against the repaired gateway instead of the retry-stacking pin that failed the previous head's noema-review (502 after 2343 s) and strix. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPmJErfkcHer4UVEgrQxUX
…of banning them (#1957) #1949's account rule sets aside an account's remaining candidates after two consecutive 429s. When a walk runs out of candidates it is willing to probe it ENDS -- with probe budget in hand and the readiness target unmet -- and the stage fails closed; because deferral needs one ready route (#1947), nothing is served either. Sixteen sidecar artifacts were collected on 2026-09-06 across .github, argos, bandscope and naruon; fourteen ran the merged rule (argos 34013128112 and bandscope 34013146167 still carry the pre-#1949 report shape). Those fourteen fall into three classes, not two: eight boots at probed/skipped/ready 16/4/5-6 spend the whole budget in the first pass and are unchanged by this commit; ONE (argos 34014143870, 06:56Z) reads 12/12/3 -- it served, yet exhausted its candidates under target with four probes unspent; five read 6/18/0 and failed closed. The sixth ready route in the healthy class (llama-3.2-11b on the second NVIDIA key, catalog position 17, ready in exactly those eight artifacts) is reached only because four OpenRouter probes were set aside -- the rule's designed benefit, which this commit keeps. .github run 34016207820's six probes were refused 429 between 07:49:35.111Z and 07:49:35.767Z; because the walk round-robins three accounts, "two consecutive 429s" on one account is two requests about 310 ms apart (nvidia_nim at .111 and .422). keyverse#143's 08:20Z noema repeated the shape in a second repository. A refusal is not a verdict on the account: run 34016093772 was inside its own preflight during that burst and its llama-3.2-11b probes on the same two NVIDIA keys answered ready at 07:50:58.7Z and 07:50:59.0Z, 84 s after those keys refused. Not claimed: that the ten unspent probes would have found a ready route inside the burst. No artifact answers it, which is why this also records retry_after_s. The change rests on the structural defect alone. A set-aside candidate is now postponed to the end of the walk; once the first pass ends under target with budget left, the postponed candidates are probed in catalog order until the sixteen-probe budget is spent. Both passes share one stop condition, so probes per stage stay <= 16, and exhaustion uses a dedicated sentinel so a None candidate cannot truncate the walk. The second pass never draws on the shared escalation budget (#1458): a postponed candidate answering "budget too small" is rejected as escalation_reserved_for_first_pass, because otherwise candidates the previous design never probed take escalations from the priced stage that had them, and a measured two-stage run stops serving a route it used to serve. _safe_retry_after_seconds records a refused probe's Retry-After as retry_after_s when it is whole delta-seconds in range. It gates on isdecimal, not isdigit: the header is provider-controlled, "²".isdigit() is True while int() on it raises, and this runs inside the probe walk's exception handler whose callers catch only ReviewPreflightError -- so a ValueError there would kill the boot before any evidence file is written. No code waits on the value (ADR-0003). Cost, stated in the ADR and PR body against the 60-job ceiling work: about 120 ms per refused probe, up to 10 x 90 s ~= 15 minutes when the postponed tail is silent (gemma-4-31b answered TimeoutError in 15 of the 19 probes that reached it), and 8 -> 24 requests on the two-stage auto path, where the priced stage doubles from 4 probes to 8. All inside the probe budget ADR-0029 bounds. Report: postponed_probed_count added, skipped_count now means "postponed and never reached". ADR-0029 amended, and its two superseded sentences marked in place. Verified by a three-lens adversarial refutation before push (control flow, evidence and design, test fidelity): all three returned refuted=true with 20 findings, each reproduced against the artifacts before acting. The blocker above, the escalation-budget regression, the miscounted evidence table, the false "healthy-minute walk is unchanged" claim, the 310 ms spacing, the sibling run's real relationship to the burst and the superseded ADR sentences all come from that pass. Gate on this tree: 2945 passed, 1 skipped, 21 subtests; coverage 100% (0 missed); interrogate 100%. Negative control on origin/main's launcher with this test file: 8 failed, 93 passed. Refs #1948, #1949. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…lidating is its own class, and the measured cost of the #1947/#1949 walk Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4
Summary
The review sidecar's preflight discards a route the moment its 16-token probe answers with any exception — including
429and5xx— while the serving gateway it feeds treats exactly those statuses as transient, retrying the route and then failing over across it (provider_errors.PROVIDER_STATUS_SURFACES[429] = (…, retryable=True);orchestrator.TRANSIENT_HTTP_STATUS). Under concurrent CI load the probes themselves spend the per-key budgets, readiness collapses, and the served set has nowhere to fail over to. This PR keeps such routes as deferred, ranked after every ready route, and leaves everything else about the preflight unchanged.Evidence
noema-reviewrun33993637015(.github#1687, 2026-09-05T21:38Z, sidecar at2e414d15, catalog already 4+4+4 after#1939), from theruntime preflight summaryin the job log:nvidia_nim_subdeepseek-v4-flashopenrouterfree routes, plusnvidia_nimdeepseek-v4-flash and deepseek-v4-pro (the sibling key's flash route was ready)gemma-3-12b/gemma-3-4bon both NVIDIA keysnvidia_nim_subdeepseek-v4-proattempts=1for every rejected route,escalations_used 0/4, preflight finished in 20 s. The single ready route then spent 542 s — two rounds of three 90 s timeouts, the gateway's full retry budget for one silent agent — and the caller got a 502. Under this PR the same run serves 1 ready + 6 deferred.Why
escalations_usedstayed 0: the escalation budget is only for the budget signature (finish_reason == "length"or reasoning-without-content,launcher.py:442-467); a probe exception goes_record_provider_exception→rejected→continueon the first attempt (:409-412), by design ("401/429/5xx are not budget evidence",:354). This PR does not touch escalation.Change
REVIEW_PREFLIGHT_DEFERRABLE_HTTP_STATUS = {408, 409, 425, 429, 500, 502, 503, 504, 529}— the vendored orchestrator'sTRANSIENT_HTTP_STATUS, so preflight and serving read the same signal the same way; a status the gateway would not retry (401/403/404/…) is still rejected.http_statusis in that set becomesdeferred; the agent is re-emitted with its catalog priority lowered byREVIEW_PREFLIGHT_DEFERRED_PRIORITY_PENALTY = 1000(_demote_agent:dataclasses.replacefor the frozenModelAgent, copy-and-set for plain objects) and appended after the ready agents. With no ready route the stage fails exactly as before, so_preflight_with_fallback's "priced catalog only after every primary route rejects" (ADR-0005) is untouched.ready_countunchanged (never inflated — it remains the discriminator peers measure), newdeferred_count,rejected_count = probed − ready − deferred;routes[].statusgains"deferred"._log_preflight_rejectionsprintspreflight_route_deferred …next topreflight_route_rejected …, and the stream sanitizer's allow-list admits both event names with the same bounded fields.Tests
test_preflight_defers_transient_probe_statuses_behind_ready_routes: ready / 429 / 404 / 503 → served orderready, 429, 503, priorities0, −1001, −1003, caller's agents untouched, counts 1/2/1, per-route statuses.test_preflight_still_fails_when_no_route_is_ready: all-429 →ReviewPreflightError,deferred_count 0, statuses stayrejected.test_demote_agent_handles_frozen_dataclasses_and_plain_objects.test_log_preflight_rejections_reports_deferred_routesandtest_sidecar_stream_sanitizer_passes_deferred_preflight_lines(unknown event names still dropped; a planted token still scrubbed).main's launcher and sanitizer. Full gate in the commit message.Tradeoff (raised by lane peer 1) and why it is bounded
A deferred route that does not recover adds to the serving walk. The first
noema-sidecar-evidencetrace (#1661run33995553859, table on#1939) shows what a route costs on the review path: a route that answers 429/5xx immediately costs seconds for its attempts; a silent route costs the gateway's full retry budget — three 90 s timeouts, twice (_invokeretries the same agent once) — 541 s measured. Deferral admits only routes that answered the probe with a transient status (a probe that timed out records nohttp_statusand stays rejected), so it never admits, on the strength of the probe, the silent class that costs 541 s; the residual risk is a route that answered the probe and goes silent at request time, which a ready route carries equally.The discriminator for this PR is the deferred routes' outcome at request time, read per route from the
provider_attempt/provider_exhausted/provider_rejected_permanentlines in the artifact (#1943/#1944/#1945): if deferred routes mostly serve or fail fast, the change is pure gain; if they routinely go silent, a cap on deferred routes is the next commit. The same trace also shows the larger costs deferral does not touch — the same agents walked three times within one request, and two preflight-ready routes never attempted — which sit withcontextual-orchestrator#1045and#1948.Not in this PR
The four permanent 404s (
gemma-3models NIM lists but does not serve) still cost a third of the catalog every run — that is a discovery-side question. The probe budget itself (every sidecar boot probing all twelve routes against per-key rate limits) is a policy value and stays with the owner. Related:#1939(catalog composition),#1943/#1944/#1945(trace + artifact + sanitizer),contextual-orchestrator#1045(gateway failover cost).Developer experience
One frozenset, one small helper, one post-loop pass; the existing probe branches and the escalation path are untouched, and the report keeps its old keys with their old meanings.
User experience
A review whose probes hit rate limits still gets served, and a stalled route has other routes to fall over to, instead of a 502 after the single survivor's retry budget runs out.
🤖 Generated with Claude Code