Skip to content

fix: Retry bei stillem <!--END-->-Drop + unterscheidbarer Exit-Code bei Totalverlust (#280, #281) - #297

Merged
TillQuandel merged 1 commit into
masterfrom
fix/280-281-stiller-verlust
Jul 15, 2026
Merged

fix: Retry bei stillem <!--END-->-Drop + unterscheidbarer Exit-Code bei Totalverlust (#280, #281)#297
TillQuandel merged 1 commit into
masterfrom
fix/280-281-stiller-verlust

Conversation

@TillQuandel

Copy link
Copy Markdown
Owner

Zusammenfassung

Kombinierter PR fuer zwei verwandte, aus derselben Testlauf-Serie (2026-07-14) stammende Befunde: ein Konzept faellt still aus dem Extractor-Output (#280), und dieser Verlust ist im Exit-Code/Summary nicht sichtbar (#281). Beide Fixes liegen in derselben Region der Extraction-Pipeline, sind aber unabhaengig voneinander lauffaehig -- daher ein PR, zwei klar getrennte Aenderungsbloecke (extractor.py fuer #280, orchestrator.py fuer #281).

#280 -- Stiller Konzeptverlust via <!--END--> ohne Retry

Problem: Wenn der Extractor ein zugewiesenes Konzept im Textfenster nicht ausreichend belegt sieht, antwortet er prompt-konform nur mit <!--END--> (kein <!--NOTE-->-Block). Diese None-Rueckgabe wurde ohne jeden Retry als dropped/empty_extraction verbucht -- im Gegensatz zum bestehenden Retry-Mechanismus fuer abgeschnittene (truncated) Bodies (B2). Traf Kernkonzepte in 3 von 6 Testlaeufen, u.a. "Amotivation" ([high]-priorisiert bei Deci & Ryan, in keiner der finalen Notes abgedeckt).

Ursache: generative/agents/extractor.py:417-424 gab bei leerem items sofort None zurueck, ohne den bereits fuer Truncation existierenden Retry-Pfad (extractor.py:428-450) analog zu nutzen.

Fix: Ein zweiter Call-Versuch mit verstaerktem Hinweis ("pruefe das Fenster noch einmal sorgfaeltig, auch indirekte Belegstellen zaehlen"), bevor endgueltig None zurueckgegeben wird. Guard revision_hint is not None -> kein Retry (dieselbe Bedingung wie beim Trunkierungs-Retry) verhindert eine Retry-Kaskade im Self-Refine/Critic-Loop.

Bewusst ausgeklammert: Die im Issue als Fix-Richtung (b) separat genannte Root-Cause-Untersuchung der Planner-Konzept-zu-Chunk-Zuordnung (concept_text_window()) ist NICHT Teil dieses Fixes -- das Issue nennt sie als optionale/investigative Ergaenzung ("ggf."), nicht als Pflichtteil. Der Retry laeuft weiterhin auf demselben Textfenster, das dem Extractor schon beim ersten Versuch vorlag; er behebt den "kein Retry"-Bug, nicht zwangslaeufig Faelle, in denen das Fenster die Belegstelle tatsaechlich nie enthielt.

Testnachweis:

  • RED: test_silent_end_drop_triggers_retry_and_recovers schlug fehl (assert 1 == 2, kein zweiter Call).
  • GREEN nach Fix: 3/3 Tests in generative/tests/test_extractor_empty_end_retry.py gruen (Retry-Erfolgsfall, Retry-auch-leer-Fall, Self-Refine-Guard).

#281 -- Totalverlust endet mit "Fertig."/Exit 0

Problem: Ein Lauf mit Totalverlust (0 Notes trotz >=1 geplantem/versuchtem Konzept) endete bisher mit der normalen Erfolgsmeldung "Fertig." und Exit-Code 0 -- ununterscheidbar von einem legitimen 0-Konzepte-Lauf (Konzeptmangel in der Quelle). Historischer Beleg: Lauf 1 verlor sein einziges geplantes Konzept durch 2x claude-CLI-Timeout, das einzige Fehlersignal stand nur in stderr.

Live-Luecke im aktuellen Code: extractor_failure_exit_code() (Issue #210) wertet ausschliesslich ctx.extractor_failures -- also nur harte Exceptions (Timeout/CLI-Fehler nach Retries). Der stille <!--END-->-Drop aus #280 (run_per_concept liefert None OHNE Exception) zaehlt zwar in dropped_total mit, faellt aber durch dieses Sieb und liefert weiterhin Exit-Code 0.

Fix: Neue Funktion total_loss_exit_code() in generative/orchestrator.py hebt den Exit-Code auf einen neuen, unterscheidbaren Wert _EXIT_TOTAL_LOSS = 4, wenn 0 finale Notes trotz >=1 versuchtem Konzept vorliegen -- unabhaengig davon, ob die Ursache eine Exception (bisher Code 3) oder ein stiller Drop (bisher Code 0) war. Zusaetzlich eine prominente stdout-Warnzeile ("⚠️ TOTALVERLUST: ...") an beiden fruehen Return-Punkten der Extraction-Stage (kein Konzept extrahiert / alle Drafts als Artefakte verworfen), nicht nur stderr-Details wie bisher. Ein legitimer 0-Konzepte-Lauf (n_attempted == 0) bleibt unveraendert bei Exit 0.

Bewusst begrenzter Scope: Die Pruefung greift an den beiden extraction-stufigen frühen Return-Punkten (vor Dedup/Stage-6), NICHT am finalen Vault-Writer-Return. Grund: dort koennte written == 0 auch durch legitimes Dedup entstehen (alle extrahierten Konzepte sind reine Duplikate bereits existierender Vault-Notes) -- das waere ein False-Positive-Totalverlust, kein echter Ausfall.

Exit-Code-Kompatibilitaet geprueft: generative/gui/runner.py/app.py/run_history.py reichen rc unveraendert durch (kein Enum/Whitelist). Das Frontend (app.js) hat Spezialbehandlung nur fuer rc===0 und rc===3; ein neuer Wert (4) faellt in den generischen Fehler-Zweig (markStageError + Fehlercode ${rc}) -- korrektes Verhalten fuer einen Totalverlust, keine Test-Regression (volle Suite inkl. generative/gui/tests gruen).

Testnachweis:

  • RED: 5/6 Tests in test_orchestrator_total_loss_exit_code.py schlugen mit AttributeError fehl (Funktion/Konstante existierten noch nicht); die Gegenprobe (legitimer 0-Konzepte-Lauf) war bereits vorher gruen (Referenzverhalten unveraendert).
  • GREEN nach Fix: 6/6 Tests gruen (reine Helper-Faelle + main()-Integration ueber --load-drafts).

Suite + Lint

  • Volle Suite generative lib/decision_engine/tests shared/tests: 5945 passed, 3 skipped, 8 deselected (keine Fehlschlaege).
  • ruff check + ruff format --check auf allen 4 geaenderten Dateien: sauber.

Geaenderte Dateien

Fixes #280
Fixes #281

…rscheidbarer Exit-Code bei Totalverlust (#280, #281)

#280: Wenn der Extractor ein Konzept im Textfenster nicht ausreichend
belegt sieht, antwortet er prompt-konform nur mit <!--END--> (kein
<!--NOTE-->-Block) -- diese None-Rueckgabe wurde bisher ohne jeden
zweiten Versuch verbucht, im Gegensatz zum bestehenden Trunkierungs-
Retry (B2). Traf Kernkonzepte in 3 von 6 Testlaeufen (u.a. "Amotivation",
[high]-priorisiert bei Deci & Ryan). Fix: derselbe Retry-Gedanke fuer
den Empty-Fall -- ein zweiter Call-Versuch mit verstaerktem Hinweis,
bevor endgueltig None zurueckgegeben wird. Bleibt auf den Retry selbst
beschraenkt; die im Issue als Fix-Richtung (b) separat aufgefuehrte
Root-Cause-Untersuchung der Planner-Konzept-zu-Chunk-Zuordnung ist
nicht Teil dieses Fixes.

#281: Ein Lauf mit Totalverlust (0 Notes trotz >=1 versuchtem Konzept)
endete bisher mit "Fertig." und Exit-Code 0 -- ununterscheidbar von
einem legitimen Konzeptmangel-Lauf. Luecke: extractor_failure_exit_code()
wertet nur harte Exceptions (ctx.extractor_failures), nicht den stillen
#280-Drop. Fix: total_loss_exit_code() hebt den Exit-Code auf einen neuen,
unterscheidbaren Wert (_EXIT_TOTAL_LOSS=4) wenn 0 finale Notes trotz >=1
versuchtem Konzept vorliegen, plus eine prominente stdout-Warnzeile
("TOTALVERLUST") zusaetzlich zu den bisherigen stderr-Details. Ein
legitimer 0-Konzepte-Lauf (nichts versucht) bleibt unveraendert bei
Exit 0.

Tests: test_extractor_empty_end_retry.py (Retry-Pfad, Retry-Guard im
Self-Refine-Loop), test_orchestrator_total_loss_exit_code.py (reine
Helper-Faelle + main()-Integration ueber --load-drafts). Volle Suite
(generative + lib/decision_engine/tests + shared/tests): 5945 passed,
3 skipped, 8 deselected.
@TillQuandel
TillQuandel merged commit 1e3b035 into master Jul 15, 2026
4 checks passed
TillQuandel added a commit that referenced this pull request Jul 17, 2026
…#335)

Root-Cause (#280/#297): der Planner weist Konzepten Chunks zu, die die
Belegstelle nicht ausreichend enthalten; das 400-Wort-Fenster
(concept_text_window) traf sie dann auch nach dem #280-Retry nicht --
der #297-Retry lief auf demselben Fenster und verdoppelte nur die
Token-Kosten des Fehlversuchs, ohne die Ursache zu beheben.

Fix: bleibt ein Konzept nach Erst-Call UND #280-Retry (extractor.
run_per_concept) weiterhin leer, folgt auf Orchestrator-Ebene genau EIN
Rescue-Versuch mit deutlich groesserem Fenster (400 -> 1200 Woerter,
_RESCUE_WINDOW_WORDS). Neuer Parameter retry_empty=False an
run_per_concept unterdrueckt dabei den internen #280-Retry, damit der
Rescue max. 1 statt bis zu 2 Zusatz-Calls kostet. Erfolg -> Draft zaehlt
normal, kein dropped-Event; Fehlschlag -> weiterhin dropped/
empty_extraction. Kein Rescue-Call, wenn das expandierte Fenster
identisch zum urspruenglichen waere (z.B. sehr kurze Dokumente).

Log-Signaturen: [extractor-window-rescue] / [extractor-window-rescue-failed].

Co-authored-by: TillQuandel <tillq@live.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants