diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 3df8a80..2dd9cad 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -4661,6 +4661,29 @@ export class DownloadManager extends EventEmitter { 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): 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[] { const folderCandidates: string[] = []; let currentDir = path.dirname(sourcePath); @@ -5326,6 +5349,7 @@ export class DownloadManager extends EventEmitter { } this.runItemIds = new Set(runItems.map((item) => item.id)); this.runPackageIds = new Set(runItems.map((item) => item.packageId)); + this.addTrailingPostProcessPackageIds(this.runPackageIds); this.runOutcomes.clear(); this.runCompletedPackages.clear(); this.retryAfterByItem.clear(); @@ -5430,6 +5454,7 @@ export class DownloadManager extends EventEmitter { } this.runItemIds = new Set(runItems.map((item) => item.id)); this.runPackageIds = new Set(runItems.map((item) => item.packageId)); + this.addTrailingPostProcessPackageIds(this.runPackageIds); this.runOutcomes.clear(); this.runCompletedPackages.clear(); this.retryAfterByItem.clear(); @@ -5519,6 +5544,7 @@ export class DownloadManager extends EventEmitter { if (this.packagePostProcessTasks.size > 0) { this.runItemIds.clear(); this.runPackageIds.clear(); + this.addTrailingPostProcessPackageIds(this.runPackageIds); this.runOutcomes.clear(); this.runCompletedPackages.clear(); this.session.running = true; @@ -5537,6 +5563,7 @@ export class DownloadManager extends EventEmitter { } this.runItemIds.clear(); this.runPackageIds.clear(); + this.addTrailingPostProcessPackageIds(this.runPackageIds); this.runOutcomes.clear(); this.runCompletedPackages.clear(); this.retryAfterByItem.clear(); @@ -5567,6 +5594,7 @@ export class DownloadManager extends EventEmitter { } this.runItemIds = new Set(runItems.map((item) => item.id)); this.runPackageIds = new Set(runItems.map((item) => item.packageId)); + this.addTrailingPostProcessPackageIds(this.runPackageIds); this.runOutcomes.clear(); this.runCompletedPackages.clear(); this.retryAfterByItem.clear(); @@ -7499,6 +7527,11 @@ export class DownloadManager extends EventEmitter { completedItems: completedItems.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.emitState(true); 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, targetedItems: targetItems.length }); + this.notifiedPackages.delete(packageId); + this.runPackageIds.add(packageId); this.persistSoon(); this.emitState(true); void this.runPackagePostProcessing(packageId).catch((err) => logger.warn(`runPackagePostProcessing Fehler (extractNow): ${compactErrorText(err)}`)); @@ -10419,6 +10454,10 @@ export class DownloadManager extends EventEmitter { if (!pkg) { 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); } logger.warn( @@ -10503,10 +10542,17 @@ export class DownloadManager extends EventEmitter { return; } 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, { title: kind === "completed" ? "✅ Paket fertig" : "❌ Paket fehlgeschlagen", message: `${pkg.name}\n${detail}`, mention: this.settings.notifyMention + }).then((ok) => { + if (!ok) { + this.notifiedPackages.delete(pkg.id); + } }); }