Three root causes found via live logcat on device:
1. concurrent refresh() race: onStartCommand received twice causes two
refresh() coroutines to run in parallel, doubling FileObserver and
catchupScan registrations. Fixed with Mutex.withLock on refresh().
2. catchupScan no cooldown: catchup syncs write files but never set
syncCooldownUntil, so every written file immediately re-triggers
onChangeDetected. Fixed by setting cooldown before enqueue and
watching work completion same as onChangeDetected does.
3. CancellationException caught silently: exception handler
catch(_: Exception) was catching CancellationException and resetting
cooldown to 0L, re-opening the loop. Fixed by rethrowing
CancellationException and setting 60s cooldown on other errors.
Icon: interlocked rings (blue/red/green/orange) with sync arrow at
center, pure black background — matches reference image.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>