Verified finding(s) from the 2026-07-09 whole-repo multi-agent review (independent finder, then adversarial verifier; verdict CONFIRMED).
Triage: below the cut (confirmed, medium) — backlog.
app/src/main/kotlin/org/libremail/data/sync/MailSyncer.kt:202 — medium
MailSyncer's background prefetch loop never consults AccountThrottleGate, so a throttled account keeps being hammered with bulk body/attachment downloads on every sync — the #360 backoff is enforced only at MailBackfiller's call site instead of in the shared background-fetch path.
Failure scenario: Gmail rate-limits the account (the documented on-device perf finding: throttling builds with volume and persists once tripped). MailBackfiller correctly skips the account for the backoff window, but every 15-min periodic sync, every IDLE push, and every folder open re-enters prefetchIfEnabled, which loops over ALL unfetched ids downloading bodies/attachments with no throttleGate.remainingBackoffMillis check — and prefetchMessage swallows its own failures, so the resulting throttle errors are never even recorded via ThrottleClassifier. The account is continuously re-throttled, interactive message opens stay in the 30-48s regime #360 was built to fix, and each new provider mechanism must remember to re-add the same skip at every caller because the consultation lives at one call site (MailBackfiller.runBackfill) instead of the shared layer.
Verifier justification (CONFIRMED): MailSyncer.prefetchIfEnabled's download loop (line 202: for (id in messageDao.getUnfetchedIds(account.id, folder)) { ... mailRepository.prefetchMessage(id) }) contains no AccountThrottleGate consult — grep confirms the only remainingBackoffMillis/isThrottled consumers are MailBackfiller.runBackfill:88 and GraphThrottle — and prefetchMessage's failures are swallowed (Result ignored at line 204; ThrottleClassifier.classify is called only in MailSyncer:165 header-sync failure, MailBackfiller:138, and GraphThrottle), so throttle rejections during bulk body/attachment downloads are neither skipped nor recorded. This contradicts AccountThrottleGate's own documented contract (KDoc lines 16-20: 'Background activity ... consults remainingBackoffMillis/isThrottled before touching an account and skips one that is still cooling down'), and prefetch is explicitly background, not interactive (MailRepositoryImpl:344 'deliberately bypasses downloadAttachment's interactive gate'). Concrete trigger: Gmail account with an active backoff window + prefetch-permitting fetch policy + unfetched bodies in the recent window → every 15-min sync, IDLE push, and folder open re-runs the full body-download loop against the throttled account; failed fetches leave the ids unfetched, so the same set is re-hammered on every entry, sustaining the throttle (the documented on-device finding: throttling builds with volume and persists once tripped) and keeping interactive opens slow. Re-rated high→medium: the outcome is self-sustaining perf degradation, not data loss or missed mail (headers keep syncing; bodies fill lazily on open).
Defective line:for (id in messageDao.getUnfetchedIds(account.id, folder)) { currentCoroutineContext().ensureActive() mailRepository.prefetchMessage(id) // best-effort; swallows its own per-message failures }
Fix hint: In MailSyncer.prefetchIfEnabled, skip when throttleGate.isThrottled(account.id) (mirroring MailBackfiller.runBackfill:88-92, with an AppLog breadcrumb), and surface prefetchMessage failures through ThrottleClassifier.classify → throttleGate.onThrottle so throttle hits from the body path are recorded; better, move both the consult and the classification into a shared prefetch helper (or MailRepositoryImpl.prefetchMessage itself) so every caller is covered instead of each call site re-adding the skip.
Verified finding(s) from the 2026-07-09 whole-repo multi-agent review (independent finder, then adversarial verifier; verdict **CONFIRMED**).
**Triage: below the cut (confirmed, medium) — backlog.**
## `app/src/main/kotlin/org/libremail/data/sync/MailSyncer.kt:202` — medium
MailSyncer's background prefetch loop never consults AccountThrottleGate, so a throttled account keeps being hammered with bulk body/attachment downloads on every sync — the #360 backoff is enforced only at MailBackfiller's call site instead of in the shared background-fetch path.
**Failure scenario:** Gmail rate-limits the account (the documented on-device perf finding: throttling builds with volume and persists once tripped). MailBackfiller correctly skips the account for the backoff window, but every 15-min periodic sync, every IDLE push, and every folder open re-enters prefetchIfEnabled, which loops over ALL unfetched ids downloading bodies/attachments with no throttleGate.remainingBackoffMillis check — and prefetchMessage swallows its own failures, so the resulting throttle errors are never even recorded via ThrottleClassifier. The account is continuously re-throttled, interactive message opens stay in the 30-48s regime #360 was built to fix, and each new provider mechanism must remember to re-add the same skip at every caller because the consultation lives at one call site (MailBackfiller.runBackfill) instead of the shared layer.
**Verifier justification (CONFIRMED):** MailSyncer.prefetchIfEnabled's download loop (line 202: `for (id in messageDao.getUnfetchedIds(account.id, folder)) { ... mailRepository.prefetchMessage(id) }`) contains no AccountThrottleGate consult — grep confirms the only remainingBackoffMillis/isThrottled consumers are MailBackfiller.runBackfill:88 and GraphThrottle — and prefetchMessage's failures are swallowed (Result ignored at line 204; ThrottleClassifier.classify is called only in MailSyncer:165 header-sync failure, MailBackfiller:138, and GraphThrottle), so throttle rejections during bulk body/attachment downloads are neither skipped nor recorded. This contradicts AccountThrottleGate's own documented contract (KDoc lines 16-20: 'Background activity ... consults remainingBackoffMillis/isThrottled before touching an account and skips one that is still cooling down'), and prefetch is explicitly background, not interactive (MailRepositoryImpl:344 'deliberately bypasses downloadAttachment's interactive gate'). Concrete trigger: Gmail account with an active backoff window + prefetch-permitting fetch policy + unfetched bodies in the recent window → every 15-min sync, IDLE push, and folder open re-runs the full body-download loop against the throttled account; failed fetches leave the ids unfetched, so the same set is re-hammered on every entry, sustaining the throttle (the documented on-device finding: throttling builds with volume and persists once tripped) and keeping interactive opens slow. Re-rated high→medium: the outcome is self-sustaining perf degradation, not data loss or missed mail (headers keep syncing; bodies fill lazily on open).
**Defective line:** `for (id in messageDao.getUnfetchedIds(account.id, folder)) {
currentCoroutineContext().ensureActive()
mailRepository.prefetchMessage(id) // best-effort; swallows its own per-message failures
}`
**Fix hint:** In MailSyncer.prefetchIfEnabled, skip when throttleGate.isThrottled(account.id) (mirroring MailBackfiller.runBackfill:88-92, with an AppLog breadcrumb), and surface prefetchMessage failures through ThrottleClassifier.classify → throttleGate.onThrottle so throttle hits from the body path are recorded; better, move both the consult and the classification into a shared prefetch helper (or MailRepositoryImpl.prefetchMessage itself) so every caller is covered instead of each call site re-adding the skip.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Verified finding(s) from the 2026-07-09 whole-repo multi-agent review (independent finder, then adversarial verifier; verdict CONFIRMED).
Triage: below the cut (confirmed, medium) — backlog.
app/src/main/kotlin/org/libremail/data/sync/MailSyncer.kt:202— mediumMailSyncer's background prefetch loop never consults AccountThrottleGate, so a throttled account keeps being hammered with bulk body/attachment downloads on every sync — the #360 backoff is enforced only at MailBackfiller's call site instead of in the shared background-fetch path.
Failure scenario: Gmail rate-limits the account (the documented on-device perf finding: throttling builds with volume and persists once tripped). MailBackfiller correctly skips the account for the backoff window, but every 15-min periodic sync, every IDLE push, and every folder open re-enters prefetchIfEnabled, which loops over ALL unfetched ids downloading bodies/attachments with no throttleGate.remainingBackoffMillis check — and prefetchMessage swallows its own failures, so the resulting throttle errors are never even recorded via ThrottleClassifier. The account is continuously re-throttled, interactive message opens stay in the 30-48s regime #360 was built to fix, and each new provider mechanism must remember to re-add the same skip at every caller because the consultation lives at one call site (MailBackfiller.runBackfill) instead of the shared layer.
Verifier justification (CONFIRMED): MailSyncer.prefetchIfEnabled's download loop (line 202:
for (id in messageDao.getUnfetchedIds(account.id, folder)) { ... mailRepository.prefetchMessage(id) }) contains no AccountThrottleGate consult — grep confirms the only remainingBackoffMillis/isThrottled consumers are MailBackfiller.runBackfill:88 and GraphThrottle — and prefetchMessage's failures are swallowed (Result ignored at line 204; ThrottleClassifier.classify is called only in MailSyncer:165 header-sync failure, MailBackfiller:138, and GraphThrottle), so throttle rejections during bulk body/attachment downloads are neither skipped nor recorded. This contradicts AccountThrottleGate's own documented contract (KDoc lines 16-20: 'Background activity ... consults remainingBackoffMillis/isThrottled before touching an account and skips one that is still cooling down'), and prefetch is explicitly background, not interactive (MailRepositoryImpl:344 'deliberately bypasses downloadAttachment's interactive gate'). Concrete trigger: Gmail account with an active backoff window + prefetch-permitting fetch policy + unfetched bodies in the recent window → every 15-min sync, IDLE push, and folder open re-runs the full body-download loop against the throttled account; failed fetches leave the ids unfetched, so the same set is re-hammered on every entry, sustaining the throttle (the documented on-device finding: throttling builds with volume and persists once tripped) and keeping interactive opens slow. Re-rated high→medium: the outcome is self-sustaining perf degradation, not data loss or missed mail (headers keep syncing; bodies fill lazily on open).Defective line:
for (id in messageDao.getUnfetchedIds(account.id, folder)) { currentCoroutineContext().ensureActive() mailRepository.prefetchMessage(id) // best-effort; swallows its own per-message failures }Fix hint: In MailSyncer.prefetchIfEnabled, skip when throttleGate.isThrottled(account.id) (mirroring MailBackfiller.runBackfill:88-92, with an AppLog breadcrumb), and surface prefetchMessage failures through ThrottleClassifier.classify → throttleGate.onThrottle so throttle hits from the body path are recorded; better, move both the consult and the classification into a shared prefetch helper (or MailRepositoryImpl.prefetchMessage itself) so every caller is covered instead of each call site re-adding the skip.