Resume + Enrichment stores no longer JSON-decode on the main thread in Application.onCreate (Resume is the heaviest cold-start cost: ~50-100ms decode at its 100k-entry cap). Both seed an empty StateFlow and hydrate off-thread on their PrefsWriter single-thread dispatcher. Mutations that arrive before the hydrate finishes defer onto that FIFO dispatcher (a hydrated gate) so they re-run on fully-loaded state -- correct for adds AND clears; a naive loaded+live merge would resurrect a clear that landed on the empty seed. Subscriptions/Playlists/History/Settings stay eager (tiny + rendered directly -> would flash empty). (audit 2.1)
Video page: details/related rows no longer leak into the status-bar strip above the player when scrolled. topPadding is now a layout inset on the detail LazyColumn, so the scroll viewport begins+clips at the player's bottom edge instead of letting rows scroll up over the clock/signal.
RYD: dropped the dead rating + view_count fields from the strawcore RydVotes FFI Record + Kotlin shim -- only likes/dislikes are rendered; RYD's rating restates the like/dislike ratio and its viewCount is a stale aggregate conflicting with the real YouTube count already shown. (audit #2 L-9)
Adversarially reviewed (Opus): clear-resurrection closed, no defer-loop, FFI slim has no readers. Headless compileDebugKotlin green.
M-2: route every SharedPreferences write (Settings/History/Subs/Resume) through
one PrefsWriter — a per-store single-thread dispatcher — so the on-disk apply()
order matches the in-memory CAS order. Previously a Main-thread toggle and an
IO-thread import could land apply() out of order, and ResumePositions detached
ordering entirely via a fresh globalScope.launch per write; a stale value could
then win the next cold-start load. Each write reads the live StateFlow so disk
converges to the latest in-memory state regardless of enqueue order.
L-14: Settings storage-usage sampling (File.length() x4 + Coil diskCache.size)
moved off the composition/Main thread into a LaunchedEffect on Dispatchers.IO.
L-2 / L-4..L-8 / L-15 / L-16: dead code + stale comments from the vc=85 SB/RYD
to Rust migration. Http.kt trimmed to STRAW_USER_AGENT; reconciled the
network_security_config / feed.rs / SubscriptionFeedViewModel / net.rs / CI
comments with reality; recencyScore overflow-guarded; ci/Dockerfile now
pre-installs build-tools 36 (AGP 9.2.1's actual floor, was auto-fetched).
Verified: headless compileDebugKotlin green on the straw-build image.
- Thumbnails + channel icons stay cached: pin an explicit 256MB Coil disk
cache + sized memory cache via SingletonImageLoader.Factory. Coil's
default disk cap is 2% of the device's free space, so on a storage-tight
phone the subs feed (most image-heavy screen) thrashed it and
re-downloaded thumbnails on every visit.
- SponsorBlock + Return-YouTube-Dislike clients moved Kotlin -> Rust
(strawcore net.rs: fetchSponsorSegments / fetchRydVotes). SponsorBlock
keeps its privacy-preserving SHA-256 hash-prefix lookup. Kotlin is now a
thin shim mapping the FFI records onto the SbSegment/RydVotes domain
types; behavior identical. Migration #2 of "all backend -> Rust".
- Fix crash: extract_channel_id sliced the channel URL by a length derived
from a lowercased copy of itself; to_lowercase() can change byte length
on non-ASCII, so a non-ASCII URL tail could panic across the FFI and
abort the app on a feed refresh. Now matches the prefix case-insensitively
against the original with length + char-boundary guards.
- Fix autoplay hijack: advancing to the next video resolves over ~500ms; if
you manually start a different video meanwhile, autoplay would replace
your choice with the stale next-up. Added a staleness fence.
Verified: cargo check/test/clippy on the wrapper, full Android
compileDebugKotlin green, adversarial FFI pre-push audit passed.