Skip to content

fix(judge): retry a transient empty/unparseable judge reply instead of banking a silent 0 - #190

Merged
eren23 merged 1 commit into
mainfrom
fix/judge-empty-response-retry
Aug 2, 2026
Merged

fix(judge): retry a transient empty/unparseable judge reply instead of banking a silent 0#190
eren23 merged 1 commit into
mainfrom
fix/judge-empty-response-retry

Conversation

@eren23

@eren23 eren23 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Fixes the judge-retry gap flagged in #189.

The bug

_ask_judge wraps its API call in _create_with_retry, which only retries API-level errors. But a 200 response with empty content — a transient judge-model failure — flows into _parse_judgement, which (correctly, by design) returns a loud UNPARSEABLE score-0 without raising. So the empty reply slipped past _create_with_retry and the benches' exception-only _judge_with_retry, banking a blank response as a real 0.

This is exactly what surfaced as continuity_with=0.0 (raw_head:"") on the #189 descent run — the Kontext with-arm visibly continued the crop, but a transient empty judge reply scored it 0.

The fix

At the single chokepoint every score_* routes through, _ask_judge now re-issues the call while the reply can't be parsed (rationale startswith("UNPARSEABLE")), bounded by VLM_JUDGE_EMPTY_RETRIES (default 3, clamped 1–5), then falls back to the loud UNPARSEABLE 0. One place → both the production view-loop judges and the paid benches benefit. A legit reply (including a real 0) never burns an extra call.

Tests (TDD — added failing first)

  • test_ask_judge_retries_empty_then_succeeds — empty body → retries → banks the valid score.
  • test_ask_judge_gives_up_loudly_after_retries — persistent empty → bounded retries → loud UNPARSEABLE 0.
  • test_ask_judge_does_not_retry_a_legit_score — a valid reply (incl. legit 0) costs exactly one call.

The existing _parse_judgement contract tests (legit-0 vs UNPARSEABLE) still pass unchanged. 986 backend tests pass, coverage 86.81%.

🤖 Generated with Claude Code

…f banking a silent 0

_create_with_retry only retries API-level errors. A 200 response with EMPTY
content — a transient judge-model failure — parses to a loud UNPARSEABLE score-0
in _parse_judgement WITHOUT raising, so it slipped past that retry AND the
benches' exception-only _judge_with_retry, banking a blank reply as a real 0.
This is what surfaced as continuity_with=0.0 (raw_head:"") on the #189 descent run.

Fix at the single chokepoint every score_* routes through: _ask_judge now
re-issues the call while the reply can't be parsed (rationale startswith
"UNPARSEABLE"), bounded by VLM_JUDGE_EMPTY_RETRIES (default 3), then falls back
to the loud UNPARSEABLE 0. Both the view-loop and the paid benches benefit; a
legit reply (incl. a real 0) never burns an extra call.

986 backend tests pass (3 new), coverage 86.81%.
@cursor

cursor Bot commented Aug 2, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@eren23
eren23 merged commit ce6fd81 into main Aug 2, 2026
5 checks passed
eren23 added a commit that referenced this pull request Aug 3, 2026
…tioning's contribution (#191)

#189 showed place_lift is structurally ~0 on real-photo chains: a famous name
(e.g. "Grand Canyon of the Yellowstone") lets the BASELINE render the place from
text alone, so both arms ceiling and the metric can't see conditioning working —
the fantasy/generated-map regime the product actually serves (a made-up place
the model can only render from its DRAWN form) is unrepresentable with real names.

DESCENT_BENCH_BLIND_LABEL strips the real name from BOTH arms' prompts (a generic
"the place marked at this spot on the map"), so place_lift isolates what the
region crop contributes. The judge never sees the label — it compares images —
so this changes only what the arms are TOLD, not scoring.

Live validation (n=2, ~$0.61), vs the real-name run's +0.0:
  mean place_lift: +6.000  (was +0.000 named)
    Grand Canyon  place with=10.0 without=0.0  lift +10.0   continuity=10.0
    Church        place with=2.0  without=0.0  lift  +2.0   continuity= 9.0
Blinded, the baseline generates a random place (verified: a generic "YOU ARE
HERE" cottage-on-a-map → place_match 0); the crop-conditioned arm reproduces the
canyon (10). Region-conditioning contributes the ENTIRE identity — a +10 lift —
proving the #188 edit-seam carries a real place when the name can't. Even the
Chester glyph transfers a weak +2. (continuity_with=10.0, not the old spurious
0 — the #190 empty-judge-retry fix confirmed working live.)

988 backend tests pass (2 new).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant