Fix: Dedup-Lifecycle der Benachrichtigungen — Recovery/Retry/Trailing-Pakete

Audit-Befunde DEDUP-1 (HIGH), DEDUP-3, DEDUP-4, NOTIFY-DEDUP-BEFORE-CONFIRM:

- DEDUP-1: recoverRetryableItems (Auto-Requeue gefailter Items beim Start)
  loescht jetzt den notifiedPackages-Eintrag der betroffenen Pakete. Vorher
  blieb der Failed-Marker aus Run 1 kleben — die Erfolgs-Benachrichtigung nach
  geglueckter Recovery in Run 2 (genau die Nachricht, auf die man wartet) kam
  nie; in Discord blieb das Paket fuer immer .
- DEDUP-3: retryExtraction/extractNow loeschen den Dedup-Marker und treten dem
  Run UNBEDINGT bei (runPackageIds.add) — der korrigierende  nach manuell
  wiederholtem Entpacken kam sonst nie an, v.a. wenn der Run schon vorbei war.
  History-Dedup bewusst unangetastet (sonst doppelte History-Eintraege).
- DEDUP-4: start()/startPackages()/startItems() ersetzen runPackageIds; Pakete
  mit noch LAUFENDEM Post-Processing (Task, Deferred, Hybrid) flogen aus dem
  Set und ihre Abschluss-Benachrichtigung wurde nach dem naechsten finishRun
  verworfen (autoExtractWhenStopped beendet den Run ~sofort). Neuer Helper
  addTrailingPostProcessPackageIds ergaenzt das neue Set an allen 5 Stellen —
  inkl. der Start-ohne-Queue-Pfade, die sogar die GERADE angestossenen
  Entpackungen aus dem Set warfen.
- Dedup-Marker wird wieder freigegeben, wenn der Versand (nach den Sender-
  Retries) endgueltig scheiterte — ein transienter Ausfall verbraucht den
  Einmal-Slot nicht mehr dauerhaft.
This commit is contained in:
Sucukdeluxe 2026-06-10 00:28:50 +02:00
parent 99a24592c5
commit 8acb22d3af

View File

