Finding
When try_dispatch_next builds an ActiveEntry for a dequeued item it unconditionally sets retry_count: 0. The download_queue.retry_count column is incremented in the DB on each failure, but that persisted value is never read back when a recovered item is re-dispatched. row_to_queue_item reconstructs a QueueItem from a DB row, yet QueueItem carries no retry_count field, so the stored retry count is silently dropped. After a restart, every previously-retried item begins again with a fresh budget of 0, as if it had never been attempted.
Evidence
crates/syntaxis/src/lib.rs:338:
(in the ActiveEntry construction inside try_dispatch_next)
crates/syntaxis/src/recovery.rs:31-46: row_to_queue_item builds a QueueItem from a QueueRow, but QueueItem has no retry_count field, so QueueRow::retry_count is never carried forward.
Why this matters
A permanently-failing download — a deleted or corrupted source that keeps returning transient-looking errors — is meant to exhaust its retry budget and stop. Because the budget resets to 0 on every dispatch after a restart, such an item retries forever on any host that restarts regularly (a phone reboots often). It permanently consumes a slot, repeatedly hits the network, and burns tracker credits. Under counter-surveillance, that is a recurring, predictable, externally-observable retry signature against the same dead endpoint — exactly the kind of stable beacon that aids traffic-pattern fingerprinting, plus a steady drain on battery and metered/scarce bandwidth.
Desired correction
Add retry_count: u32 to QueueItem. Populate it from QueueRow::retry_count (clamping to u32) in row_to_queue_item, and pass it through to ActiveEntry in try_dispatch_next instead of the hardcoded 0.
Done when: a recovered item with N prior retry attempts fails permanently after max_retries - N further attempts, not after max_retries.
Finding
When
try_dispatch_nextbuilds anActiveEntryfor a dequeued item it unconditionally setsretry_count: 0. Thedownload_queue.retry_countcolumn is incremented in the DB on each failure, but that persisted value is never read back when a recovered item is re-dispatched.row_to_queue_itemreconstructs aQueueItemfrom a DB row, yetQueueItemcarries noretry_countfield, so the stored retry count is silently dropped. After a restart, every previously-retried item begins again with a fresh budget of 0, as if it had never been attempted.Evidence
crates/syntaxis/src/lib.rs:338:(in the
ActiveEntryconstruction insidetry_dispatch_next)crates/syntaxis/src/recovery.rs:31-46:row_to_queue_itembuilds aQueueItemfrom aQueueRow, butQueueItemhas noretry_countfield, soQueueRow::retry_countis never carried forward.Why this matters
A permanently-failing download — a deleted or corrupted source that keeps returning transient-looking errors — is meant to exhaust its retry budget and stop. Because the budget resets to 0 on every dispatch after a restart, such an item retries forever on any host that restarts regularly (a phone reboots often). It permanently consumes a slot, repeatedly hits the network, and burns tracker credits. Under counter-surveillance, that is a recurring, predictable, externally-observable retry signature against the same dead endpoint — exactly the kind of stable beacon that aids traffic-pattern fingerprinting, plus a steady drain on battery and metered/scarce bandwidth.
Desired correction
Add
retry_count: u32toQueueItem. Populate it fromQueueRow::retry_count(clamping tou32) inrow_to_queue_item, and pass it through toActiveEntryintry_dispatch_nextinstead of the hardcoded0.Done when: a recovered item with N prior retry attempts fails permanently after
max_retries - Nfurther attempts, not aftermax_retries.