User-Entscheidung: ein Mega-Debrid-Account am Tageslimit soll bis zum Programm-Neustart uebersprungen werden, nicht alle 20s/2min neu getestet. Ground Truth (Support-Bundle gegrept): der limitierte Account liefert im Web-Pfad NIE eine unterscheidbare Meldung — "Kein Server" = 0 Treffer, "Antwort leer" = 20.861. Tageslimit und transienter Blip sind auf Message-Ebene nicht trennbar (generate() findet ohne processDebrid- Code keinen Code -> return null -> "Antwort leer"). Ein Trigger auf "Kein Server" waere toter Code. Loesung (Verhaltens-Signal statt Wortlaut): - megaDebridEmptyResponseStreaks zaehlt aufeinanderfolgende "Antwort leer"/"Kein Server"- Treffer je Account; ab 3 wird der Account bis Neustart geparkt (until=MAX_SAFE_INTEGER, nur In-Memory -> Neustart loescht). Erfolg/anderer Fehler setzt zurueck. - classifyAccountFailure markiert beide Signale als limitSignal (Symmetrie: ein einzelner evtl. transienter Treffer parkt NICHT, behaelt kurzen Cooldown). - Skip-Branch: "uebersprungen (bis Neustart gesperrt)", traegt nicht zu earliestCooldownUntil bei (kein absurder Retry-Timer); Post-Loop wirft klare Endmeldung wenn alle geparkt. - generate() surfacet "Kein Server" zusaetzlich als Page-Error (falls es doch im HTML steht). - UI: Rotations-Verlauf zeigt "bis Neustart gesperrt". Verifiziert: tsc 9 (Baseline), 655 Tests + 5 neue (inkl. Wiring-E2E der eine echte leere Antwort durch unrestrictWithAccounts->classify->catch->Park treibt), Build gruen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
138 lines
8.3 KiB
Markdown
138 lines
8.3 KiB
Markdown
# Lessons
|
||
|
||
## 2026-05-31 — Fix-Diagnose EMPIRISCH bestätigen, bevor man released (Timeout ≠ Account-Hänger)
|
||
|
||
**Muster:** "acc2/acc3 nie versucht" wurde als "acc1 hängt → Per-Account-Timeout +
|
||
Rotation" diagnostiziert und als v1.7.168 released. Falsch: Mega-Debrid-**Web** ist eine
|
||
180s-Polling-Schleife (`mega-web-fallback.ts`) — acc1 *pollte* legitim, der 60s-Global-
|
||
Timeout (nicht "Hängen") schnitt es ab. Mein 25s-Per-Account-Cap machte es SCHLIMMER
|
||
(endlose 25s-Rotation, Datei nie aufgelöst). Erst der User-Log + Lesen der Provider-
|
||
Impl deckte es auf. Revert v1.7.169.
|
||
|
||
**Regel:**
|
||
- Ein Timeout bei einem langsam-pollenden Provider ist KEIN Account-Fehler → darf keine
|
||
Rotation/kein Skippen auslösen. Vor "Account hängt"-Annahmen die Provider-Impl lesen
|
||
(Polling? internes Ceiling? wie lange dauert ein Erfolg legitim?).
|
||
- Bei zwei gegensätzlichen Diagnosen (hier: Timeout-zu-kurz vs. IP-Block — stand in der
|
||
EIGENEN Memory!) NICHT die bequeme wählen + releasen. Erst empirisch diskriminieren
|
||
(Env-Var auf Server, Beobachtung, oder gezielte User-Frage). Ein Symptom, das BEIDE
|
||
Hypothesen gleich gut erklärt ("Timeout nach Xs"), beweist keine.
|
||
- NICHT lokal "verifizieren" wenn das Problem umgebungsspezifisch ist (geblockte
|
||
Server-IP) — lokaler Erfolg ist falsch-positiv.
|
||
|
||
## 2026-05-30 — Abgestürzten/„aufgehängten" Chat fortsetzen: zuerst reflog lesen
|
||
|
||
**Muster:** User bat, einen anderen, aufgehängten Chat-Strang „zu Ende zu bringen".
|
||
Der Working Tree sah harmlos aus (nur untracked), aber der eigentliche Fortschritt lag
|
||
in einem per `reset --hard HEAD~1` weggesetzten Commit, der nur noch im **reflog**
|
||
(dangling) lebte.
|
||
|
||
**Regel:** Bei „mach weiter wo es hing":
|
||
1. `git reflog` + `git log --oneline -20` zuerst — Ground Truth, NICHT der
|
||
(evtl. stale) gitStatus-Snapshot oder Konversations-interne Annahmen.
|
||
2. Reset-weggesetzte/dangling Commits (`git fsck --lost-found`, reflog) inspizieren
|
||
(`git show <sha>`) — dort steckt oft die unfertige Arbeit.
|
||
3. **Verstehen WARUM weggesetzt**, bevor man blind cherry-picked: hier brach ein
|
||
bestehender Test (`.toBe(signal)`-Identitätscheck), den der Fix zwingend ändert.
|
||
Der Reset war die Reaktion darauf, nicht „Fix war falsch". Erst die Reset-Ursache
|
||
beheben (Test auf Verhalten umstellen), dann den Fix recovern.
|
||
4. Eigene Memory (`project_*`) lesen — sie dokumentierte Bug + intendierten Fix exakt.
|
||
|
||
## 2026-05-30 — Release verifizieren BEVOR "fertig" gesagt wird; curl -F mit Leerzeichen im Pfad
|
||
|
||
**Muster A (Edit ins Leere + trotzdem released):** Ein Edit schlug fehl ("String not
|
||
found"), ich habe es übersehen, committet und v1.7.165 released — die Datei enthielt
|
||
das Feature NICHT. Erst der nächste Blick zeigte es.
|
||
**Regel:** Nach jedem Feature-Edit VOR dem Release `git show HEAD:datei | grep <marker>`
|
||
— bestätigen dass der Code wirklich im Release-Commit ist, nicht nur dass `git commit`
|
||
durchlief.
|
||
|
||
**Muster B (Gitea UNIQUE constraint):** `npm run release:gitea` pusht erst den Tag,
|
||
dann erstellt es den Release. Gitea legt beim Tag-Push automatisch einen Tag-Release-
|
||
Eintrag an (name=null). `fetchExistingRelease` im Script matcht den nicht → POST create
|
||
→ `UNIQUE constraint failed: release.repo_id, release.tag_name`. Commit + Tag sind dann
|
||
schon gepusht, nur der Release+Assets fehlen.
|
||
**Recovery:** `GET /api/v1/repos/.../releases/tags/<tag>` → id holen → `PATCH releases/<id>`
|
||
mit name/body/draft:false → Assets per `POST releases/<id>/assets?name=<url-encoded>` hochladen.
|
||
|
||
**Muster C (curl -F Datei mit Leerzeichen):** `curl -F "attachment=@release/Datei mit
|
||
Leerzeichen.exe.blockmap"` lädt FALSCHEN Inhalt hoch (Server-Size != lokale Size).
|
||
**Regel:** Datei mit Leerzeichen im Namen erst nach `/tmp/leerzeichenfrei` kopieren,
|
||
DAS hochladen, Asset-Name über `?name=<url-encoded>` setzen. Danach Server-Size gegen
|
||
lokale Size prüfen.
|
||
|
||
|
||
|
||
## 2026-05-30 — Nicht in chaotische Parallel-Tool-Batches verfallen (User-Korrektur: "bist du in nem endless loop")
|
||
|
||
**Muster:** Bei einem großen Multi-File-Edit habe ich Dutzende Tool-Calls (Bash-Probes,
|
||
Reads, Edits, Python-Inline-Skripte, mehrfache tsc-Läufe) in EINEN Message-Block gepackt.
|
||
Resultat: Ein einzelner Fehler/Cancel hat die ganze parallele Kette abgebrochen, Edits
|
||
landeten halb, ich verlor den Überblick welche Änderung wirklich auf Disk war, und es
|
||
wirkte wie eine Endlosschleife. Dazu: wegwerf-`scripts/_*.py`/`_*.txt` als Workaround
|
||
gegen Output-Encoding statt der dedizierten Tools.
|
||
|
||
**Regel:**
|
||
- Edits über mehrere Dateien **sequenziell, einer nach dem anderen**, mit kurzer
|
||
Verifikation dazwischen — nicht 20 spekulative Calls auf einmal.
|
||
- Nach jedem Edit, der fehlschlagen kann (Anchor evtl. nicht eindeutig), das Ergebnis
|
||
lesen, bevor der nächste folgt. Edit/Write erroren laut — darauf vertrauen.
|
||
- KEINE Wegwerf-Python-Skripte ins Repo schreiben, um Shell-Output zu parsen. `Grep`/
|
||
`Read`/`Edit` nutzen. Wenn doch ein Temp nötig ist: nach `os.tmpdir()`, nie nach
|
||
`scripts/`, und sofort wieder löschen.
|
||
- Verifikation gebündelt am ENDE (1× tsc, 1× build, 1× vitest), nicht 10× zwischendrin.
|
||
|
||
|
||
## 2026-05-28 — Analyse-Befund gegen beobachtete Realität gaten (Advisor-Korrektur)
|
||
|
||
**Muster:** Meine Analyse sagte einen *häufigen* Bug voraus (jede letzte Datei im
|
||
Standard-Modus + jede Nested-Datei landet unbenannt), während der User nur "1-2 pro
|
||
Staffel" meldete. Ich habe die Diskrepanz bemerkt ("zu schwer um unbemerkt zu bleiben")
|
||
und sie mit weiterem Timing-Argument wegrationalisiert.
|
||
|
||
**Regel:** Wenn die eigene Analyse etwas vorhersagt, das der beobachteten Realität
|
||
widerspricht, NICHT die bequeme Lesart wählen — **mit einem Reproduktions-Test gaten**,
|
||
bevor man fixt. Failing Test gegen den Ist-Stand zuerst (TDD/systematic-debugging Phase 4):
|
||
- reproduziert → Bug bestätigt, mit Sicherheit fixen.
|
||
- reproduziert nicht → Analyse hat eine Mitigation übersehen, kein Fix für Nicht-Bug.
|
||
|
||
## 2026-05-28 — Crash-Debris im Working Tree: stashen, nicht verwerfen
|
||
|
||
**Muster:** Eine abgestürzte Session (API 400) hinterließ ein uncommittetes Working Tree,
|
||
das drei releaste Commits revertierte. Verlockung: `git checkout`/discard, um clean HEAD
|
||
zu bekommen.
|
||
|
||
**Regel:** Fremde/unverstandene uncommittete Änderungen **`git stash`** (non-destruktiv,
|
||
recoverable), nie blind verwerfen. Gibt clean HEAD, nichts geht verloren, kein Stall auf
|
||
User-Rückfrage. Danach dem User sagen WAS gestasht wurde und WARUM.
|
||
|
||
## Wiring-Lock vs. Mechanism-Test
|
||
|
||
Ein Test, der eine Hilfsfunktion mit dem richtigen Flag direkt aufruft, beweist nur, dass
|
||
das Flag funktioniert — NICHT, dass der Produktionspfad das Flag setzt. Für echte
|
||
Absicherung einen End-to-End-Test durch den realen Einstiegspunkt fahren und per
|
||
Negativ-Gate (Flag temporär entfernen → Test muss fallen) verifizieren.
|
||
|
||
## 2026-05-31 — Log-Symptom ≠ User-Wortlaut: greppen, bevor man auf eine Meldung triggert
|
||
|
||
**Muster:** User meldete Mega-Debrid-Tageslimit als „Kein Server für diesen Hoster". Ich
|
||
wollte den Fix an genau diese Meldung (`MEGA_DEBRID_NO_SERVER_RE`) hängen. Der Advisor
|
||
stoppte: der Screenshot zeigte als Cooldown-Grund **„Antwort leer"**, nicht „Kein Server".
|
||
|
||
**Beweis (Support-Bundle gegrept):** „Kein Server"/„Erreur"/„aucun serveur" = **0** Treffer
|
||
im ganzen Bundle, „Antwort leer" = **20.861** Treffer. Der limitierte Account liefert im
|
||
Web-Pfad NIE eine unterscheidbare Meldung — `generate()` findet ohne `processDebrid`-Code
|
||
keinen Code → `return null` → der Aufrufer macht daraus „Antwort leer". Ein Trigger auf
|
||
„Kein Server" wäre toter Code gewesen (= die v1.7.172-Falle, zum 2. Mal fast getreten).
|
||
|
||
**Regel:** Bevor man einen Fix an einen bestimmten Meldungstext hängt, in den ECHTEN Logs
|
||
greppen, ob dieser Text dort überhaupt vorkommt (`count`-Mode, alt-Text vs. Ist-Text). Sind
|
||
zwei Fälle auf Message-Ebene nicht unterscheidbar (Tageslimit vs. transienter Blip → beide
|
||
„Antwort leer"), nicht raten — über ein **Verhaltens-Signal** klassifizieren: hier eine
|
||
Streak (3× hintereinander leer → geparkt), nicht der einmalige Wortlaut.
|
||
|
||
**Wiring-Test nicht vergessen** (eigene Lesson): die Helfer-Unit-Tests beweisen nur den
|
||
Zähler. Ein E2E-Test muss eine ECHTE leere Antwort durch den realen Einstiegspunkt
|
||
(`unrestrictWithAccounts` → `classifyAccountFailure` → catch → Park) treiben, sonst bleibt
|
||
unbewiesen, dass der Produktionspfad das Signal überhaupt setzt.
|