Skip to content

Commit 4af22ab

Browse files
committed
Bugfix: Don't double-dispatch MCP autoConfirm copies
`waitForScanThenStart` in `TransferProgressDialog.svelte` had a race: it subscribed to `scan-preview-complete` first, then awaited `checkScanPreviewStatus`. If the scan completed during that await (common for small sources over MCP, where the scan finishes in microseconds), both the listener AND the `alreadyComplete` branch would call `startOperation()`, dispatching the same copy/move/delete twice. The symptom from an MCP-triggered 4.47 GB NAS→/tmp copy: two separate `copy_between_volumes` operations in the backend with different `operation_ids`, each streaming the full file. They serialized on the SMB session mutex, so the second one truncated `/tmp/<file>` back to zero and re-downloaded it — ~2× the wait time and memory pressure for the user. The UI path wasn't affected because manual Confirm takes long enough that `isScanning` is already false by then, and the dialog takes the direct `startOperation()` branch at onMount. Fix: both paths now converge on a local `kickOff()` helper guarded by a `started` flag. Whoever wins the race calls `kickOff()` first, flips `started = true`, and the second call becomes a no-op. The error and cancelled listeners also flip `started = true` so a late `scan-preview-complete` can't dispatch an operation after we've decided to error/cancel. No UX change: the same operation starts the same way on the same timeline; we just don't start it twice.
1 parent 6f79392 commit 4af22ab

1 file changed

Lines changed: 23 additions & 7 deletions

File tree

apps/desktop/src/lib/file-operations/transfer/TransferProgressDialog.svelte

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -628,8 +628,14 @@
628628
/**
629629
* Waits for the scan preview to complete, then starts the write operation.
630630
*
631-
* Subscribes to scan events BEFORE checking status to avoid the race where
632-
* the scan completes between the status check and listener registration.
631+
* Two independent signals can say "scan done": the `scan-preview-complete`
632+
* event firing, or the post-subscription `checkScanPreviewStatus` IPC
633+
* returning true. Either can win the race. Both converge on `kickOff()`,
634+
* which is idempotent via the `started` flag — so the operation dispatches
635+
* exactly once, even if both signals arrive during the `await`.
636+
*
637+
* We subscribe to events BEFORE the status check so a fast completion
638+
* between subscription and check isn't missed.
633639
*
634640
* Precondition: previewId must be non-null (guaranteed by TransferDialog,
635641
* which awaits startScanPreview IPC before calling onConfirm).
@@ -640,6 +646,15 @@
640646
void startOperation()
641647
return
642648
}
649+
650+
let started = false
651+
const kickOff = () => {
652+
if (started) return
653+
started = true
654+
cleanupScanListeners()
655+
void startOperation()
656+
}
657+
643658
// Subscribe to events FIRST to avoid missing fast completions.
644659
// Same pattern as TransferDialog.startScan().
645660
scanUnlisteners.push(
@@ -662,15 +677,15 @@
662677
scanDirsFound = event.dirsTotal
663678
scanBytesFound = event.bytesTotal
664679
waitingForScan = false
665-
cleanupScanListeners()
666-
// Scan results are now cached — start the operation (guaranteed cache hit)
667-
void startOperation()
680+
kickOff()
668681
}),
669682
)
670683
671684
scanUnlisteners.push(
672685
await onScanPreviewError((event) => {
673686
if (!isOurScanEvent(event.previewId)) return
687+
if (started) return // already dispatched or terminated; ignore late errors
688+
started = true // terminal — don't let a late scan-complete dispatch an operation
674689
log.error('Scan preview error: {message}', { message: event.message })
675690
waitingForScan = false
676691
cleanupScanListeners()
@@ -685,6 +700,8 @@
685700
scanUnlisteners.push(
686701
await onScanPreviewCancelled((event) => {
687702
if (!isOurScanEvent(event.previewId)) return
703+
if (started) return // already dispatched or terminated; ignore late cancellations
704+
started = true // terminal — don't let a late scan-complete dispatch an operation
688705
log.info('Scan preview cancelled')
689706
waitingForScan = false
690707
cleanupScanListeners()
@@ -698,8 +715,7 @@
698715
log.info('Scan preview already complete for previewId={previewId}, starting operation immediately', {
699716
previewId,
700717
})
701-
cleanupScanListeners()
702-
void startOperation()
718+
kickOff()
703719
return
704720
}
705721

0 commit comments

Comments
 (0)