If a person pauses a download and automation cancels it for taking too long, the interface and the runner disagree about who controls the transfer. I excluded intentional pause from the transfer budget while continuing to check task cancellation. Waiting needed more than one undifferentiated elapsed-time counter.

Read why progress stopped before deciding what time means

No progress can mean a slow network, a paused native item, or a person cancelling the entire task. The first needs a timeout, the second needs respect for deliberate waiting, and the third needs cleanup. Treating them as one stalled state makes the interface promise something the runner later contradicts.

DownloadFrom keeps remaining and lastTick and periodically subtracts elapsed time. Time is charged while no item has arrived, while transfer has finished and hashing continues, or while the item is not paused. Native pause is the exception. Errors do not grant unlimited waiting, and intentional pause does not disable task cancellation.

Pausing does not remove timeout machinery

The poll interval is fifty milliseconds and the budget is updated from actual timestamp differences. This samples pause state; it is not perfect instrumentation of the exact instant of every human click. The policy is that observed paused intervals should not continuously consume the transfer-failure budget.

When remaining expires, a still-active item is cancelled and DOWNLOAD_TIMEOUT is returned. Hashing after native completion still consumes budget because the waiting caller has not yet received its verified result. Limiting the accounting to network movement alone would omit the cost of reading the saved artifact. The caller waits for a usable receipt, not merely for disappearance of a progress indicator.

Cancellation remains effective while the native item is paused

The periodic check calls check before accounting for elapsed time. If the task is cancelled while its item is paused, the transfer is cancelled and the waiting call rejects. Pause and cancel are not alternate labels for one action: temporarily stopping and asking the work to end express different intent.

The cancellation helper attempts native cancel only when an item exists and transferDone is still false, accounting for an already-released terminal object. Finally removes the timer and session listener so later work does not inherit this wait. The helper handles this transfer; it does not claim to cancel every unrelated follow-up operation belonging to a larger business workflow.

A stale button must not overwrite completion with cancellation

ActiveDownloads.perform verifies the current profile and live item, then rereads native state at the moment of action. A stale pause button cannot outrank a transfer that is already complete. Update releases terminal items, and an action without a current capability is refused.

Cancel can synchronously emit done and release the native object. The implementation rereads state only if the same active object remains afterward. The original fixture models completion winning during cancellation and later object access throwing. This race matters because a successfully completed file should not be relabelled cancelled merely to satisfy an older control request from the interface.

Check a pause longer than the nominal budget

The original pause fixture holds an item longer than its short timeout, checks that cancellation did not occur, then completes and checks the file size. Another case checks that task cancellation still invokes cancel on a paused item. Passing just one condition would not establish the control relationship expected by the person using the interface.

This article reviews those sources and tests; it is not a new run of the original app or network transfer. The linked receipt package compares bytes and implements no timer or pause. I keep that reuse boundary explicit. The general design lesson is to distinguish intentional waiting from failure while honoring cancellation even inside the intentional-wait state.