Opening an uncached message stalled ~35-74s (avg 48s) behind the reader
spinner because the on-demand IMAP body fetch has no priority over the
continuous full-history backfill (#12) and loses the race for the
account's IMAP throughput (connect-per-op client, no shared lock).
Introduce InteractiveImapGate (@Singleton), a process-wide priority
signal mirroring MailMaintenanceGate/AccountThrottleGate (#360):
- MailRepositoryImpl wraps the user-facing IMAP paths (openMessage,
inlineImages, downloadAttachment, buildReplyDraft) in withInteractive
{}, which raises an in-flight counter for the duration and always
lowers it in a finally, so a failed fetch can never strand it.
- MailBackfiller parks (awaitInteractiveIdle) at its per-page yield
point while the counter is non-zero, resuming the instant it clears.
This is also the slice's first yield point, so a slice never begins a
page while a user is waiting on a body.
- Backfill's own content prefetch bypasses the gate (calls
ensureAttachmentFile directly) so it never yields to itself.
A counter, not a mutex, is used so overlapping interactive fetches run
concurrently and backfill waits for all to clear. PII-free AppLog
park/resume breadcrumbs (accountLogRef) at the backfill yield points.
Builds on #360's throttle framework (orthogonal: that backs off after a
provider rejects background work; this yields to a foreground fetch).
Tests: InteractiveImapGateTest (counter/park/resume/error-release/
concurrency + Turbine), MailBackfillerTest park+resume+no-deadlock,
MailRepositoryImplTest gate-held-during-open, and
InteractiveImapGateInstrumentedTest (on-device pause/resume across the
CI API matrix).
Closes#355