1. Collapse the session pipeline's three fetch stacks
flowchart LR
C1[Session self._fetch_from_cdn] --> T1[cdn.try_sources loop]
C2[Session self._fetch_from_cdn_fast] --> T1
C3[Session._fetch_one prefetch] --> T3[own per-source fallback, swallows errors]
A[fetch_json_async] --> T2[own retry loop + own CDN fallback]
A -. own lazy niquests import + own key format .-> X[(cache & session)]
T1 --> D[CDN sources]
T2 --> D
T3 --> D
E[events.py schedule fetch] --> T4[hardcoded jsDelivr URL for a foreign repo]
T4 --> D
classDef leak stroke:#dc2626,stroke-width:2px;
class T2,T3,T4 leak
After — one engine, adapters at the seam
Session pipeline fetch engine
retry · circuit breaker · CDN fallback · cache read/write · key format
asyncio.run async engine
native schedule adapter
vendored file test adapter
stubs today
Problem
Retry, circuit-breaking, CDN fallback and cache-key knowledge is copied into four modules — async_fetch, cdn (used by core only), core's prefetch, and events' schedule fetch, which appends a fourth, foreign URL grammar.
Solution
Treat the async fetch as the one fetch engine behind a single seam; sync callers, prefetch, and the schedule fetch become adapters on it, and fallback, breaker and key format migrate behind the engine's interface.
- locality: one place to patch a retry bug, not four
- events.py stops importing private http symbols
- tests already stub one seam today
- cache-key drift cured by construction
- cdn.try_sources finds its only caller