Concurrency and scheduling
The moment you have more than one channel and a scheduler, two events can land on the same session at the same time. Without a lock, they clobber each other. And an assistant that only runs when you message it is only half alive. We will fix both problems.
Locks and schedules side by side
Per-session locks serialize turns. A daemon thread runs scheduled tasks as their own isolated sessions.
import threading
session_locks: dict[str, threading.Lock] = {}
locks_guard = threading.Lock()
def get_lock(user_id: str) -> threading.Lock:
with locks_guard:
if user_id not in session_locks:
session_locks[user_id] = threading.Lock()
return session_locks[user_id]
def run_agent_turn_safe(user_id: str, text: str) -> str:
with get_lock(user_id):
return run_agent_turn(user_id, text)One lock per session, created lazily. The guard lock is just to make the dictionary update safe across threads.
import schedule, time
def setup_heartbeats():
def daily_brief():
run_agent_turn_safe(
user_id='cron:daily-brief',
text='Summarize what I worked on yesterday and suggest today\'s focus.',
)
schedule.every().day.at('08:00').do(daily_brief)
def scheduler_loop():
while True:
schedule.run_pending()
time.sleep(30)
t = threading.Thread(target=scheduler_loop, daemon=True)
t.start()Scheduled tasks get their own session id so they stay isolated from user conversations. A daemon thread runs the schedule loop in the background.
A global lock serializes every user in the world behind each other. Two unrelated people cannot hold a conversation at the same time. Per-session locks let different users talk in parallel while keeping each individual session consistent. It is a free win.
Quiz: Quiz
Loading practice…