54 KiB
Lessons
2026-08-07 — Einen kleinen Release nicht durch redundante Gates aufblasen
Symptom: Die angefragten Änderungen waren implementiert und getestet, trotzdem lief die Arbeit durch wiederholte Status-, Review- und Harness-Schleifen übermäßig lange weiter. Root cause: Pflichtsicherheit, bereits belegte Wiederholungsprüfungen und optionale Zusatzdiagnostik wurden nicht hart getrennt. Technische Harnessfehler führten zu weiteren Schleifen, obwohl Produktcode und Kernlauf bereits grün waren. Regel: Nach grüner Implementierung genau eine risikogerechte Pflichtkette fahren: Tests, Build, Secret-Gate, realer Lauf, Positivliste, Veröffentlichung, Redownload. Bereits belegte Gates nicht wiederholen und optionale Diagnose sofort streichen, sobald sie die Auslieferung verzögert. Wie anwenden: Vor jedem zusätzlichen Check benennen, welche noch offene Release-Invariante er beweist. Beweist er keine neue Pflichtinvariante, wird er nicht ausgeführt.
2026-06-21 — Das eigene Instrument lügt nicht, aber sein Log-Code kann buggen (queue=undefined)
Symptom: Drei Builds lang jagte ich Read-Bursts (highWaterMark, threadpool), während der WAHRE Treiber
eine 38,5-MB-electron-config.json war, die 137×/73s geklont/geparst/serialisiert wurde (~47% Main-Thread).
Erst das in v3.3.98 eingebaute config-load/config-serialize-Log machte es sichtbar — aber mein eigenes
Log-Feld queue= las .length auf dem pendingQueue-OBJEKT (immer undefined) und hätte mich fast in die
falsche Richtung (pendingQueue statt history) geschickt.
Root cause: (1) Ich hatte die config-Persistenz als „instrumentieren, nicht fixen" zurückgestellt (richtig
für die unsichere Migration), aber den Lag-Treiber dort nicht früh genug vermutet. (2) Instrument-Felder selbst
müssen verifiziert werden: (obj || []).length auf einem Objekt = undefined, still falsch.
Regel: Wenn der User „es laggt unverändert" sagt obwohl die letzte Messung gut aussah, ist der gemessene
Pfad NICHT der Hot-Path — sofort BREITER messen (jeden IPC-Handler, jede periodische Main-Op, Main-Thread-
Longtask-Monitor), nicht den schon-gemessenen Pfad weiter optimieren. Und Instrument-Ausgaben gegen ein
bekanntes Beispiel prüfen (zeigt queue= je eine echte Zahl?).
Wie anwenden: Bei „Symptom unverändert trotz Fix": Hypothese fallen lassen, Coverage verbreitern. Log-Felder
beim Schreiben mit einem realen Wert gegenchecken, nie blind (x||[]).length auf unklar getypten Feldern.
2026-06-21 — „Brot finden, nicht Krümel": die EINE Änderung, die alle Kosten killt, schlägt drei sichere Teilfixes
Symptom: Fix-Design bot loadShallow (Klon vermeiden) + cache-repopulate + resolution-cache. Adversary zeigte: loadShallow killt nur den Klon (~10s von 34s), die 38 Serializes (8,4s) + 38 Post-Write-Reparses (15,2s) bleiben, weil Writes den Cache nullen → loadShallow allein = Krümel. Root cause: Alle drei Kosten (parse+clone+serialize) entstehen daraus, dass history IM Hot-Config liegt. Nur history RAUS aus der immer-geladenen Datei (eigene electron-history.json) killt alle drei gleichzeitig. cache-repopulate-Gate feuerte nie (toter Code); resolution-cache hätte stale-Pools → Failover-Regression (rotation/byse) riskiert = die EINE Sache die Uploads STILL korrumpiert, schlimmer als Lag. Regel: Wenn der User „komplett wegmachen" fordert und mehrere sichere Teilfixes vs. ein riskanterer Komplettfix zur Wahl stehen: den Komplettfix nehmen, aber RICHTIG absichern (hier: fsync+verify-before-strip, permanenter .pre-history-split.bak, Migration packaged-only + per-Init nicht in load(), Crash-Window-Fallback, Test gegen die ECHTE 194MB-Fixture). Teilfixes die den Treiber nur anknabbern NICHT bündeln (verwässert Messung
- Risiko). Einen Fix der etwas STILL korrumpieren könnte (stale Account-Pools) NIE für Performance einbauen. Wie anwenden: Bei mehreren Fix-Optionen fragen: „welche EINE Änderung entfernt die gemeinsame Wurzel ALLER Kostenpfade?" — die nehmen und maximal absichern, statt N sichere Teilfixes die je nur einen Pfad treffen.
2026-06-21 — Ein-Variablen-Disziplin: nicht zwei Fixes bündeln, wenn einer den anderen maskiert
Symptom: Nach dem tp=8-Win wollte ich in EINEM Build A (1MB highWaterMark, Read-Burst) + B (Renderer chunked rAF Batch-Drain, der 243ms-Longtask) + C-Instrument shippen. Root cause / Korrektur (Advisor): Der Renderer war 14/15 Fenstern gesund; der EINE 243ms-Longtask (W14) ist laut beiden Agenten DOWNSTREAM des Main-Thread-Read-Bursts (geflutetes IPC). Fix A reduziert diese Stalls → der Renderer-Longtask verschwindet wahrscheinlich OHNE B. B mitzuliefern (a) verwässert die nächste Messung (war die Besserung A oder B?) und (b) fasst den Progress-Hot-Path an, der hier schon gebissen hat (formatDateTime-Burst, ghost-fix). Regel: Wenn Fix A einen vermuteten Symptom-Treiber X reduziert und Fix B genau X behandeln würde — NUR A shippen, messen, B nur nachziehen wenn X überlebt. Sonst kann das nächste Log nicht sauber attribuieren. Bei gekoppelten Symptomen ist die Reihenfolge (Upstream-Fix zuerst, dann messen) wichtiger als „alles auf einmal". Wie anwenden: Vor dem Bündeln fragen: „Maskiert Fix A die Wirkung, die Fix B beheben soll?" Wenn ja → entkoppeln, A zuerst, eine Variable pro Build.
2026-06-21 — Nicht aus EINEM konfundierten Sample eine Ursache behaupten
Symptom: Ich wollte dem User sagen „1-Sekunden-Persist-Freeze gefunden" auf Basis von W13 (max=1021ms, heap→142MB). Root cause / Korrektur (Advisor): W13 ist EIN Sample und konfundiert (hat gleichzeitig FSReqCallback=66) und das EINZIGE Heap-Spike-Fenster. Die anderen isolierten Maxes (W4 415ms/heap41, W10 852ms/heap18) haben NIEDRIGEN Heap → sind KEIN 140MB-structuredClone+stringify → eine andere Ursache (account-failed sync load() nahe Connection-Churn). Eine Behauptung aus einem konfundierten Punkt hätte den falschen Fix priorisiert. Regel: Bei isolierten Spitzen erst die Co-Signale (heap, FSReq, gc, Nachbarfenster) gegenchecken, ob sie EINE Familie sind. Wenn die Magnitude-Signatur (hier: Heap-Spike) nicht bei allen passt → es sind mehrere Ursachen. „Instrumentieren + bestätigen", nicht „gefunden", solange nur ein konfundierter Punkt existiert. Wie anwenden: Vor „Ursache X gefunden": gibt es ≥2 unkonfundierte Samples mit derselben Signatur? Wenn nein → als Hypothese formulieren und messen, nicht als Befund verkaufen.
2026-06-21 — Histogram-Korrelation beweist KEINE Kausalrichtung; rss-Mathe als Sanity-Check
Symptom: ELD-Spikes korrelierten exakt mit hohem FSReqCallback (File-Reads in flight) → ich wollte
sofort ein Read-Concurrency-Semaphore über 5 Dateien bauen.
Root cause / Korrektur (Advisor): (1) Korrelation ≠ Kausalität: hohe in-flight-Reads können auch SYMPTOM
sein — ein aus ANDEREM Grund blockierter Loop drained die Read-Completions nicht, also stapeln sie sich im
Snapshot. (2) rss-Mathe widerlegte meine „Read-Buffer ballonen den Speicher"-These: 70 Streams × 256KB ≈
18MB, aber rss schwang ~300MB → das ist Heap-/Objekt-Churn (GC), nicht die Read-Buffer.
Regel: Bevor ich auf Basis einer Histogramm-Korrelation einen Multi-File-Refactor baue: (a) Kausalrichtung
mit einem BILLIGEN reversiblen 1-Zeilen-Hebel testen (hier UV_THREADPOOL_SIZE 64→8), (b) die Größenordnung
gegenrechnen (passt die vermutete Quelle zahlenmäßig zur beobachteten Wirkung?), (c) den fehlenden Co-Faktor
(GC) erst MESSEN, bevor ich ihn aus- oder einschließe. Ein Build kann gleichzeitig Kandidaten-Fix UND
Diskriminator sein.
Wie anwenden: Bei „X korreliert mit Y, also fixe X": erst fragen „könnte Y → X statt X → Y?" und „passt
die Magnitude?". Wenn nein/unklar → erst der billige reversible Knopf + Messung, dann der teure Refactor.
2026-04-21 — DOM-Doppelrender bei Bulk-State-Changes
Symptom: User klickt auf "Erneut versuchen" mit 500+ Jobs → App hängt sekundenlang.
Root cause: retrySelectedJobs() ruft renderQueueTable + updateQueueActionButtons + updateStatusBar auf, startSelectedUpload() ruft direkt danach genau dieselben Funktionen nochmal auf.
Regel: Wenn ein Click-Handler await anotherHandler() aufruft und der innere Handler seinen eigenen kompletten Render-Zyklus hat, NIEMALS noch einen davor. Einmal ist genug — der folgende innere Render sieht die frischen State-Mutationen ohnehin.
Wie anwenden: Vor jeder await fn()-Folge in einem Handler prüfen: macht fn schon renderQueueTable()? Wenn ja, äußere Render-Calls löschen.
2026-04-21 — State-Checks MÜSSEN hinter die Semaphore-Queue
Symptom: Pre-Job-Swap prüfte _failedAccounts vor semaphore.acquire. Bei N parallelen Workers war der Check zum Start für ALLE leer — niemand hat geswapt. Erst nachdem alle im Semaphore ordentlich gewartet hatten und einer fehlschlug, wurde _failedAccounts befüllt, aber die anderen hatten ihren Check längst hinter sich.
Regel: State-basierte Entscheidungen (failed accounts, overrides, cached stats) gehören direkt vor die Aktion die sie betreffen — nach jeder async await die die Position in der Queue bestimmt. Nicht am Task-Start für später wichtigen State abfragen.
Wie anwenden: Bei Queue-basierten Pipelines prüfen: "Was kann sich zwischen Task-Start und dem tatsächlichen Execute ändern?" Alles was sich ändern kann, muss direkt vor dem Execute geprüft werden, nicht davor.
2026-04-21 — Reaktive Config-Updates für laufende State-Maschinen
Symptom: User fügt mid-batch einen neuen Account hinzu, aber der UploadManager merkt nicht dass die Config sich geändert hat. account-failed Event feuert nur einmal pro Account → keine zweite Re-Resolve-Chance.
Regel: Wenn ein State nur bei Events neu evaluiert wird und Events "nur einmal" feuern, muss jede externe Zustandsänderung (Config-Save, User-Action) den State explizit triggern.
Wie anwenden: Save-Handler müssen aktive State-Maschinen informieren. Lieber einen überflüssigen Re-Resolve-Call als einen verpassten. Für Upload-Manager: nach saveConfig → re-evaluate failed accounts ohne Override.
2026-04-21 — Error-Klassifikation: fileRejected vs accountError
Symptom: Voller Byse-Account wurde nicht rotiert — skip-rotation-file-rejected geloggt für jede Datei.
Root cause: Generisches Match auf Prefix-String ("lehnte Datei ab") klassifizierte ALLE Byse-Errors als file-level, inklusive Account-voll-Meldungen.
Regel: Hoster-Parser setzen den spezifischen Flag (fileRejected ODER accountError), nicht beide nie. Classifier matcht konkrete Phrasen (Duplicate, Not video format, …), niemals generische Wrapper-Strings die für mehrere Fehlerarten benutzt werden.
Wie anwenden:
- Bei neuen Hostern: per-status-Klassifikation bereits im Parser, nicht erst im Upload-Manager.
- Classifier-Regexes auf Rejection-Kernphrasen, nicht auf UI-Prefix.
- Defensive:
accountError === truegewinnt immer gegenfileRejected— Account-Rotation ist weniger schlimm als endlose Fails auf einem toten Account.
2026-04-21 — Keine fake Build-ETAs
Symptom: User wartet 5+ min auf Tauri-Build den ich mit "1-2min" angekündigt habe.
Regel: Tauri-Release-Builds brauchen real 3-6 min (Rust + NSIS + MSI). Keine Zeitangabe oder ehrlich "kann 3-6min dauern" schreiben.
Wie anwenden: Wenn User nach Status fragt: sofort tail des Logs + ls des Bundle-Ordners zitieren, nicht raten.
2026-05-24 — Packaged-Electron Log-Pfade: nie __dirname/.. zum Schreiben
Symptom: doodstream-debug.log hatte auf dem Server null aktuelle Einträge; nur alte Dev-Logs. Fehler "kein Filecode" war nicht diagnostizierbar.
Root cause: path.join(__dirname, '..', 'x.log') zeigt im gepackten Build in resources/app.asar (read-only). fs.appendFileSync wirft EACCES, der try/catch schluckt es → null Production-Logs.
Regel: Schreibbare Pfade IMMER über app.getPath('userData') (lazy require('electron'), Fallback __dirname/.. nur für Tests/plain-node). Gilt für jede Datei die der gepackte App schreibt.
Wie anwenden: Bei jedem neuen Log/Cache/State-File prüfen: wohin schreibt das im NSIS-Build? Nicht ins Install-Verzeichnis, nicht in asar.
2026-05-24 — Hoster-Fehler: echten Status surfacen, nicht generisch schlucken
Symptom: "upload_result Seite hat keinen filecode ()" — nichtssagend; User dachte doodstream-Format geändert.
Root cause: XFileSharing liefert den echten Grund im st-Feld (Error: duplicate / file too big / …). Code ignorierte st komplett und warf nur den leeren Body.
Regel: Bei Hoster-Parsefehlern immer die Server-Statusfelder (st/msg/code) + Kontext (welcher CDN-Node, war filecode da) in die Fehlermeldung packen. Format-Struktur unverändert + leerer Inhalt = Backend-Ablehnung, kein Parsing-Bug.
2026-05-25 — Queue leer nach Update: Auto-Dedup zu aggressiv (nicht Save/Restore)
Symptom: Queue gestoppt, App-Update -> nach Neustart Queue leer ("Dateien hierhin ziehen"). User dachte Save/Restore kaputt.
Root cause: Queue WIRD korrekt gespeichert (pendingQueue) + restored. ABER _autoDeduplicateFromLog (läuft bei init nach restore) entfernte Jobs per fileName|hoster-Match gegen das GESAMTE Lifetime-fileuploader.log — UNABHÄNGIG vom Status. Pending 'preview'-Jobs, deren Datei früher mal hochgeladen wurde, flogen alle raus -> komplette Queue weg. "Update-spezifisch" nur weil der Server-App nur beim Update neustartet (normaler Restart hätte dasselbe getan).
Verifiziert: Reale electron-config.json: 4 preview-Jobs, alle 4 Keys im Log -> alte Logik entfernt 4/4. Neue Logik (nur status==='done' droppen) entfernt 0/4.
Regel: Auto-Cleanup/Dedup darf NIE pending/actionable User-Arbeit löschen. Nur genuin abgeschlossene ('done') Jobs decluttern. Lifetime-Logs sind Historie, nicht Session-Fortschritt — nicht als "schon erledigt"-Quelle für pending Jobs missbrauchen.
Wie anwenden: Bei jeder Filter/Remove-Logik auf User-State: nach Status gaten, nicht nur nach Identitäts-Match gegen historische Daten.
2026-05-28 — Doodstream "kein Filecode": Web-Scraping ist die falsche Ebene, API ist der Fix
Symptom: Wiederkehrend "kein Filecode — Server gab leeren Link zurueck" bei großen Dateien (~1GB/7min Upload), trotz 3.3.26-3.3.29. Queue voll roter Fehler. Root cause (recherchiert + verifiziert): Der Web-Upload holt den Filecode aus einem XFileSharing-HTML-Formular. Bei langen Uploads kommt das Formular leer zurück, weil (a) der per-Seitenaufruf sess_id-Token über den 7min-Upload altert UND (b) der server-seitige File-Registration-Callback (cgi-bin/fs.cgi-Äquivalent) unter Last timeoutet → kein file_code gemintet. Wichtig: Das ist KEIN async-delay — die Datei taucht NICHT später in der Liste auf (die Registrierung, die sie listen würde, ist genau das was failt). File-list-Polling (wie Byse) hilft hier also kaum. Fix: Die offizielle doodapi.co JSON-API nutzen, wenn ein API-Key da ist — sie liefert result[0].filecode DIREKT in JSON (kein HTML-Formular) und nutzt einen persistenten api_key (kein alternder sess_id). Git-Historie: die API war der ORIGINAL-Pfad (initial commit); Web-Login kam später nur "als Alternative zum API-Key" — Key-Bevorzugung stellt also den gedachten Primärpfad wieder her, kämpft nicht gegen eine bewusste Entscheidung. Regel: Bei Hoster-Integrationen die offizielle API der Web-Scraping-Ebene vorziehen wo möglich. Empty-form/codeless-2xx = Hoster-Backend-Flake (hosterTransient), Account NICHT als tot markieren — auf BEIDEN Pfaden (Web + API) gleich klassifizieren. Voraussetzung: Engagiert nur wenn der Doodstream-Account einen gültigen API-Key hat (doodstream.com/settings). Keyless-Accounts bleiben beim Web-Pfad.
2026-05-28 — Doodstream empty-form: live diagnosis confirmed API path is the fix
Verifiziert mit echtem Account-Key (read-only API-Calls):
- account/info → status 200, Key gültig, Storage unlimited. Premium ABGELAUFEN (2025-10-03) — Uploads gehen TROTZDEM.
- upload/server → liefert gültigen Node (cv1130ed.cloudatacdn.com) auch ohne Premium → API-Upload-Pfad nutzbar.
- file/list → 90.548 Dateien; Uploads landen server-seitig INTERMITTIEREND (viele Burn-Notice-Folgen genau im "Fehler"-Zeitfenster vorhanden). Das leere Formular ist also nicht "immer kaputt", sondern manchmal — der Web-Form-Registrierungs-Callback (fs-public.intconnect.net) timeoutet sporadisch. Konsequenz: API-Weg (result[0].filecode inline) umgeht den failenden Callback → richtiger Fix. file/list-Recovery ist NICHT tote Last (Dateien erscheinen ja) — aber bei 90k-Accounts MUSS man sort=created&order=desc erzwingen, sonst ist die frische Datei nicht auf Seite 1. Regel: Bei "geht manchmal/manchmal nicht" + Hoster mit offizieller API: erst per read-only API-Call (account/info, file/list) gegen den ECHTEN Account verifizieren statt am Client weiterzuraten. Das beendet Spekulations-Schleifen.
2026-06-17 — Rotation-State pro-Call neu zu bauen = stiller No-Op beim Drip-Feed
Symptom: Account-Rotation (v3.3.74) verteilte korrekt bei einem Drag-Drop-Batch (N Dateien auf einmal), aber bei Folder-Monitor / Einzeldatei-Uploads landete IMMER alles auf Account 1 — die Sekundär-Accounts bekamen nichts.
Root cause: Der Picker wurde INNERHALB jedes buildUploadTasks/buildUploadTasksFromJobs-Calls frisch erzeugt → rotIdx startete jedes Mal bei 0. Der Folder-Monitor speist neue Dateien einzeln via add-jobs-to-batch in einen laufenden Batch (und per-Detection-autoStart feuert start-upload pro Datei). Jeder dieser Calls baute den Picker neu von Index 0 → 0 % len = 0 = Account 1, immer.
Verifiziert: Unit-Tests gaben falsche Sicherheit — alle 10 nutzten EINEN Picker über N Calls (testet nur Intra-Batch). Der Bug lebte genau im Cross-Call-Verhalten, das kein Test abdeckte.
Fix: Rotation-Cursor lebt AUSSERHALB des Pickers: modul-level _rotationCursors in main.js als authoritative Quelle (einmal aus config geseedet), config.rotationCursors als Restart-Survival-Backing (das 30-Tage-Quota überlebt App-Neustarts). Picker bekommt { indices }-Seed + exponiert indices()/dirty(). Persist nur wenn dirty().
Regel: Rotations-/Round-Robin-/Cursor-State, der über mehrere Aufruf-Eintrittspunkte fair verteilen soll, MUSS die Aufrufe überdauern — und wenn das Ziel ein Zeitfenster über Sessions hinweg ist (Quota), auch Restarts. Picker/Iterator pro Call neu instanziieren ist nur korrekt, wenn jeder Call die GESAMTE zu verteilende Menge sieht.
Wie anwenden: Bei jeder "verteile reihum"-Funktion fragen: über wie viele Eintrittspunkte/Calls läuft die zu verteilende Menge real? (Hier: drag-drop=1 Call ABER folder-monitor=1 Call pro Datei.) Tests MÜSSEN den Cross-Call/Drip-Feed-Pfad mit frisch geseedetem State nachstellen, nicht nur einen langlebigen Picker.
2026-06-19 — Transiente Infra-Fehler (5xx/ECONNRESET) NIE als Account-Fehler klassifizieren
Symptom: User: "byse bissl unstabiler als sonst", Queue voll "Upload-Antwort von byse.sx war kein JSON (HTTP 502): <!doctype html>" und "read ECONNRESET". EIN byse-Gateway-Aussetzer kaskadierte durch ALLE Failover-Accounts (Primär→Fallback #3, jeder kriegt denselben 502) und blacklistete sie für den Batch.
Root cause: Die Upload-POST-Throw-Sites (hosters.js) warfen PLAIN Errors OHNE Klassifikations-Flag. Ein 502 matchte keinen der Classifier (_isTransientNetworkError hatte nur errno-Regexes ENOTFOUND/ECONNRESET/..., KEIN HTTP-5xx-Pattern; _shouldSkipRetryOnAccountError nur /\b(401|403|429)\b/, kein 5xx) → GENERISCH → while(task.accountId) → mark-failed + emit('account-failed') → Failover-Kaskade + Blacklist. (Der Server-Lookup-Pfad war schon geschützt via hosterTransient — nur der POST-Pfad nicht.)
Wichtig — Diagnose-Disziplin: "Hat byse die API geändert?" → NEIN, per Live-Probe verifiziert (GET /upload/server liefert dokumentiertes JSON; Upload-Contract unverändert). 502/HTML + ECONNRESET = Gateway-Infra, kein Contract-Bruch. Bei "manchmal kaputt" + offizielle API: Fehler-BODY anschauen (502-HTML ≠ API-Änderung) statt raten.
Fix: Expliziter err.transientNetwork-Flag am Throw-Site gesetzt wenn statusCode >= 500 (bzw. payload.status===500); 401/403/429 bleiben PLAIN (Account-Fehler). Flag wird in _isTransientNetworkError ZUERST geprüft (vor dem !err.message-Guard) und macht _isFileRejectedError/_shouldSkipRetryOnAccountError autoritativ false (gegen Keyword-False-Positives im HTML-Snippet). transientNetwork (NICHT hosterTransient) = retry SAME account, dann clean fail vor der Rotation-Schleife → keine Kaskade, kein Blacklist. Server-Lookup-Pfad symmetrisch gehärtet (apiGet taggt 5xx, getUploadServer reicht den Flag auf den gewrappten Error durch).
Regel: Hoster-Infra-Fehler (HTTP 5xx/Bad Gateway/Service Unavailable, Connection-Reset/Timeout) sind NIE Account-Faults — alle Accounts treffen dasselbe Gateway, Failover ist sinnlos, Blacklist schädlich. Fail open: retry same account, dann clean fail. Klassifikation per explizitem Flag am Throw-Site (autoritativ), Message-Regex nur als Defensive-Fallback. Throw-Site-Tagging IMMER end-to-end testen (uploadFile mit gestubbtem 5xx-Response treiben), nicht nur den Flag im Mock injizieren.
Nebenbefund: byse-Recovery-Poll war tot — _fetchByseFileList baute api.byse.sx/api/file/list (das /api/-Präfix gilt NUR für doodstreams doodapi.co-Host; byse hat schon die api.-Subdomain). Live: /api/file/list→302 auf Docs, /file/list→200 JSON. Copy-paste vom doodstream-Pattern. Fix: Präfix raus.
2026-06-19 — DNS-Auflösung + Cert ≠ legitime Domain (Parking-Falle); kaputte Hoster-Daten nicht raten, sondern verwerfen
Symptom: byse-Uploads scheiterten an getaddrinfo ENOTFOUND s1065.filemoon — der von byses /upload/server gelieferte Host hatte KEINE TLD (sNNNN.filemoon), unauflösbar. Intermittierend (manche Uploads klappten = vollständiger Host).
Beinahe-Fehler: Ich wollte fast .art anhängen — s1065.filemoon.art löste auf UND hatte ein dediziertes *.filemoon.art-Cert. ABER: PTR der IP = k8s-svc-lander-...parklogic.net = DOMAIN-PARKING. filemoon.nl (auch auflösend) = Shared-Catch-all-Cert mit Müll-SAN (kaobei.cc/babesex.xyz). BEIDE geparkt/gesquattet, nicht byses echte Server. Uploads dahin = auf Parking-Page oder zu Fremden.
Regel: „Löst auf + hat HTTPS-Cert" beweist NICHT, dass eine Domain der echte Service ist. Vor dem Behandeln einer Domain als legitim: PTR/Reverse-DNS (parklogic/lander/sedoparking = geparkt) UND Cert-SAN (riesige unzusammenhängende SAN-Liste = Shared-Parking-Cert) prüfen. Bei Hoster-Migrationen besetzen Squatter alte/neue Marken-TLDs.
Regel (kaputte Hoster-Daten): Wenn ein Hoster strukturell kaputte Daten liefert (Host ohne TLD), den korrekten Wert NICHT raten/halluzinieren (würde der User-Regel „niemals halluzinieren, immer verifizieren" widersprechen). Stattdessen das offensichtlich Kaputte als ungültig VERWERFEN und die bestehende Retry-/Cache-Maschinerie nach einem gültigen Wert fragen lassen (hier: normalizeAbsoluteUrl returnt null bei /(^|\.)filemoon$/ → getUploadServer retried + LAST_UPLOAD_SERVERS-Cache). Fail-open bleibt: alles kaputt → clean fail, kein Cascade.
2026-06-19 — Dedup-/Skip-Key ändern: ALLE Re-Queue-Pfade prüfen (mutate-in-place vs. preview-rebuild)
Kontext: Der _completedUploadKeys-Guard gated buildQueuePreview() — ein Key dort heißt "erzeuge KEINE Preview für file|hoster". Beim Fix für die Ghost-Wiederkehr (Key über Auto-Remove behalten + über Restart persistieren) bestand die echte Gefahr, dass ein persistierter Key einen absichtlichen Re-Upload still verschluckt.
Beinahe-Fehler (Advisor gefangen): Ich hatte den Clear nur im Modal-Add-Pfad (applyHosterSelection, pendingPaths.size>0). Frage: hängt IRGENDEIN Re-Upload-/Retry-Pfad an buildQueuePreview? Wenn ja → persistierter Key = stiller No-Op nach Restart, für den User unsichtbar bis er drauf stößt (er kann nicht testen).
Verifikation: retrySelectedJobs (reuploadBtn) und _retryFailedFromBuckets (Retry-Failed) mutieren BEIDE den bestehenden Job in-place (j.status='queued'/'preview') und starten direkt — sie konsultieren _completedUploadKeys NIE. Also sicher. Nur das Hinzufügen einer frischen Datei läuft über buildQueuePreview, und genau dort clears applyHosterSelection.
Regel: Bevor du einen Key/Flag änderst, der eine Rebuild-Funktion gated, JEDEN Pfad lesen, der einen erledigten Job zurück in den Upload bringt. Mutate-in-place-Pfade sind immun; nur Rebuild-via-Preview-Pfade brauchen einen Key-Clear. Nicht auf Symptome schließen — die Handler lesen.
Wie anwenden: Skip-Guard-Change → grep alle Caller der gegateten Funktion + alle „erneut/retry/reupload"-Handler; pro Handler entscheiden: mutiert er Jobs oder baut er neu? Persistierte Skip-Keys NUR bei explizitem User-Re-Add clearen, nicht bei In-Place-Retry.
2026-06-19 — „Alle Re-Add-Pfade" heißt WIRKLICH alle: es gab DREI, nicht zwei (v3.3.83)
Symptom: Nach v3.3.82 („retry mutiert in-place, ist immun") fand ein 2. Hunt: retrySelectedJobs von einem DONE-Job re-added den Pfad zu selectedFiles UND behält den completed-Key → nach Restart blockt buildQueuePreview die Wiederherstellung → absichtlicher Re-Upload still verloren. Plus: der Folder-Monitor-Pre-Selected-Branch ist ein VIERTER Add-Pfad, der applyHosterSelection komplett umgeht → Key-Clear lief nie.
Root cause meiner Fehlannahme: In v3.3.82 hatte ich „retry mutiert in-place → immun" geschlossen — korrekt für den GHOST (Doppel-Preview), aber NICHT für den retry-of-DONE-Fall, wo der Job einen LIVE completed-Key trägt (Key wird nur auf 'done' gesetzt). Ich hatte 2 von 4 Re-Add-Pfaden gefunden (applyHosterSelection, manueller Delete), die anderen 2 (retry, folder-monitor) übersehen.
Regel: Wenn ein Dedup-/Skip-Key existiert, exhaustiv ALLE Stellen enumerieren, die (a) den Key SETZEN und (b) einen Pfad zurück in selectedFiles bringen. Pro Re-Add-Pfad einzeln prüfen, ob er den Key clear. „In-place vs rebuild" ist NICHT die einzige Achse — auch ein in-place-Reset kann einen stale persistierten Key hinterlassen, der ERST nach Restart beißt.
Wie anwenden: grep den Key-Namen → jede .add-Stelle und jede selectedFiles.push/selectedFiles =-Stelle auflisten → Kreuztabelle Add-Pfad × cleart-Key. Lücke = Bug. Clear-Logik in EINEN Helper (clearDedupKeysForPaths) ziehen und an JEDEM Re-Add-Pfad aufrufen, statt pro Pfad zu duplizieren (sonst wird der nächste Pfad wieder vergessen).
2026-06-19 — Per-Cell-Delete braucht eine eigene persistierte Suppression, nicht Überladung des completed-Keys (v3.3.83)
Symptom: Multi-Hoster-Datei F (Zeilen F|A, F|B); User löscht nur F|A. buildQueuePreview baut F|A beim nächsten Rebuild (neue Datei adden / Folder-Drop) wieder auf → F wird doch zu A hochgeladen (Quota verbrannt, ungewollter Link). _deletedJobIds half nicht (alte Job-ID, neue Preview-ID).
Designentscheidung: Separates _suppressedPreviewKeys-Set statt _completedUploadKeys zu überladen — completed-Key hat Re-Upload-nach-Delete-Semantik (wird beim Delete GECLEART), suppression hat die GEGENTEILIGE (wird beim Delete GESETZT). Überladen hätte „re-upload nach delete" gebrochen.
Korrektheits-Invarianten (Advisor-verifiziert): (1) Suppression NUR setzen wenn die Datei nach syncSelectedFilesFromQueue noch in selectedFiles ist (sonst sinnlos). (2) status==='done' AUSSCHLIESSEN (done hat eigene completed-Key-Semantik; nicht ungefragt das re-upload-after-delete-Feature ändern). (3) Re-Add-Clear ist PFAD-basiert (alle Hoster), retry-Clear ist KEY-basiert (exakter file|hoster) — pfad-basiertes Clear bei retry würde den Ghost eines Geschwister-Hosters auferstehen lassen. (4) Persistenz filtert auf selectedFileMap (wie completedKeys) → überlebt Restart solange Geschwister-Job existiert, wird inert wenn Datei selectedFiles verlässt.
Verifikations-Ehrlichkeit: Renderer-Code ist nicht node:test-bar. Im Report „verifiziert per Logik + Regressions-Suite (362 grün) + identischer Smoke-Boot", NICHT „getestet" schreiben. Triviale Helper NICHT nur fürs Testen in ein lib extrahieren (schlechtes Risk/Reward).
2026-06-19 — Ein offener Flag ist KEINE Zustimmung: erzwinge die Entscheidung oder parke sie (Single-Instance-Lock)
Symptom: Single-Instance-Lock (G) über 3 Turns hinweg in Prosa „dem User vorgelegt"; User hat 0× darauf reagiert (stattdessen Hunt re-run). Drohte ein 4. Mal als vage Sorge im Summary aufzutauchen. Regel: Re-Bestätigung dass ein Bug ECHT ist ≠ Zustimmung zu einer Verhaltensänderung. Die offene Frage bei user-facing Changes ist nicht „ist es real" sondern „will der User dieses Verhalten" (hier: startet er je absichtlich 2 Instanzen?). Eine vierte Prosa-Erwähnung ist „ambient worry", keine Entscheidung. Wie anwenden: Nach dem Release EINE AskUserQuestion feuern (user-facing Change + ggf. weitere geparkte Punkte als Multi-Select gebündelt) ODER in EINER Zeile sagen „geparkt bis dein Wort" und aufhören es zu wiederholen. Nie denselben user-facing Flag 3+ Mal als Sorge raisen.
2026-06-19 — Security-E2E muss JEDEN Collector-Default-Pfad treiben, nicht nur den Aggregat-Hub (Remote-Diagnostics)
Kontext: Read-only Remote-Diagnose gebaut (Agent im App-Prozess + lokales MCP-Gateway, connect-by-code). Redaktion ist die EINZIGE Garantie, dass kein Secret die Box verlässt. Drei Scrub-Ebenen: (1) sanitizeConfig redactet CRED_KEY-gekeyte Felder strukturell, (2) valueScrub ersetzt bekannte Config-Secret-WERTE per split/join, (3) redactLogText pattern-scrubt Secret-SHAPES (Bearer/token=/cookie:/discord-webhook/?key=) in Freitext.
Bug 1 (E2E-Gate gefangen): Ein Hoster-zurückgegebenes token=<opaque> in einem History-Error-String überlebte — es ist KEIN gespeichertes Config-Credential, also greift value-scrub nicht, und das line-48-Pattern kannte nur access_token, nicht bare token. Fix: token-Familie (token/auth_token/refresh_token/session_token + standalone Bearer <opaque>) ins Pattern.
Bug 2 (Advisor gefangen, NACH grünem E2E): get_config_redacted section:'all' und get_queue_state {includeJobs:true} (DEFAULT-Pfad!) liefen nur durch value-scrub, nie pattern-scrub → derselbe opaque-Token-Leak. redactLogText direkt über kompaktes JSON laufen zu lassen geht NICHT (die cookie:/[^\n]*-Patterns fressen über Feldgrenzen → JSON kaputt). Fix: _deepRedact = per-String-Leaf-Walk, der redactLogText auf jedes Leaf einzeln anwendet (JSON-safe, da pro Leaf begrenzt). history aus get_config gedroppt (hat eigenen Collector).
Warum der erste E2E es verfehlte (der Diskriminator): server_health ruft getQueueState({includeJobs:false}) → kein Job-Error je serialisiert; UND das Fixture nutzte apiKey=<SECRET> als Queue-Error → value-scrub fing es eh, auch ohne pattern-scrub. ZWEI Zufälle versteckten den Leak. Der direkte get_queue_state{includeJobs:true}-Pfad mit NICHT-Config-Token war nie getrieben.
Regel: Bei einem Redaktions-Gate (a) jeden Collector EINZELN mit seinen DEFAULT-Args treiben, nicht nur den Aggregat-Hub, der bequeme Flags setzt; (b) Fixtures MÜSSEN ein NICHT-Config-Secret enthalten (opaque Token, der nur als Shape erkennbar ist), sonst testet man value-scrub und glaubt, pattern-scrub zu testen; (c) ein grüner E2E heißt nicht „dicht" — Advisor/Review über jeden Pfad laufen lassen, der einen Freitext-Fehlerstring serialisiert.
Wie anwenden: Denylist-Redaktion kann einen bare opaque String OHNE key=/Bearer/URL-Kontext NICHT scrubben — das ist inhärent, kein Defekt. Commit-/Release-Notes ehrlich halten („common secret shapes pattern-scrubbed", nicht „kein Secret verlässt je die Box"). Default-Bind 127.0.0.1 erzwingen (_safeDiagBindAddress), 0.0.0.0-Option erst wenn wss/TLS existiert — nie plaintext ws:// auf allen Interfaces.
2026-06-19 — Ein Diagnose-Tool darf NIE den Prozess einfrieren, den es diagnostiziert (v3.3.85)
Kontext: Nach Release v3.3.84 das Diagnose-System "intensiv durchtesten". Drei Werkzeuge gebaut: (1) Live-Integration-Harness, das den ECHTEN Gateway-MCP-Prozess (StdioClientTransport) gegen einen echten in-process Agent fährt und ALLE 14 Tools durchprüft; (2) adversariale Redaktions-/Abuse-Probe; (3) unabhängiger code-reviewer-Audit-Subagent. Parallel laufen lassen.
Bug 1 (Probe + Audit, REAL DoS): read_log kompilierte den vom Client gelieferten grep zu new RegExp(grep,'i') und lief synchron über bis zu 1 MB Log-Tail — IM Electron-Main-Prozess. (a+)+$ gegen eine lange Zeile = katastrophisches Backtracking → ganze App friert ein (empirisch: 8s-Timeout, gekillt). JS-Regex ist synchron und nicht abbrechbar → der einzige sichere Fix ist KEINE User-Regex: grep ist jetzt case-insensitive Substring-Filter mit |-Alternation. Provably linear.
Bug 2 (Probe, Whitelist-Integrität): Die Op-Tabelle war ein Plain-Object-Literal → OPS['constructor']/['toString']/['valueOf'] lösen geerbte Object.prototype-Funktionen auf, bestehen typeof fn==='function' und liefern {ok:true}. Harmlos (kein Secret/Write), aber Whitelist-Loch. Fix: typeof op==='string' && Object.prototype.hasOwnProperty.call(OPS,op).
Bug 3 (nur Live-Integration sichtbar): Der Gateway las die App-Version aus info.data.version, der Collector liefert sie aber als info.data.app.version — der "connected to vX.Y.Z"-Hinweis war still leer. Unit-Tests mit Stubs fingen das NIE; nur das Fahren des echten Gateway-Prozesses gegen den echten Collector deckte die Shape-Diskrepanz auf.
Regeln: (1) Ein read-only Diagnose-Agent, der IM Zielprozess läuft, darf keine vom Client kontrollierte Synchron-Operation mit unbegrenztem Aufwand ausführen (Regex, JSON.parse von Riesen-Payloads, etc.) — sonst DoS der diagnostizierten App. Literal-Match statt Regex; alles clampen. (2) Whitelist NIE als Plain-Object mit obj[key]-Lookup — Prototype-Member lecken; Set/Map/null-proto/hasOwnProperty. (3) Eine Live-Integration gegen den ECHTEN Out-of-Process-Consumer findet Shape-/Contract-Mismatches, die Mock-Unit-Tests strukturell nicht sehen — bei jedem Protokoll-Grenzübergang (MCP-Tool↔Collector) mindestens EINEN echten End-to-End-Lauf. (4) Ein grüner Custom-E2E ist kein Freibrief: der Advisor/Audit fand den Queue-Leak NACH grünem E2E, weil das Gate bequeme Flags (includeJobs:false) setzte — jeden Collector mit DEFAULT-Args fahren.
2026-06-19 — "Mach es wie " = das Projekt FINDEN und EXAKT mappen, nicht raten (Tailscale/Allowlist, v3.3.86)
Kontext: User: "nutzen wir dasselbe wie der downloader mit tailscale was mcp betrifft" + "ka, das was der downloader nutzt, mach dasselbe". Der User kannte die Details NICHT — er wollte 1:1-Replikation eines Schwester-Projekts.
Vorgehen das funktioniert hat: (1) Sibling-Projekte gelistet, per Grep nach tailscale/100.64/ts.net gesucht → Real-Debrid-Downloader/tools/rd-diagnostics-mcp gefunden (eine fast identische MCP-Ferndiagnose existierte schon). (2) Einen Explore-Subagenten eine PRÄZISE Implementierungs-Map mit file:line + Code-Excerpts erstellen lassen (bind modes, fail-closed allowlist, code-format, bridge, UI, IPC). (3) Das Muster EXAKT repliziert statt zu raten.
Was der Downloader anders macht (und warum es Tailscale ermöglicht): Host steckt IM Code ({v,h,p,t,n,fp?,s?}), nicht extern. Zwei Bind-Modi: lokal (127.0.0.1) ODER Netzwerk (0.0.0.0) — letzteres NUR mit nicht-leerer fail-closed IP-Allowlist: leere Allowlist = nur Loopback; geprüft am ECHTEN socket.remoteAddress (NIE forwarded-Header), ::ffff:-normalisiert, CIDR-Matching. Tailscale wird NICHT autodetektiert — es ist nur eine der os.networkInterfaces()-IPs, erreicht über den Tunnel; Allowlist (auf den Tailnet, z.B. 100.64.0.0/10) + Token sind das Gate, WireGuard ist die Verschlüsselung.
Regel: Bei "mach es wie X": X lokalisieren (grep), die security-kritischen Teile mit einem Subagenten verbatim mappen, dann replizieren. Eine fail-closed Allowlist (Loopback immer erlaubt, leer=loopback-only, real peer IP) ist das richtige Modell für netzwerk-erreichbare read-only Diagnose über einen vertrauten Tunnel — plaintext-Transport ist ok, WENN der Tunnel (Tailscale/WireGuard) verschlüsselt UND die Allowlist+Token den Zugriff gaten. Den HAPPY-Path (allowlisted non-loopback peer über echten 0.0.0.0-Socket) auch LIVE testen, nicht nur per Komposition aus Unit+Wiring.
Prozess-Stolperstein: Test NACH dem Feature-Commit hinzugefügt → release_gitea.mjs brach ab ("uncommitted tracked changes"). Vor jedem Release: git status --porcelain | grep -v '^??' muss leer sein. Tracked-aber-uncommitted (auch ein nachgereichter Test) blockt den Build.
2026-06-21 — "Gefühlt laggy nach Zeit, CPU/RAM normal" = ERST messen, dann in echtem Blink profilen (v3.3.87)
Kontext: User: Programm fühlt sich nach langer Laufzeit mit vielen Uploads zäh an, CPU ~40%/8 Kerne, RAM 6/32 GB — beide normal/stabil. Erste Hypothese (Hauptprozess-Config-I/O skaliert mit wachsender History) war für DIESEN User FALSCH.
Was es wirklich war (gemessen + profiliert): renderRecentUploadsPanel hatte einen Append-only-Fastpath, gegated auf rows.length > _recentLastRenderedLen. maybeAddSessionFile capped per push-then-slice (2000→2001→zurück auf 2000). Ab dem Cap ist rows.length auf 2000 fixiert → Gate für IMMER false → JEDE Completion fiel in den Full-innerHTML-Rebuild von 2000 Zeilen. Blink-Messung (Playwright, table-layout:fixed, gleiche Engine wie Electron): ~80 ms pro Completion → wiederkehrender 80-ms-Freeze. Fix (append-evict, Gate auf pendingAppends>0, Overflow vom DOM-Boden evicten): 80 ms → 7,4 ms (>10×), DOM bleibt exakt == Daten (Cap/Reihenfolge/keine Dupes), über 5000 Completions verifiziert.
Regel 1 — Magnituden NICHT raten, LESEN: „wächst über Zeit" ist eine Annahme über GRÖSSE. Die echte electron-config.json war 52 KB (History 23 Zeilen) — ein einziger node-Read killte die ganze Config-I/O-Theorie. Bevor man eine „skaliert-mit-X"-Ursache fixt: X am echten Artefakt messen (Dateigröße, Array-Länge, Job-Count im persistierten State).
Regel 2 — Im ECHTEN Renderer-Engine profilen, nicht analytisch raten: jsdom rendert kein Blink-Layout. Playwright (Chromium = Electron-Blink) mit performance.now() um (a) Rebuild und (b) erzwungenes Relayout nach Style-Write liefert die Zahl, die entscheidet: 3 ms = unsichtbar, 80 ms = DIE Ursache. Dieselbe Messung ist Fix-Auswahl UND Vorher/Nachher-Verifikation (das Goal verlangt „verifiziere dass behoben" — ein grüner Test beweist Korrektheit, NICHT dass der Lag weg ist).
Regel 3 — Multi-Agent-Findings gegen primäre Evidenz prüfen (Control-Char-Falschpositiv): Der Hunt meldete HIGH-ish einen „_sessionFileKeys delete-key separator mismatch". Beim Versuch ihn zu fixen matchte der Edit-old_string NICHT. Char-Code-Dump (HAS_U0001: True) zeigte: die Zeile hat ECHTE U+0001-Zeichen — die Read-Tools der Verifier-Agenten rendern Steuerzeichen unsichtbar, sie schlossen fälschlich „keine Separatoren". KEIN Bug. Wenn ein Fix-old_string nicht matcht obwohl Grep ihn zeigt: Char-Codes dumpen, bevor man dem Tool misstraut — die Quelle kann unsichtbar von der Read-Anzeige abweichen.
Regel 4 — Den negligible-aber-realen Befund mit Zahl ABLEHNEN, nicht aus dem Bauch: queueJobs O(N)-Scan pro Render (wächst unbounded, da removeFromQueueOnDone=false UND Folder-Monitor EINEN Batch via addJobs am Leben hält → 500-Cap-Prune feuert nie) — real, aber Blink-gemessen <0,1 ms bei 3000 Jobs. Den riskanten Inkremental-Counter-Refactor mit DIESER Zahl skippen, nicht mit „fühlt sich klein an".
Wie anwenden: Append-only-Optimierungen, die auf Längenwachstum gaten, brechen still an JEDEM Cap (push-then-slice fixiert die Länge) — stattdessen die Anzahl NEUER Items zählen und am Boden evicten. „Mach es wie die Queue-Tabelle (virtualisieren)" war hier NICHT nötig: die Messung zeigte stehende 2000 Zeilen kosten median 0,4 ms; nur der Rebuild war teuer. Simplest-Fix der die gemessene Ursache trifft schlägt die größere Architektur-Änderung.
2026-06-21 — "Audit JEDE zeile" = audit + measure + risk-appropriate DEFER, nicht fix-everything (v3.3.88)
Kontext: Nach dem v3.3.87-Lag-Fix Folge-Goal: „schau dir wirklich JEDE zeile an die du geschrieben hast und schau ob es solche probleme gibt o. geben könnte". 18-Agenten-Audit + Eigen-Review jeder Hot-Path-Zeile + Blink-Benchmarks.
Befund: Der Audit fand, dass MEIN eigener T1-Fix (config-store cache, 29d1944) eine latente Regression einführte: load() macht ein unconditionales structuredClone der GANZEN config (inkl. unbounded history) pro Call → write-interleaved loads 2,2–2,4× LANGSAMER als das alte read+parse (gemessen @8000 Batches: 9,65 ms → 22,96 ms). Skaliert mit historySize. ABER: der echte User hat 8 Batches / 4,8 KB → Mikrosekunden. Negligible.
Die Falle (Advisor hat geblockt): Ich wollte es „elegant" fixen mit history.slice() (shallow) statt deep-clone. Advisor: STOPP. load() ist der gefährlichste Code im Repo (config + credentials; Korruption = Datenverlust), ich war hier schon mal von Cache-Semantik gebissen worden. Und: der Perf-Win und das Risiko sind DIESELBE Münze — der Speedup kommt NUR vom Sharing der Batch-Objekte by-reference, und genau dieses Sharing IST die Silent-Cache-Corruption-Gefahr (hängt an einem globalen Invariant „nichts deep-mutated je eine history-Batch" den ich über zukünftigen Code + jeden getHistory-Consumer nicht erzwingen kann). Es gibt KEINE sichere Version dieses Ansatzes → falsches Werkzeug für safety-kritischen Code. Hardcoded 5 keys in _cloneConfig wäre ein zweiter Footgun (zukünftiger top-level key verschwindet still aus jedem load()).
Regel: „Audit jede Zeile" heißt JEDE Zeile ANSCHAUEN + die Magnitude MESSEN + eine risiko-angemessene Entscheidung treffen — NICHT jeden geflaggten Befund fixen. Bei einem Audit-Goal ist „ich habe jede Zeile geprüft, jeden Befund als sub-ms bei realistischer History gemessen, den Mechanismus bestätigt aber den Fix als riskante Persistenz-Chirurgie für einen latenten Mikro-Cost eingestuft, also dokumentiere ich ihn statt ihn zu shippen" die VOLLSTÄNDIGE, gründliche Antwort. Jeden geflaggten Punkt unabhängig vom Risiko zu fixen ist keine Gründlichkeit — so wird aus einer Lag-Fix-Session ein Datenverlust-Incident. Nur den EINEN Befund shippen der im echten Szenario beißt (doodstream _debugLog: sync statSync+appendFileSync ~8–15×/Upload auf dem Main-Loop während des Uploads → hinter logVerbose gaten, default off, near-zero risk). Den Rest als bewusste Defers mit Messzahlen dokumentieren.
Wie anwenden: Wenn ein Goal („JEDE!! JEDE!!!") + ein Stop-Hook Druck erzeugen, immer weiterzuschneiden: das ist genau der Moment, den Advisor VOR dem Edit zu rufen. Magnitude am ECHTEN Artefakt prüfen (der User-Config, nicht @8000-Batches-Hypothese). Persistenz-/Credential-Code nur anfassen wenn der Fix risiko-frei UND der Gewinn real-spürbar ist — sonst dokumentieren und stoppen.
2026-06-21 — "Nicht-Persistenz also sicher" ist ein Trugschluss: der Redaktions-Layer ist GENAUSO gefährlich (v3.3.89)
Kontext: 3. identische /goal-Re-Fire („JEDE zeile, alles drum-und-dran"). Diesmal die un-auditierte Remote-Diagnostics-Code (v3.3.84/85) zeilenweise auditiert (52 Agenten). 14 von 15 actionable Findings konvergierten auf EINEN Cold-Path-Freeze: server_health macht O(historySize) sync-Arbeit pro Request (~6–7 config-clones + unbounded history-walks; limit slict nur den Output). Gemessen 258 ms–6,7 s bei großer History → friert die App ein, die es diagnostiziert (verletzt die v3.3.85-Regel).
Der Trugschluss (Advisor hat geblockt): Ich begründete „diesmal ist der Fix sicher, weil es Diagnostics-Collectors sind, KEIN Credential-Persistenz-Code wie letzte Runde". FALSCH. lib/diagnostics-collectors.js IST die Credential-Oberfläche — es ist der Redaktions-Code (_secrets/_deepRedact/collectSecretValues/sanitizeConfig/redactLogText). Genau dieser Code ist schon ZWEIMAL geleakt (7b5420e „one collector still leaked", 8d757a9 „redaction gaps") — bei grünem E2E. Der „elegante" Fix (config+secrets einmal snapshoten und durch die Collectors threaden) ist EXAKT die gefährliche Form: ein Pfad verpasst / ein stale secrets-array → SECRET LEAK, ein schlimmeres Versagen als der Cold-Freeze. Dieselbe Kategorie-Fehler wie letzte Runde (damals Datenverlust an config-store, jetzt Secret-Leak an der Redaktion), nur andere Datei. „Nicht Persistenz" hat mich getäuscht.
Regel: Die Frage ist nicht „ist es Persistenz?", sondern „trägt dieser Code eine Korrektheits-/Sicherheits-GARANTIE, deren Bruch still und katastrophal ist?" — Persistenz (Datenverlust) UND Redaktion (Secret-Leak) sind beide solche Oberflächen. Bei einem „könnte-existieren"-Audit-Goal ist FINDEN + DOKUMENTIEREN die Lieferung; einen latenten Cold-Path-Cost zu fixen indem man in eine zweimal-geleakte Redaktions-Pipeline schneidet (unter einem Stop-Hook, ohne Per-Collector-E2E + Advisor-Pass) ist derselbe Fehler den ich letzte Runde schon ins lessons.md geschrieben hatte. KONSISTENT anwenden. Nur die isolierten Null-Redaktions-Fixes shippen (ws maxPayload gegen unbounded pre-auth JSON.parse; sendToClient readyState+try-guard gegen uncaughtException). Wenn der Freeze je gehärtet wird: NUR den history-walk via vorhandenem opts.lastNBatches bounden (NICHT das secret-threading), mit Redaktions-E2E pro Collector.
Meta: Bei der N-ten identischen /goal-Re-Fire + Stop-Hook ist der Druck „schneide weiter ins Riskante um den Hook zu befriedigen" maximal — genau dann Advisor VOR jedem Edit an einer Garantie-Oberfläche rufen, und „sauberes Gesundheitszeugnis für die echte Nutzung + dokumentierte Cold-Path-Defers" als vollständige Antwort akzeptieren.
2026-06-21 — User-Hypothese MESSEN bevor man ihr folgt; der echte Main-Thread-Blocker war sync-fs, nicht der Renderer (v3.3.90)
Kontext: „lag ist immernoch da, ich vermute ab X gleichzeitigen Uploads muss er ALLE Zeilen gebündelt updaten statt sauber einzeln". 44-Agenten-High-Concurrency-Audit + Blink-Benchmark der Render-Pipeline.
Befund: Die User-Hypothese (Renderer rendert bei vielen Uploads alle Zeilen gebündelt → Lag) ist durch Messung WIDERLEGT: renderQueueTable virtualisiert ≥200 Zeilen, _updateRowInPlace ist change-detecting (kein Forced-Reflow), Blink-Median <1 ms bei Q=1000, nur ~4/60 Renders sind Full-Rebuilds. Der Renderer ist NICHT der Flaschenhals. Der ECHTE Blocker: lib/clouddrop-upload.js _uploadChunked las jeden 16-MB-Chunk mit fs.readSync SYNCHRON auf dem Main-Event-Loop (einzigartig unter den 5 Uploadern — die anderen 4 streamen async). Bei jedem Read ~5–9 ms SSD / 30–100 ms langsame Platte friert der GANZE Main-Loop (alle Progress/IPC/Render/andere-Uploads). Skaliert mit der Zahl paralleler clouddrop-Uploads. Passt exakt auf „laggy beim Hochladen, schlimmer mit mehr gleichzeitig". User nutzt clouddrop.
Fix: fs.openSync/readSync/closeSync → fs.promises.open + await fh.read + await fh.close(). Byte-Äquivalenz mit Hash-Vergleich über alle Chunk-Grenzfälle verifiziert (volle/partielle/multi-Chunk/1-Byte) BEVOR geshipped — ein Chunk-Read-Bug = korrupter Upload, deshalb Pflicht-Verifikation, nicht „sieht richtig aus". Separater Fix: rotation-retry + suspect-alternate progressCb in upload-manager.js feuerten _emitProgress (sync emit + frischer Object-Spread) bei JEDEM Stream-Chunk (hunderte/s/Job) — der 250-ms-lastEmitTime-Gate des Primary-Path fehlte. Gate gespiegelt (activeEntry-Mutation bleibt ungated für Stats/Speed-Monitor, nur der emit ist gegated).
Regel: Wenn der User eine konkrete Mechanik vermutet („er updatet alle Zeilen gebündelt"), die Mechanik MESSEN bevor man sie fixt — nicht der Plausibilität folgen. Die Messung kann die Hypothese widerlegen UND den echten Verursacher woanders aufdecken (hier: nicht Renderer-DOM, sondern sync-fs im Upload-Datapfad). „Laggy bei moderater CPU" (40%/8 Kerne = ein Kern bei 100%) zeigt auf Main-Thread-Sättigung/sync-Blocking, NICHT auf DOM-Amplifikation. Bei Daten-Pfad-Fixes (Upload-Bytes) immer Byte-Äquivalenz beweisen, nicht nur Tests grün.
Discriminator nicht vergessen: Mit der echten User-Config (parallelCount 2×5 Hoster ≈10 gleichzeitig) sind „100 gleichzeitig" nur erreichbar wenn die Parallel-Counts hochgedreht wurden — sonst sind „100" die QUEUE-Größe, nicht concurrent. Nach dem Ship dem User die Unterscheidungsfrage stellen (Lag clouddrop-spezifisch? Parallel-Counts erhöht?), statt blind Sieg zu erklären — bei echter High-Concurrency bräuchte es ein Concurrency-Cap / Worker-Prozess, keinen Mikro-Fix.
2026-06-21 — Zwei lebende Hypothesen mit GEGENSÄTZLICHEN Fixes: instrumentieren statt per Elimination refactoren (v3.3.91)
Kontext: Discriminator beantwortet — Lag ist TRUE high-concurrency (User fährt 50+ gleichzeitig, Counts hochgedreht), nicht clouddrop. Ich wollte einen Mess-Workflow starten, um „inherent TLS → Worker" zu belegen.
Die Falle (Advisor hat geblockt): Der Workflow hätte die JS-Kosten (schon weitgehend als billig gemessen) nur RE-bestätigt und dann „TLS → Worker" per ELIMINATION geschlossen — derselbe Renderer-Rate-Fehler eine Ebene höher. Den credential-tragenden Upload-Core (throttle/rotation/abort/progress) auf Eliminations-Schluss umzubauen ist genau „measure-before-build" verletzt. ZWEI Hypothesen leben und brauchen GEGENSÄTZLICHE Fixes: (A) Main-Thread CPU-blockiert (TLS/crypto/sync) → Event-Loop stallt → Cap/Worker helfen; (B) Main-Thread fein aber IO-STARVED (libuv-Threadpool/Sockets) → Loop bleibt responsiv, Uploads stauen nur → Worker sind VERSCHWENDET, Config fixt es. Ich konnte im Sandbox die echte 50-fach-TLS-Last nicht messen → also hätte JEDER Sandbox-Bench die falsche Antwort per Elimination geliefert.
Regel: Wenn zwei Hypothesen gegensätzliche, teure/schwer-reversible Fixes implizieren UND du die entscheidende Größe im Sandbox nicht messen kannst — baue das MESSINSTRUMENT in die echte App, nicht den Fix. Hier: perf_hooks.monitorEventLoopDelay im Main-Prozess, geloggt während Uploads (reine Zahlen → keine Redaktions-Oberfläche). Hohe mean/p99 → CPU-blockiert → Worker gerechtfertigt; niedrige Delay während Uploads stauen → IO-bound → Worker verschwendet, Threadpool/Sockets ist der Hebel. Die Zahl entscheidet die ganze Architektur und blockiert nicht. Den Worker/Child-Process-Refactor NIE off-sandbox per Elimination shippen — erst die echte-App-Zahl + explizites User-OK (hart reversibel, fasst Credentials/Abort/Rotation an).
Billigster konkreter Verdächtiger zuerst (reversibel, kein Refactor): UV_THREADPOOL_SIZE Default 4 — alle Uploader speisen undici aus fs.createReadStream + DNS getaddrinfo durch denselben Pool → 50 concurrent vs 4 Threads = harter Cliff bei kleiner Connection-Zahl = exakt „ab X connections". Auf 64 (erste Zeile vor require('electron'), libuv liest beim Lazy-Init; Threads on-demand → 64-Max kostet nichts wenn ungenutzt). EINE Env-Var testet die Hypothese mit null Risiko. Windows-Eigenheit dabei gefunden: getaddrinfo wird vom Windows-DNS-Client-Service serialisiert → die DNS-Hälfte des Cliffs ist auf Windows maskiert (fs-Read-Hälfte profitiert trotzdem) — weiterer Grund, warum nur die echte-App-ELD-Zahl zählt, nicht der Sandbox-Bench.
2026-06-21 — Das REGIME erfragen bevor man misst/fixt; die Lag-Knoten war eine verschwendete Intl-Format pro Progress-Event (v3.3.93)
Kontext: User liefert echte Daten: „25 connections okay, 50+61 laggt, EVTL wenn die uploadenden Zeilen nicht im Bild sind." Ich wollte sofort meine „non-virtual reflow"-Theorie benchmarken/fixen.
Die Falle (Advisor hat geblockt — ZUM DRITTEN MAL die Regime-Falle): Meine Theorie ruhte auf zwei UNBESTÄTIGTEN Annahmen — (1) Queue <200 (non-virtual), (2) Dateiname-Sort. Beide für einen User mit 50-61 concurrent wahrscheinlich FALSCH. Nach M=10 (falsches Regime) und Renderer-cleared-dann-doch-nicht wäre das der dritte Regime-Fehler gewesen: „synthetischer Bench auf angenommenem Regime". Advisor: ERST die zwei Fakten vom User holen (Queue-Größe? Geklickte Sort-Spalte?) — sie entscheiden, OB die Theorie überhaupt gilt. Antwort: Queue 200-1000 (VIRTUELL → off-screen Zeilen NICHT im DOM → reflow-Theorie tot) + Sort nach Fortschritt/Speed (dynamisch). Das lenkte auf den PER-EVENT-Pfad statt den Render-Pfad.
Befund (gemessen, nicht geraten): maybeAddSessionFile(job) berechnete formatDateTime(new Date()) UNBEDINGT ganz oben — VOR dem status==='done'-Check, der für alles andere früh returnt. formatDateTime macht ZWEI Intl-Locale-Formate (toLocaleDateString+toLocaleTimeString) = ~83µs/Call gemessen. Läuft bei JEDEM Progress-Event (onUploadProgressBatch loopt den M-Item-Batch → handleProgress → maybeAddSessionFile) = 10×M/s, und WIRFT es weg für alle nicht-done-Events. Skaliert exakt mit M (250/s @25 → 610/s @61) und feuert in BURSTS: jeder Batch = M Calls back-to-back = synchroner Main-Thread-Block ~2,4ms@25 → ~5ms@61 alle 100ms → sprengt das 16ms-Frame-Budget → Scroll-Stutter. Per-Event, NICHT per-Render → scroll-unabhängig → erklärt „Lag wenn aktive Zeilen off-screen" exakt. DAS war der 25→50-Cliff.
Fix: const dt = formatDateTime(new Date()) in den if (!_sessionFileKeys.has(dedupKey))-Block verschoben → läuft 1× pro echt-neuem fertigen Upload statt pro Progress-Tick. Faithful Blink-Bench am BESTÄTIGTEN Regime (Q=500 virtuell, Progress-Sort, Scrolling, M=25/50/61, OLD vs FIXED): Per-Batch 1,7/3,2/4,1ms (OLD, M-skalierend) → 0/0/0ms (FIXED, flach). Frame-P95 7,3→4,2ms @61. Render/Scroll-Pfad selbst flach ~2,5ms über alle M → KEIN zweiter Knoten dort.
Regel: Bei perzeptuellem Lag IMMER zuerst das REGIME erfragen (Datenmenge, aktive Konfiguration wie Sort-Spalte), bevor man benchmarkt oder fixt — eine plausible Mechanik für das FALSCHE Regime zu messen ist exakt der M=10-Fehler. Wenn der User eine konkrete Beobachtung liefert („wenn off-screen"), ist scroll-UNABHÄNGIG (per-event) vs scroll-abhängig (per-render) der Schlüssel-Diskriminator. Und: verschwendete Arbeit auf dem heißesten Pfad (Intl/new Date/Regex/DOM-Query UNBEDINGT berechnet, dann verworfen) ist ein klassischer M-skalierender Lag-Knoten — formatDateTime immer hinter den Guard schieben der das Ergebnis tatsächlich nutzt. Prozess-Grenze beachten: Renderer-Jank ≠ Main-Prozess; das Main-ELD-Log sieht Renderer-Lag NICHT.