Warm opens are instant, but the first open in a session still paid the
one-time reads (contacts, the channel-slot probe) in the navigation path —
so opening the menu after login, then the first Chat, read as two separate
stalls. Fold them into one quiet wait behind the menu instead.
DeviceState.prewarm() spawns a tracked background task that fetches contacts
then the channel slots (sequential — concurrent reads collide on the BLE
UART), best-effort and cancelled on reset/close. _resume_monitor kicks it off
once the link is up (only when connected — a deferred connect warms lazily on
first use). A getter reached before the warm finishes awaits the same
in-flight fetch, so it never doubles a read.
The channel probe itself still walks every slot on firmware that doesn't
reject out-of-range indices; bounding that is a separate change, deferred on
purpose — a long one-time poll behind the menu is fine for now.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>