People text in bursts. A real conversation looks like this:Documentation Index
Fetch the complete documentation index at: https://docs.photon.codes/docs/llms.txt
Use this file to discover all available pages before exploring further.
Drain in the handler, not the enqueuer
The single most important rule: the messages stay in the queue table until the handler reads them. Don’t pull them into the job payload at enqueue time. Why: if the batch-flush job gets cancelled before the handler runs, anything in the payload is lost. Anything still in the queue is naturally picked up by the next batch. Keeping the data in the queue table until the last possible moment makes cancellation a non-event for those messages. If the flush job is cancelled between steps F and G, the rows stay inbatch_queue and the next incoming message picks them up.
Carry-forward
Sometimes the handler does drain the queue but is then cancelled mid-generation. Those messages are now in memory inside a cancelled job - they’d be lost on the floor. The fix is acarried_messages table. When a job is cancelled after draining, write the drained messages there. The next batch’s handler reads from carried_messages first and prepends them as [Earlier message] ... lines so the model sees them as historical context, not as fresh input.
In-flight cancellation
When a new message arrives and you have a job in flight (reading, generating, or sending), you need to stop it. Two pieces:- A cancellation flag in a per-chat
in_flighttable. The enqueuer setscancelled_atand callsboss.cancel(jobId). - Polling inside the handler. The send stage in particular polls
cancelled_atevery 500ms and aborts via anAbortController.
cancelled_at against the chain’s own chainStartedAt timestamp, not against “is the flag set.” Otherwise a stale flag from a prior cancelled chain orphans the new one. The flag is per-chain, not per-chat.