Why they can't overlap
Chat and heartbeat share OAuth credentials and ChromaDB access. Running them in parallel risks rate-limit collisions and ChromaDB lock contention.
Lightweight mutual exclusion
- A stream counter tracks live chat streams.
- A heartbeat flag tracks background ticks.
- Chat waits for the heartbeat flag to clear before starting.
- Heartbeat skips its tick if a chat stream is active.
- Both check before proceeding, not during — no lock contention.
Why this is simpler than asyncio.Lock
Single-user system. The contention is always 'is Dad active right now?' — a boolean question. Locks would imply contested concurrency, which isn't what's happening here.