@ -4661,6 +4661,29 @@ export class DownloadManager extends EventEmitter {
return false; return false;
} }
// Packages whose post-processing (task, deferred pass or hybrid round) is still
// alive must keep their run-membership when the run set is replaced/cleared —
// otherwise their terminal notification is dropped as soon as session.running
// flips false (the notify guard checks running || runPackageIds).
private addTrailingPostProcessPackageIds(target: Set<string>): void {
for (const id of this.packagePostProcessTasks.keys()) {
target.add(id);
}
for (const [id, controller] of this.packageDeferredPostProcessAbortControllers) {
if (!controller.signal.aborted) {
target.add(id);
}
}
for (const [id, hybridSet] of this.packageHybridPostProcessControllers) {
for (const c of hybridSet) {
if (!c.signal.aborted) {
target.add(id);
break;
}
}
}
}
private buildCollectFolderCandidates(sourcePath: string, sourceRoot: string, pkg: PackageEntry): string[] { private buildCollectFolderCandidates(sourcePath: string, sourceRoot: string, pkg: PackageEntry): string[] {
const folderCandidates: string[] = []; const folderCandidates: string[] = [];
let currentDir = path.dirname(sourcePath); let currentDir = path.dirname(sourcePath);
@ -5326,6 +5349,7 @@ export class DownloadManager extends EventEmitter {
} }
this.runItemIds = new Set(runItems.map((item) => item.id)); this.runItemIds = new Set(runItems.map((item) => item.id));
this.runPackageIds = new Set(runItems.map((item) => item.packageId)); this.runPackageIds = new Set(runItems.map((item) => item.packageId));
this.addTrailingPostProcessPackageIds(this.runPackageIds);
this.runOutcomes.clear(); this.runOutcomes.clear();
this.runCompletedPackages.clear(); this.runCompletedPackages.clear();
this.retryAfterByItem.clear(); this.retryAfterByItem.clear();
@ -5430,6 +5454,7 @@ export class DownloadManager extends EventEmitter {
} }
this.runItemIds = new Set(runItems.map((item) => item.id)); this.runItemIds = new Set(runItems.map((item) => item.id));
this.runPackageIds = new Set(runItems.map((item) => item.packageId)); this.runPackageIds = new Set(runItems.map((item) => item.packageId));
this.addTrailingPostProcessPackageIds(this.runPackageIds);
this.runOutcomes.clear(); this.runOutcomes.clear();
this.runCompletedPackages.clear(); this.runCompletedPackages.clear();
this.retryAfterByItem.clear(); this.retryAfterByItem.clear();
@ -5519,6 +5544,7 @@ export class DownloadManager extends EventEmitter {
if (this.packagePostProcessTasks.size > 0) { if (this.packagePostProcessTasks.size > 0) {
this.runItemIds.clear(); this.runItemIds.clear();
this.runPackageIds.clear(); this.runPackageIds.clear();
this.addTrailingPostProcessPackageIds(this.runPackageIds);
this.runOutcomes.clear(); this.runOutcomes.clear();
this.runCompletedPackages.clear(); this.runCompletedPackages.clear();
this.session.running = true; this.session.running = true;
@ -5537,6 +5563,7 @@ export class DownloadManager extends EventEmitter {
} }
this.runItemIds.clear(); this.runItemIds.clear();
this.runPackageIds.clear(); this.runPackageIds.clear();
this.addTrailingPostProcessPackageIds(this.runPackageIds);
this.runOutcomes.clear(); this.runOutcomes.clear();
this.runCompletedPackages.clear(); this.runCompletedPackages.clear();
this.retryAfterByItem.clear(); this.retryAfterByItem.clear();
@ -5567,6 +5594,7 @@ export class DownloadManager extends EventEmitter {
} }
this.runItemIds = new Set(runItems.map((item) => item.id)); this.runItemIds = new Set(runItems.map((item) => item.id));
this.runPackageIds = new Set(runItems.map((item) => item.packageId)); this.runPackageIds = new Set(runItems.map((item) => item.packageId));
this.addTrailingPostProcessPackageIds(this.runPackageIds);
this.runOutcomes.clear(); this.runOutcomes.clear();
this.runCompletedPackages.clear(); this.runCompletedPackages.clear();
this.retryAfterByItem.clear(); this.retryAfterByItem.clear();
@ -7499,6 +7527,11 @@ export class DownloadManager extends EventEmitter {
completedItems: completedItems.length, completedItems: completedItems.length,
targetedItems: targetItems.length targetedItems: targetItems.length
}); });
// Fresh outcome must notify again (the corrective checkmark after a notified
// extraction failure); unconditional run-membership so the notify guard also
// passes when the retry happens after the run already ended.
this.notifiedPackages.delete(packageId);
this.runPackageIds.add(packageId);
this.persistSoon(); this.persistSoon();
this.emitState(true); this.emitState(true);
void this.runPackagePostProcessing(packageId).catch((err) => logger.warn(`runPackagePostProcessing Fehler (retryExtraction): ${compactErrorText(err)}`)); void this.runPackagePostProcessing(packageId).catch((err) => logger.warn(`runPackagePostProcessing Fehler (retryExtraction): ${compactErrorText(err)}`));
@ -7527,6 +7560,8 @@ export class DownloadManager extends EventEmitter {
completedItems: completedItems.length, completedItems: completedItems.length,
targetedItems: targetItems.length targetedItems: targetItems.length
}); });
this.notifiedPackages.delete(packageId);
this.runPackageIds.add(packageId);
this.persistSoon(); this.persistSoon();
this.emitState(true); this.emitState(true);
void this.runPackagePostProcessing(packageId).catch((err) => logger.warn(`runPackagePostProcessing Fehler (extractNow): ${compactErrorText(err)}`)); void this.runPackagePostProcessing(packageId).catch((err) => logger.warn(`runPackagePostProcessing Fehler (extractNow): ${compactErrorText(err)}`));
@ -10419,6 +10454,10 @@ export class DownloadManager extends EventEmitter {
if (!pkg) { if (!pkg) {
continue; continue;
} }
// The package gets a fresh chance — its next outcome must notify again
// (a recovery success after a notified failure is the message the user
// most wants). History dedup stays untouched (separate semantics).
this.notifiedPackages.delete(packageId);
this.refreshPackageStatus(pkg); this.refreshPackageStatus(pkg);
} }
logger.warn( logger.warn(
@ -10503,10 +10542,17 @@ export class DownloadManager extends EventEmitter {
return; return;
} }
this.notifiedPackages.add(pkg.id); this.notifiedPackages.add(pkg.id);
// Release the dedup marker if the delivery ultimately failed (after the
// sender's own retries), so a later manual re-run can notify again instead
// of a transient outage permanently consuming the once-per-package slot.
void sendNotification(url, { void sendNotification(url, {
title: kind === "completed" ? "✅ Paket fertig" : "❌ Paket fehlgeschlagen", title: kind === "completed" ? "✅ Paket fertig" : "❌ Paket fehlgeschlagen",
message: `${pkg.name}\n${detail}`, message: `${pkg.name}\n${detail}`,
mention: this.settings.notifyMention mention: this.settings.notifyMention
}).then((ok) => {
if (!ok) {
this.notifiedPackages.delete(pkg.id);
}
}); });
} }