fix(backtest): a failed engine call is one lost cell, and a spent quota stops that engine — never the campaign - #1959
Merged
Conversation
…aign A cert back-test campaign ended with no report when one engine's daily quota ran out: the CLI answered 429 on every retry, the runner classified it transient, exhausted its retry budget and raised `EngineFailed`, and `_replay_item_cells` — which absorbed only `EngineUnavailable` — let it escape, discarding every finished cell with the work root. Three changes, so a campaign always lands `metrics/cert-backtest.json`: - `_replay_item_cells` absorbs `EngineFailed` as one lost cell, recorded under `provenance.lost_cells` with the new `engine-failed` reason and printed as it happens, exactly as an unreadable artifact is. - The runner classifies a terminal quota marker (the `gemini` CLI's `TerminalQuotaError` and the prose it prints with it) as non-transient, so the cell fails after one attempt rather than after the backoff budget, and raises `EngineQuotaExhausted` naming the cause. An ordinary bare-429 throttle keeps its retries. - Once an engine reports its quota spent, the campaign stops attempting that engine's cells and records each under the `quota-exhausted` reason, so the report's counts stay honest while no further backoff is spent on a certain failure. Tracked apart from `unavailable`: an engine whose binary was missing and one whose allowance ran out are different facts, and the report says which. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1957.
What changed
A cert back-test campaign no longer ends when one engine call fails; it lands a report that names what it could not score.
_replay_item_cellsincert_backtest.pycatchesEngineFailedbesideEngineUnavailable, records the cell underprovenance.lost_cellsasengine-failed, and continues. The scorer treats it as a loss exactly as a missing or invalid cell; short entries still sort below full ones.pipeline/runner.pygainsEngineQuotaExhausted(EngineFailed), raised after one attempt and no backoff when the captured stderr carries a terminal-quota marker (TerminalQuotaError,exhausted your daily quota, both gemini's, named to that engine in the docstring). Ordinary throttles (HTTP 429,Quota exceeded for quota metric,retry-after) stay on the transient retry path; tests pin both sides. The replay distinguishes the two by exception type, not message text.quota_exhaustedset (never merged intounavailable: a missing binary and a spent allowance are different facts and the CLI prints a distinct drop line for each). Every further cell of that engine is recorded asquota-exhaustedwithout being attempted, so the report's counts stay honest.CertBacktestCellLoss.reasongainsengine-failedandquota-exhausted(extracted to a namedCertBacktestLossReasonalias); two stale field descriptions on the provenance block corrected;schemas/regenerated. The metrics-refresh PR body's per-loss gloss now reads(no score — unreadable, failed, or not attempted after a spent quota), pinned by test.metrics/README.md's cert-backtest reading rules now state that aquota-exhaustedtruncation keeps the prefix of the dispatched replay order, so the surviving slice is differently composed (floor, band mix, base rate and provisioning mix all follow the prefix) and such an entry is not comparable with a full one; and that a quota drop carries a loss per petition while a missing-binary drop carries none.docs/pipeline.mdanddocs/cli.mdupdated;config/tracking.yaml's runner comment andRunnerConfig's docstring now state the terminal carve-out frommax_attempts, since the same runner serves predict and evaluate.Executed checks
--engine stub): a report lands with all five entries and emptylost_cells, before and after the review fixes.EngineFailedand lands a report with the cell underlost_cellswhile the other predictors score; a spent quota means exactly one gemini attempt across two petitions with both cells recorded as losses and both other engines scoring both petitions; terminal-quota stderr → one attempt, no sleep,EngineQuotaExhausted; ordinary throttles still transient.scripts/gate.shlock, lint, types, test (4984 passed, 2 pre-existing skips), data, schemas — all pass.uv run fedcourts process-digest --allbyte-identical toorigin/staging, re-verified after thetracking.yamlcomment edit.What is not yet verified on a runner: the terminal-quota classification reads the subprocess's captured stderr; the marker text is quoted from run 35643195366's log, where the classifier demonstrably read that same stream, but the tests prove the classifier given the string, not the stream. The first real gemini quota event settles it. The campaign-survival half (item 1) is stream-independent and fully covered.
Review
code-reviewer,docs-reviewer,stats-reviewer: six blockers, all fixed (the "three backoffs apiece" wording that this change itself made false; thetracking.yamlcomment; the metrics-refresh gloss; two stale schema descriptions; the order-determined truncation reading rule; the asymmetry between the two whole-predictor drops). Three recommendations declined, stated here: a rejected codex login now shows as oneengine-failedloss per petition rather than a campaign crash (rerouting it into the missing-binary path is its own change); the metrics-refresh headline can still name an offline baseline "top predictor" when a concrete--enginedispatch truncates every engine (pre-existing, orthogonal); which engine ran out is not carried onReplayOutcome(recoverable fromlost_cellsand the CLI drop line).Merge: left for the maintainer — it changes the shared runner's retry classification. Effect check for the promotion body: the next
run-backteston a day gemini's allowance is spent lands a report whoseprovenance.lost_cellscarriesquota-exhaustedfor gemini's cells and the job log prints the quota drop line; on a clean day the report is unchanged in shape andlost_cellsis empty.🤖 Generated with Claude Code