Skip to content
C.W.K.
Stream
Lesson 04 of 08 · published

Streaming Guard — Chat ↔ Cron Turns, Per Conversation

~9 min · heartbeat, streaming-guard

Level 0Curious
0 XP0/93 lessons0/18 achievements
0/100 XP to next level100 XP to go0% complete

Where the collision actually is

Dad's chat and a scheduled cron turn can both want to write into the same conversation. If they do it at the same time they share one JSONL file, one SDK subprocess, and one SQLite row being written — that is the real collision. It is a per-conversation risk, so the guard is per conversation too.

Lightweight mutual exclusion, one conversation at a time

  • A stream counter per conversation tracks Dad's live streams.
  • A flag per conversation marks that a scheduled turn owns it right now.
  • Chat waits for that conversation's flag to clear before starting, so a cron turn already firing there usually finishes first. The wait is bounded at 30 seconds: past that, Dad's turn goes ahead anyway with the cron turn still running, because a scheduled job may not hold him up indefinitely.
  • A chat-mode cron turn resolves its target conversation up front and defers only if Dad is streaming in that conversation.
  • A fallback-mode cron has no conversation at all and never takes part.
  • Both sides check before proceeding, not during — no lock contention.

Why not global

The first version asked one global question: is anything streaming? It looks safer and it is not. With a global gate, any long chat Dad had open made every cron defer — weather, comment checks, soul wakes — until they hit their retry ceiling and were abandoned. Nothing failed loudly. Scheduled work simply stopped happening while he talked. Narrowing the guard to the conversation that actually collides fixed it without giving up any safety.

Principle: Scope a guard to the thing that actually collides. A lock wider than the real conflict does not buy extra safety — it buys starvation, and starvation is quiet.

Why a counter and not asyncio.Lock

Single-user system. The question is always "is Dad active in this conversation?" — a boolean per key. A lock would imply contested concurrency, which is not what is happening here.

Light does not mean loose. The counter goes up before a stream starts and comes down in a finally on every exit path; the flag is cleared even when the turn fails. One leaked count and every later turn believes forever that someone is still using the conversation.

The two directions are deliberately different. Dad's chat may wait briefly for a scheduled turn that is already mid-flight; a scheduled turn yields to Dad and tries again later. The priority is set by what the product is for — the conversation he is having now.

And lighter than a lock is not the same as race-free. Test the ordering of the check and the flip inside one event loop, and check that errors and cancellations never leave a flag behind. If the process topology ever grows more workers, a boolean contract may stop being enough; today it is the smallest tool that fits.

The guard protects more than resources. A background reply landing in the middle of Dad's live exchange, or overwriting the avatar's state, breaks the turn-taking of a conversation. The technical mutex is keeping conversational manners too.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Pippa— warm💛 by Ttori— warm

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.