From 85c8d6bdf9c87309526f38ae0a114c269a600ed5 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Wed, 17 Jun 2026 12:51:29 +0200 Subject: [PATCH] Fix: Mega-Cooldown vor Tagesreset-Park bevorzugen + Token-Truncation + Rewind-Fail-Haertung (Synthese-Pass) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Capstone-Synthese/Regressions-Pass ueber den kumulativen Audit-Diff (v1.7.212..HEAD) hat 3 Fix-INTERAKTIONEN gefunden — der Rest komponiert sauber. C1 (MED): Sind BEIDE Mega-Modi aktiv (API + Web), aggregiert die Provider-Kette bei Fehlschlag beide Token in einen String: `mega_debrid_reset_park:LONG` (API bis Tagesreset geparkt) + `mega_debrid_cooldown:30000` (Web nur kurz im Cooldown). Der Manager pruefte reset_park VOR cooldown → das Item wurde ~24h geparkt, obwohl der Web-Modus in ~30s wieder verfuegbar gewesen waere. Fix: den Cooldown-Zweig (kuerzerer Delay) VOR dem reset_park-Zweig pruefen; der erholbarere Pfad gewinnt. C2 (LOW): Das reset_park-Token konnte von compactErrorText (220-Zeichen-Cap) abgeschnitten werden, wenn Mega nicht der Lead-Provider war und ein vorheriger Provider eine lange Fehlermeldung lieferte → Park wurde still uebersprungen und Mega am Tageslimit weiter gehaemmert. Fix: beide Mega-Token aus der UNGEKUERZTEN error.message parsen statt aus dem gekuerzten Anzeige-String. RANGE1-truncate-fail (LOW, Haertung + Doku-Korrektur): Der v1.7.219-Commit behauptete, das Setzen von downloadedBytes ZUERST rette den Fall eines fehlgeschlagenen finalen Rewind-truncate ueber den Re-Entry-prealloc-reconcile. Das stimmt nicht — bei truncate-Fehler finalisiert tryFinalizeItemFromDisk die exakt-langen Garbage-Datei VOR dem Re-Entry (net-neutral vs pre-audit, KEINE Regression, aber der Claim war falsch). Haertung: bei fehlgeschlagenem finalem Rewind die Teil-Datei verwerfen (rmSync + downloadedBytes=0) → Finalize lehnt ab → sauberer Re-Download. Best-effort (faellt der rm auch, bleibt es net-neutral) — macht den 219-Claim jetzt wahr. Rot-bewiesen: Single-Pass-processItem-Test mit Aggregat beider Mega-Token → retryAfter < 60s (Cooldown) und fullStatus "Cooldown" statt "Tagesreset"; mit deaktiviertem Cooldown-Zweig (alte Reihenfolge) ~24h-Park = rot. Volle Suite 886 gruen, tsc=6. --- src/main/download-manager.ts | 44 ++++++++++++++++------------- tasks/audit-loop.md | 29 +++++++++++++++++++ tests/download-manager.test.ts | 51 ++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 19 deletions(-) diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 98528e5..c7a42f6 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -9436,24 +9436,8 @@ export class DownloadManager extends EventEmitter { return; } - const megaResetPark = parseMegaDebridResetPark(errorText); - if (megaResetPark && active.unrestrictRetries < maxUnrestrictRetries) { - active.unrestrictRetries += 1; - item.retries += 1; - logger.warn(`Mega-Debrid bis Tagesreset gesperrt: item=${item.fileName || item.id}, retry=${active.unrestrictRetries}/${retryDisplayLimit}, delay=${megaResetPark.delayMs}ms, link=${item.url.slice(0, 80)}`); - this.queueRetry( - item, - active, - megaResetPark.delayMs, - `Mega-Debrid bis Tagesreset gesperrt, Pause ${Math.ceil(megaResetPark.delayMs / 1000)}s` - ); - item.lastError = megaResetPark.detail || errorText; - this.persistSoon(); - this.emitState(); - return; - } - - const megaCooldownRetry = parseMegaDebridCooldownRetry(errorText); + const megaRawError = error instanceof Error ? String(error.message || "") : String(error || ""); + const megaCooldownRetry = parseMegaDebridCooldownRetry(megaRawError); if (megaCooldownRetry && active.unrestrictRetries < maxUnrestrictRetries) { active.unrestrictRetries += 1; item.retries += 1; @@ -9470,6 +9454,23 @@ export class DownloadManager extends EventEmitter { return; } + const megaResetPark = parseMegaDebridResetPark(megaRawError); + if (megaResetPark && active.unrestrictRetries < maxUnrestrictRetries) { + active.unrestrictRetries += 1; + item.retries += 1; + logger.warn(`Mega-Debrid bis Tagesreset gesperrt: item=${item.fileName || item.id}, retry=${active.unrestrictRetries}/${retryDisplayLimit}, delay=${megaResetPark.delayMs}ms, link=${item.url.slice(0, 80)}`); + this.queueRetry( + item, + active, + megaResetPark.delayMs, + `Mega-Debrid bis Tagesreset gesperrt, Pause ${Math.ceil(megaResetPark.delayMs / 1000)}s` + ); + item.lastError = megaResetPark.detail || errorText; + this.persistSoon(); + this.emitState(); + return; + } + if (isMegaDebridTransientResolveFailure(errorText) && active.unrestrictRetries < maxUnrestrictRetries) { active.unrestrictRetries += 1; item.retries += 1; @@ -10607,7 +10608,12 @@ export class DownloadManager extends EventEmitter { rewindTarget }); } catch (rewindError) { - logAttemptEvent("WARN", "Resume-Schutz: finales Rueckspulen fehlgeschlagen", { + try { + fs.rmSync(effectiveTargetPath, { force: true }); + item.downloadedBytes = 0; + } catch { + } + logAttemptEvent("WARN", "Resume-Schutz: finales Rueckspulen fehlgeschlagen, Teil-Datei verworfen", { attempt, written, rewindTarget, diff --git a/tasks/audit-loop.md b/tasks/audit-loop.md index b118923..91fee79 100644 --- a/tasks/audit-loop.md +++ b/tasks/audit-loop.md @@ -1,5 +1,12 @@ # Autonomer Audit-Loop — Download/Fehler/Rotation (Goal 2026-06-17, 8h) +## GOAL-ANPASSUNG (Nutzer, nach Runde 8 + Synthese-Start): noch Runde 9 + 10, dann Goal BEENDEN. +Plan: Synthese-Pass (wrexz7mdf, Capstone R1-8) auswerten → Runde 9 (Provider-spezifische +Unrestrict-/Rotations-Pfade: Mega-Web-Fallback Session/Single-Flight, AllDebrid Host-Cooldown/ +Rapidgator-Backoff, DebridLink-Key-Rotation, Passwort-Cache-Race) → Runde 10 (Concurrency/Locking + +Settings/Persistenz-Integritaet: Hybrid-Race, targetPath-Claim/Release-Races, Settings-Migration, +Backup/Restore, account-check) → Abschlussbericht + Ende. Fixes je rot-bewiesen, buendeln zu v1.7.220 falls HIGH/MED. + Disziplin: erst BELEGEN (Code-Zitat + konkretes Szenario), dann adversarisch verifizieren, dann TDD-Fix. Kein Blind-Fix. Tests gruen + tsc=6 nach jeder Runde. Periodisch releasen. @@ -185,6 +192,28 @@ Multi-Agent-adversarisch, Routing-Fix frueher advisor-gesegnet, Praezedenz 215/2 Runde-5/6-Charakterisierungen (PP-SEM-1 benign, DISK-1 deferred, deferred-LOW-Cluster #10/#11/#13 benign) NICHT released — dokumentiert. +## SYNTHESE/Regressions-Pass (Fix-Interaktion ueber kumulativen Diff v1.7.212..HEAD) — Workflow wrexz7mdf +3 Reviewer (Catch-Cascade / Streaming / Provider-Chain) + adversarisch verifizieren. 3 confirmed (1 MED, 2 LOW), +2 refuted. Bestaetigt: Rest komponiert sauber; Risiko war (wie Advisor sagte) Fix-Interaktion, nicht neue Bugs. +- **CONFIRMED MED (GEFIXT) C1 reset_park maskiert kurzen Cooldown:** Bei BEIDEN Mega-Modi aktiv aggregiert die + Provider-Kette beide Token (`mega_debrid_reset_park:LONG` von API-Park + `mega_debrid_cooldown:30000` von + Web-Cooldown). Manager prueft reset_park VOR cooldown → Item ~24h geparkt obwohl Web in ~30s erholt. Fix: + Cooldown-Zweig VOR reset_park (kuerzerer Delay gewinnt). Rot-bewiesen (Single-Pass-processItem-Test: Aggregat + beider Token → retryAfter < 60s, fullStatus "Cooldown" nicht "Tagesreset"; ohne Reorder ~24h). +- **CONFIRMED LOW (GEFIXT) C2 Token-Truncation:** reset_park-Token konnte von compactErrorText (220-Zeichen-Cap) + abgeschnitten werden, wenn Mega nicht Lead + vorheriger Provider verbose → Park still uebersprungen. Fix: Mega- + Token aus der UNGEKUERZTEN error.message parsen (megaRawError). (Im selben Edit wie C1.) +- **CONFIRMED LOW (GEHAERTET + Doku korrigiert) RANGE1-TRUNCATEFAIL-FINALIZE:** Mein 219-Commit OVERCLAIMte — + bei FEHLGESCHLAGENEM finalem Rewind-truncate finalisiert tryFinalizeItemFromDisk(9238) die Garbage-Datei + (size==totalBytes) VOR dem Re-Entry. NET-NEUTRAL vs pre-audit (v1.7.212 hatte denselben Finalize), KEINE + Regression. Haertung: bei truncate-Fehler jetzt rmSync(Teil-Datei)+downloadedBytes=0 → Finalize lehnt ab → + sauberer Re-Download (best-effort; faellt der rm auch, bleibt es net-neutral). Macht den 219-Claim wahr. +- **Refuted (1/3 je):** reset_park von shelve-guard/unrestrictRetries-Exhaustion unter finite retryLimit + verdraengt — beide nicht bestaetigt (compound/nicht erreichbar). +- **Capstone-Verdikt:** Die 8-Runden-Fixes komponieren — inner-rewind success-only-reset und final-rewind sind + per-attempt mutually exclusive; prealloc-reconcile double-truncated nicht nach erfolgreichem Rewind; Routing + + cooldown + park kohaerent NACH C1/C2-Fix. Diese 3 Synthese-Fixes buendeln zu v1.7.220. + ## Runde 8 (Byte-Streaming downloadToFile: Range/Resume/Append/Truncation) — Workflow wwjz4srkq 3 Finder + adversarisch verifizieren. 3 confirmed (1 HIGH + 2 MED, alle Silent-Corruption-Familie), 1 refuted. Advisor VOR Implementierung konsultiert (HIGH-Hot-Path) — Design + Check-A-Branch (totalBytes-null) bestaetigt. diff --git a/tests/download-manager.test.ts b/tests/download-manager.test.ts index 9db5a8c..72cdd15 100644 --- a/tests/download-manager.test.ts +++ b/tests/download-manager.test.ts @@ -301,6 +301,57 @@ describe("download manager", () => { expect(session.items[itemId].retries).toBeLessThan(20); }); + it("prefers the short Mega cooldown over the until-reset park when an aggregate error carries both tokens", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-mega-both-")); + tempDirs.push(root); + const storagePaths = createStoragePaths(path.join(root, "state")); + initPackageLogs(storagePaths.baseDir); + initItemLogs(storagePaths.baseDir); + + const session = emptySession(); + const packageId = "mega-both-pkg"; + const itemId = "mega-both-item"; + const outputDir = path.join(root, "downloads", "MegaBoth"); + const extractDir = path.join(root, "extract", "MegaBoth"); + fs.mkdirSync(outputDir, { recursive: true }); + fs.mkdirSync(extractDir, { recursive: true }); + session.packageOrder = [packageId]; + session.packages[packageId] = { + id: packageId, name: "MegaBoth", outputDir, extractDir, + status: "downloading", itemIds: [itemId], cancelled: false, enabled: true, + createdAt: Date.now(), updatedAt: Date.now() + } as any; + session.items[itemId] = { + id: itemId, packageId, url: "https://rapidgator.net/file/mega-both.rar", provider: "megadebrid-api", + status: "downloading", retries: 0, speedBps: 0, downloadedBytes: 0, totalBytes: null, + progressPercent: 0, fileName: "mega-both.rar", targetPath: "", resumable: true, + attempts: 0, lastError: "", fullStatus: "", createdAt: Date.now(), updatedAt: Date.now() + } as any; + + const manager = new DownloadManager( + { ...defaultSettings(), megaLogin: "u", megaPassword: "p", megaCredentials: "u:p", megaDebridApiEnabled: true, megaDebridWebEnabled: true, outputDir: path.join(root, "downloads"), extractDir: path.join(root, "extract") }, + session, + storagePaths + ); + (manager as any).retryStateByItem.set(itemId, { freshRetryUsed: true, resumeHardResetUsed: true, stallRetries: 0, genericErrorRetries: 0, unrestrictRetries: 0 }); + (manager as any).debridService.unrestrictLink = async () => { + throw new Error("Unrestrict fehlgeschlagen: Mega-Debrid API: mega_debrid_reset_park:86400000:Alle Accounts bis zum Tagesreset gesperrt | Mega-Debrid Web: mega_debrid_cooldown:30000:rate limit"); + }; + + const active = { itemId, packageId, abortController: new AbortController(), abortReason: "none", resumable: true, nonResumableCounted: false, blockedOnDiskWrite: false, blockedOnDiskSince: 0 }; + (manager as any).activeTasks.set(itemId, active); + + const before = Date.now(); + await (manager as any).processItem(active); + + const retryAfter = (manager as any).retryAfterByItem.get(itemId) as number; + expect(retryAfter).toBeGreaterThan(0); + const delayMs = retryAfter - before; + expect(delayMs).toBeLessThan(60000); + expect(session.items[itemId].fullStatus).toMatch(/Cooldown/i); + expect(session.items[itemId].fullStatus).not.toMatch(/Tagesreset/i); + }); + it("keeps the quick post-process requeue once the final package items are finished", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-postprocess-final-")); tempDirs.push(root);