Two bugs caused multiple articles to land on the same publish slot:
1. main.py: asyncio.create_task() returned immediately, allowing a second
pipeline trigger (N8N + Telegram /run or two N8N calls) to start a
second concurrent run. Added asyncio.Lock (_pipeline_lock) so any
second trigger while the pipeline is running is rejected immediately.
2. scheduler.py: reserve_publish_slot() read the list of occupied slots
and wrote the new slot in two separate DB connections. Concurrent threads
could both see the same "free" slot before either committed its write.
Fixed by wrapping the entire read-find-write cycle in a threading.Lock
(_slot_lock) and a single DB connection, so the slot check and the
slot assignment are atomic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>