fix(generative): Timeout-Retry im legacy-Profil + verlorene Konzepte sichtbar (#210)#216
Merged
Merged
Conversation
…sichtbar (closes #210) - runtime_config: LEGACY.timeout_retries 0 -> 1 (fast/balanced erben; quality hatte schon 1). Ein transienter 300s-CLI-Haenger kostet nicht mehr den ganzen Lauf; das Backend spawnt pro Versuch bereits einen frischen CLI-Prozess. - run_extractors_per_concept: trennt harte Call-Ausfaelle (Timeout/CLI-Fehler nach Retries) von legitimen Leer-Extraktionen und gibt sie als failures zurueck. - orchestrator.main: Warn-Block im Run-Summary ('N von M Konzepten verloren') + Exit-Code 3 (unterscheidbar von hartem Abbruch=1). Voller Erfolg bleibt 0. __main__ propagiert den Code via sys.exit(main()). - n_dropped (pipeline_runs) zaehlt Ausfaelle bereits mit -> kein Schema-Change. - ATOMIC_AGENT_CALL_TIMEOUT (konfigurierbares Call-Timeout) war bereits verdrahtet.
…achbesserung) rc===3 (orchestrator.py:_EXIT_EXTRACTOR_LOSS) heisst "Lauf abgeschlossen, aber >=1 Konzept beim Extrahieren verloren" -- kein Absturz. Bisher lief das im generischen Fehler-Zweig: markStageError() faerbte die zuletzt aktive Stage (typisch die erfolgreich gelaufene Stage 8) faelschlich rot, und loadOutputs() lief nur bei rc===0, sodass die real geschriebenen Notes dem GUI-Nutzer nicht angezeigt wurden (Fable-Kontrollreview-Fund). rc===3 laeuft jetzt im selben Zweig wie rc===0 (Stages als erledigt statt Fehler markiert, Outputs geladen, Fokus auf Ergebnis-Sektion), zusaetzlich eine eigene Warnzeile/Banner mit an format_extractor_failure_report() (orchestrator.py) angelehntem Wortlaut inkl. der aus dem Log geparsten Konzept-Anzahl. Verlaufs-Badge zeigt fuer rc=3 konsistent "Teilverlust (Timeout)" statt "Fehlercode 3". Keine JS-Testinfrastruktur im Repo vorhanden (geprueft) -- Verhalten per `node --check` + Code-Inspektion belegt. Zusaetzlich: Testluecke geschlossen. Der bestehende Test test_sub_call_full_does_not_retry_timeout_by_default prueft nur den Modul-Default (kein timeout_retries-Argument uebergeben). Neuer Test test_sub_call_full_explicit_timeout_retries_zero_does_not_retry uebergibt explizit timeout_retries=0 bei auf 1 gesetztem Modul-Default und beweist, dass der explizite Wert gewinnt (kein Retry).
TillQuandel
pushed a commit
that referenced
this pull request
Jul 12, 2026
…/Extractor/Faithfulness-Skip-Events + Dashboard-Event-Robustheit (#197 Nachbesserung) Extern verifizierte Funde (Mistral-Resample + Fable). Der Gate-Funnel ist oben dicht: jeder Note-Verlust traegt jetzt ein stage_outcome-Event. Funnel-Luecken (orchestrator.py, TDD): - entity_resolution Cluster-Merge (consumed-Set): dedup/dropped/ entity_resolution_merge, Survivor als detail (strukturell wie sibling-Dedup). - _collect_stage6_results BaseException-Zweig: droppte ohne Event UND ohne Crash-Report — jetzt beides (Crash-Report ohne per-Note-Payload + Event). - run_extractors_per_concept: empty-ctext (Konzept nicht im Volltext), None (leere Extraktion) und Exception (call_failed) → extractor/dropped-Events. - _apply_faithfulness_gate: Gate aktiv + create, aber leerer page_index (PDF ohne [S. N]-Marker) → faithfulness/skipped/no_page_index. Dashboard-/DB-Event-Robustheit (minimale Hunks): - eval_dashboard._read_token_runs: Event-Records (kein model) vom call-Zaehler ausschliessen — verhindert aufgeblaehte calls + 0-Token-Phantomzeile. - eval_dashboard_server model-Backfill: Scan-Fenster 5 -> 50 Zeilen (Events am Trace-Anfang tragen kein model). - db.init_db: PRAGMA busy_timeout=5000 (kein WAL-Umbau) + _add_column-Helper, der nur 'duplicate column name' verschluckt und andere OperationalError (z.B. 'database is locked') re-raised. Tests: 8 neu (5 orchestrator TDD in test_stage_outcome_events.py, 3 in neuem test_stage_outcome_robustness.py). Suite 5705 -> 5713 passed, 0 failed. ruff format/check clean. Konflikt: #216 fasst run_extractors_per_concept Exception-Zweig an (failures) — beim Merge beide Zeilen behalten. #222/#212 konfliktfrei zu Punkt 5/6.
…es (#210 Runde 2) rc=3 kann auch mit 0 geschriebenen Notes auftreten (orchestrator.py Early-Return `if not drafts:` bei nicht-leeren extractor_failures -- alle Konzepte verloren). Banner-Text haengt jetzt vom echten Notes-Count aus loadOutputs()/api/outputs ab statt pauschal 'Notes trotzdem geschrieben' zu behaupten. rcLabel fuer rc=3 im Verlauf verallgemeinert auf 'Teilverlust (Timeout/CLI-Fehler)', da die Ursache auch ein CLI-Fehler sein kann.
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.
Problem (#210)
Das Subscription-Backend (claude CLI) verlor bei einem 300s-Timeout Konzepte auf zwei Wegen — beide belegt in
generative/.cache/runs/:runtime-configzeigtetimeout_retries=0fürs legacy-Profil → ein transienter CLI-Hänger warfRuntimeError, der Orchestrator starb mit Traceback (Exit 1) inkl. bereits gelaufener Stages 1–4.=== Fertig: 1 Notes ===, Exit 0, keine Warnung — das verlorene Konzept war nur im Trace sichtbar.Fix-Ansatz
1. Retry-Budget auch im legacy-Profil.
LEGACY.timeout_retries0 → 1 inruntime_config.py. fast/balanced erben den Default; quality hatte schon 1. Die Retry-Schicht bleibt bewusst in der Config, NICHT im Backend — der bestehende Backend-Kontrakt (test_sub_call_full_does_not_retry_timeout_by_default: übergebenestimeout_retries=0wird treu geehrt) bleibt intakt. Das Backend spawnt pro Versuch bereits einen frischen CLI-Prozess (subprocess.run/create_subprocess_execin der Retry-Schleife) und gilt für sync- UND async-Pfad.2. Kein stilles Verschlucken + Exit-Code.
run_extractors_per_concepttrennt jetzt harte Call-Ausfälle (Exception: Timeout/CLI-Fehler nach Retries) von legitimen Leer-Extraktionen (run_per_concept→None, Konzept zu schwach im Text) und gibt sie alsfailures-Liste zurück.main()druckt einen Warn-Block direkt im Run-Summary („N von M Konzepten beim Extrahieren verloren …") und beendet den Prozess mit einem eigenen Exit-Code.Exit-Code-Semantik:
0= voller Erfolg,1= harter Abbruch (unbehandelte Exception, z.B. Planner-Timeout nach Retries → kein Plan),3= Lauf abgeschlossen, aber ≥1 Konzept endgültig verloren.3hält den Teilverlust von einem harten Absturz unterscheidbar (CLI/GUI/CI können reagieren).__main__propagiert viasys.exit(main());cli.pyreichtorchestrator.main() or 0durch.pipeline_runs: Kein Schema-Change.
n_droppedexistiert bereits und zählt Ausfälle schon mit (dropped_total= Fehler + Leer-Extraktionen) → das Dashboard sieht den Verlust in der Aggregat-Zahl.Konfigurierbares Call-Timeout (#210 Punkt 3): bereits vorhanden via
ATOMIC_AGENT_CALL_TIMEOUT(runtime_config → base.py → Backend) — nichts zu tun.Bewusst NICHT enthalten (separate PRs): Stage-Outcome-Events (#197), pipeline_runs-Insert-Entkopplung von der Eval (#198).
RED-Beweis (TDD)
Vor dem Fix:
test_orchestrator_timeout_visibility.py→ImportError: cannot import name 'format_extractor_failure_report'(Helfer/4-Tupel existierten nicht).test_legacy_profile_retries_transient_timeout_once→AssertionError: assert 0 >= 1(RuntimeConfig(...timeout_retries=0...)).Nach dem Fix: beide grün. Backend mockt Timeout beim 1. Call + Erfolg beim Retry / Timeout bei allen Calls (keine echten CLI-/LLM-Calls).
Suite-Ergebnis
pytest generative lib/decision_engine/tests shared/tests -q→ 5702 passed, 3 skipped, 0 failed (inkl. 9 neuer Tests aus diesem PR).ruff format --check .undruff check .grün.Cross-Model-Review
Mistral
magistral-medium-latest(voller Diff): keine echten Funde. Gezielt geprüft und als korrekt bestätigt: 4-Tupel-Entpackung (by-chapter akkumuliertfailures, single-doc reassigned), Exit-Code-Pfade (0/1/3 inkl. No-Drafts → leere Failures → Exit 0), Config-Änderung. Keine Regressionen/Race-Conditions.Closes #210
Nachbesserung (Fable-Kontrollreview, Mistral-Resample + Qwen ohne weitere Funde)
Nebeneffekt dokumentiert:
timeout_retrieserreicht auch das litellm-Backend._backend_runtime_kwargs()(agents/base.py) reichttimeout_retriesunterschiedslos an beide Backends durch — nicht nur ans Subscription-/CLI-Backend. Im litellm-Pfad (_litellm_backend.py:_resolve_num_retries) wird derselbe Wert 1:1 zunum_retriesgemappt, das dort aber eine breitere Fehlerklasse abdeckt als beim CLI-Backend (dort ausschließlichsubprocess.TimeoutExpired): litellm retryt beiTimeoutUNDAPIErrorUNDAPIConnectionErroreinheitlich (Doku-String in_resolve_num_retries, Parität #148). D.h. das legacy-Profil-Retry-Budget (0→1, dieser PR) senkt nicht nur die Timeout-Toleranz des CLI-Pfads, sondern erhöht im litellm-Pfad zusätzlich die Toleranz gegenüber generischen API-/Verbindungsfehlern. Bewusster Nebeneffekt der gemeinsamen Config-Quelle, kein Bug — aber kein reines "CLI-Timeout-Feature", wie der PR-Titel nahelegen könnte.GUI: Exit-Code 3 (Teilverlust) wurde bislang wie ein Absturz behandelt.
generative/gui/static/app.jshatrc===3imexited-Handler in den generischen Fehler-Zweig fallen lassen:markStageError()färbte die zuletzt aktive Stage (typischerweise die erfolgreich gelaufene Stage 8) rot, das Log meldete „Lauf mit Fehlercode 3 beendet.", undloadOutputs()lief nur beirc===0— die tatsächlich geschriebenen Notes wurden dem GUI-Nutzer also gar nicht angezeigt. Fix:rc===3läuft jetzt im selben Zweig wierc===0(Stages als erledigt statt Fehler markiert, Outputs geladen, Fokus auf die Ergebnis-Sektion), zusätzlich eine eigene Warnzeile/Banner „Teilverlust: N Konzept(en) beim Extrahieren verloren (Timeout/CLI-Fehler, auch nach Retry) — Notes trotzdem geschrieben." (Wortlaut anformat_extractor_failure_report()inorchestrator.pyangelehnt; N wird aus der entsprechenden Log-Zeile geparst, fällt ohne Treffer auf eine zahlenfreie Formulierung zurück statt zu raten). Der Verlaufs-Badge (addHistoryEntry) zeigt fürrc===3konsistent „Teilverlust (Timeout)" statt des generischen „Fehlercode 3". Keine dedizierte JS-Testinfrastruktur im Repo vorhanden (keinpackage.json/jsdom/jest, geprüft) — Verhalten pernode --check(Syntax) + Code-Inspektion belegt, kein neues Test-Framework eingeführt.Testbeleg präzisiert (Punkt 1 oben,
test_sub_call_full_does_not_retry_timeout_by_default). Der bestehende Test ruftsub_call_full(...)ohnetimeout_retries-Argument auf und prüft damit nur den Modul-Default (_TIMEOUT_RETRIES, zufällig ebenfalls 0). Er hätte einen Bug wietimeout_retries or _TIMEOUT_RETRIES(0 ist falsy → fiele fälschlich auf den Default zurück) nicht erkannt. Neuer Testtest_sub_call_full_explicit_timeout_retries_zero_does_not_retrysetzt den Modul-Default explizit auf 1 (monkeypatch.setattr(sub_backend, "_TIMEOUT_RETRIES", 1)) und übergibt trotzdem explizittimeout_retries=0— beweist, dass der explizite Wert den Default überstimmt (call_count == 1, kein Retry). Dieis None-Weiche incall_full()war bereits korrekt; der Test hält das jetzt regressionssicher fest.Suite:
pytest generative lib/decision_engine/tests shared/tests -q→ 5703 passed, 3 skipped, 0 failed (inkl. 1 neuer Test aus dieser Nachbesserung).ruff format --check .+ruff check .grün.Nachbesserung Runde 2 (Opus-bestätigter Fund)
Teilverlust-Banner behauptete auch bei 0 geschriebenen Notes „Notes trotzdem geschrieben". rc=3 kann laut Orchestrator-Early-Return (
if not drafts:bei nicht-leerenextractor_failures) auch auftreten, wenn ALLE Konzepte verloren gingen und gar keine Note geschrieben wurde — der bisherige Banner-Text war dann falsch. Fix:buildExtractorLossText()erhält jetzt den echten Notes-Count ausloadOutputs()//api/outputs(nicht den geparsten Verlust-Count) und wählt beiwrittenCount === 0die neutrale Formulierung „... — keine Notes geschrieben." statt „... — Notes trotzdem geschrieben."; zusätzlich das Verlaufs-Label für rc=3 von „Teilverlust (Timeout)" auf „Teilverlust (Timeout/CLI-Fehler)" verallgemeinert, da die Ursache auch ein CLI-Fehler sein kann.