a04588740b
Stage 5. A /media cache hit no longer touches SQLite: it records the access timestamp into a module-level accumulator (a dict write on the event loop, cheap and atomic), and a supervised 60s background task flushes the whole batch in one executemany UPDATE. This removes both per-hit write sites — the awaited to_thread in download_media_file and the fire-and-forget create_task in the pre-semaphore fast path — so under active RSS polling the threadpool is no longer starved by per-request access-time UPDATEs. - file_io: add update_media_file_access_bulk_sync (one connection, executemany; empty batch is a no-op). - api_server: _access_updates accumulator + _flush_access_updates (snapshot- then-clear atomically before the await so writes during the flush land in the fresh dict; re-queue the batch with setdefault on write failure so a fresher concurrent write is never clobbered and no access-time is lost) + _access_flush_loop under _supervised + a final flush on shutdown, ordered after the loop task is cancelled and before the io threadpool is shut down. - Keys use str(channel) to match the TEXT channel column (a str/int mix would make the UPDATE WHERE silently never match, evicting still-used files). - tests/test_stage5_sqlite.py: 7 tests (no-sync-write hot path, str-key discipline, hit->flush->DB, empty no-op, snapshot-then-clear race, re-queue- without-clobbering-fresh, bulk SQL). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>