ADR-0010: Judge-Runaway-Guard¶
- Status: akzeptiert · 2026-06-28
- Bereich: Judge
Kontext¶
OpenAIJudgeBackend.judge() rief chat.completions.create ungestreamt auf — ohne max_tokens und ohne expliziten Timeout. Wird ein Thinking-Modell als Judge gewählt (z. B. qwen/qwen3.6-35b-a3b), denkt es auf einem einzelnen Call unbegrenzt weiter; jeder Call läuft in den SDK-Default-Timeout (600 s) × 2 Retries (≈ 30 min), der resultierende APITimeoutError blieb ungefangen und ließ den Judge-Prozess crashen (Exit ≠ 0). Beobachtet am 2026-06-28: 33 min bei 96 % GPU, 0 Verdicts, dann Crash; über die GUI sah es wie ein „verschwundener Run" aus.
Kräfte/Constraints: Ein schlechter Judge-Modell-Griff muss schnell, sichtbar und nicht-destruktiv scheitern (Sekunden statt 30 min, klare Meldung statt Crash, Run finalisiert oder bricht sauber ab). Bestehende judge.yaml-Configs und alte judgements.jsonl (ohne neue Felder) müssen unverändert weiterlaufen (Rückwärtskompatibilität). Nicht-Ziel: Thinking-Judges nutzbar zu machen — die Steuerung zu einem nicht-Thinking-Modell wie qwen3.6-27b bleibt via Docs/Memory.
Entscheidung¶
Zwei Schichten in touchstone/judge.py:
- Bounded transport (Backend):
OpenAIJudgeBackenderhälttimeout,max_retries: int = 0undmax_tokens; derOpenAI()-Client wird mittimeout/max_retrieskonstruiert (Timeout nur wenn gesetzt geforwardet). Der SDK-Call wird intry/exceptgewrappt: einOpenAIErrorwird zu der neuen, typisiertenJudgeCallErrorumgemünzt (raise JudgeCallError(...) from e); ein Nicht-OpenAIError(Programmierfehler) wird durchgereicht statt als Judge-Fehler maskiert. - Degrade + Circuit-Breaker (Orchestrierung):
judge_responsesumschließt denscore_response-Aufruf. EinJudgeCallErrordegradiert die Zelle zu einem unscored Error-Verdict (unscored=True, judge_error=True, Rationale⚠ Judge-Fehler (nicht bewertbar): {err}) und zählt einen Folge-Fehler-Zähler hoch; ein Erfolg dazwischen setzt den Zähler zurück. Erreicht der Zählermax_consecutive_failures(>0), wird die neueJudgeAbortedgeworfen. Error-Verdicts werden nicht anon_verdictgestreamt und in_judge_and_persists Clean-Rewrite herausgefiltert (if v.judge_error: continue), sodass ein Resume mit gutem Modell die Zelle neu bewertet.score_dimensionsfängtJudgeCallErrorzusätzlich ab und liefert einen degradiertenModelReport(Defense-in-Depth).
Verdrahtung: JudgeConfig bekommt drei Felder mit Defaults — call_timeout_s: float = 120, max_consecutive_failures: int = 3, max_tokens: int | None = None. Der Judge-CLI-Command konstruiert das Backend mit timeout=jc.call_timeout_s, max_tokens=jc.max_tokens, reicht max_consecutive_failures=jc.max_consecutive_failures durch und fängt JudgeAborted → rote Meldung + typer.Exit(code=1) (sauberer Abbruch, kein Traceback). Die GUI nutzt den CLI-Subprozess und erbt das (Exit ≠ 0 → Sentinel failed).
Erwogene Alternativen¶
max_consecutive_failuresper Default als reinerjudge_bundle-Parameter aktiv — verworfen:judge_bundle/judge_responsesdefaulten auf0(Breaker aus), damit ein direkterjudge_bundle-Aufruf ohne Config sicher bleibt; nur die CLI reicht den konfigurierten Wert (Default 3) ein.max_tokens-Cap als primärer Guard (Default an) — verworfen: ein Cap würde lange, legitime holistische Rationales abschneiden; der Timeout ist der primäre Guard, der Cap bleibt Opt-in (max_tokens: null) für Nutzer, die Compute hart deckeln wollen.- Error-Verdicts persistieren — verworfen: dann überspränge ein Resume mit einem guten Modell die Zellen und sie blieben für immer „error"; daher Persistenz-Filter über
judge_error. - Beliebige Exceptions im Backend zu
JudgeCallErrorumwandeln — verworfen: ein Nicht-OpenAIErrorist ein Programmier-/Client-Bug und wird durchgereicht, nicht als Judge-Fehler maskiert. - Thinking-Judges nutzbar machen — bewusstes Nicht-Ziel: gelöst über
qwen3.6-27bvia Docs/Memory, nicht im Guard.
Auswirkungen¶
- Positiv: Ein durchdrehendes Judge-Modell scheitert in Sekunden statt ~30 min, mit klarer Meldung statt Crash; der Run finalisiert oder bricht sauber ab (Exit 1, GUI-Sentinel
failed). - Positiv: Verstreute Einzel-Call-Fehler degradieren nur einzelne Zellen (unscored, aus dem Mittel ausgenommen) und brechen den Run nicht ab; ein Erfolg resettet den Breaker.
- Positiv: Rückwärtskompatibel — bestehende
judge.yamlund altejudgements.jsonlohnejudge_errorladen unverändert (Feld defaultet aufFalse). - Trade-off / Restgrenze:
max_tokensist standardmäßig aus (kein Truncating-Risiko), d. h. ohne Opt-in deckelt der Guard nur die Zeit, nicht die Tokenmenge. - Trade-off / Restgrenze: Thinking-Judges bleiben unbrauchbar — der Guard macht ihren Fehlschlag nur schnell/sichtbar, behebt ihn aber nicht (Modellwahl bleibt Nutzer-/Doku-Verantwortung).
Belege & Links¶
- Spec:
docs/superpowers/specs/2026-06-28-judge-runaway-guard-design.md· Code:touchstone/judge.py,touchstone/cli.py· Tests:tests/test_judge_guard.py - Verwandt: —