ai-chat · architecture

How a conversation travels through ai-chat

Three services, one database, one Redis, and a queue between every hop. This page walks every flow step by step with real payloads, and lets you play with the parts that are easiest to get wrong: how typing becomes turns, how the prompt is built, and how sessions end.

01The system

Click any box for what it does and where its code lives. Highlighted paths in the flows below are drawn on this map.

ai-chat service queue (Redis/BullMQ or SQS) data store yours / external

02Flows, step by step

Pick a flow, then step through it (or press play). Each step lights up the hops on the map above and shows what actually crosses the wire.

    03Typing becomes turns

    People send a thought in several quick messages. Answering each one separately produces replies that talk over each other, so the client SDK batches them into one turn. Drag the settings and add messages to see where the batch closes. These are the agent's Client & pacing settings.

    04How the prompt is built

    chat-expert assembles the system prompt in a fixed order: everything that is the same for every conversation of an agent first, then per-conversation material, then per-request instructions. Providers cache repeated prompt prefixes, so this order makes the static part (often most of the tokens, once knowledge is attached) cheap after the first turn. Toggle the layers to see how the cacheable share moves.

    Cacheable prefix

    Example request (one turn)

    
        

    05How sessions live and end

    Each agent sets a session length. The timer starts when the conversation opens and never pauses. Reopening a live conversation continues it; reopening an ended one shows it read-only. Choose a configuration to see the lifecycle.

    
        

    06The guarantees, and what provides them

    ProblemMechanism
    A resent turn creates duplicatesThe client resends the identical payload until acknowledged; the queue deduplicates on conversationId:clientTurnId:attempt for 5 minutes, and uq(conversation_id, client_message_id) makes the insert a no-op.
    A redelivered reply is shown twiceReply bubbles are stored under a deterministic turn id (r-<turnId>); uq(conversation_id, turn_id, part_index) rejects the second insert. Reveal is a compare-and-set PENDING → SENT.
    Two replies generated at once ignore each otherAt most one generation per conversation (inflight_dispatch_id claim). Messages arriving meanwhile are answered together afterwards, with the first reply in history.
    Turns of one conversation processed out of orderOrdered queues: each message takes a per-conversation sequence number; a worker runs N only after N−1 (Redis driver), or SQS FIFO groups processed strictly sequentially (SQS driver).
    A goodbye lands above the answer it should followThe end waits for an in-flight generation, and a part is never revealed while an earlier part is still pending.
    A late reply appears under the goodbyeResponses whose dispatch no longer holds the claim are dropped as stale. Ending the conversation clears the claim.
    A lost generation leaves the user waiting foreverThe sweeper releases claims older than GENERATION_TIMEOUT_MS, marks the turn failed and emits turn.failed. The user retries with retryAttempt + 1.
    A lost reveal job blocks the transcriptOverdue pending parts are rescheduled, and after 10 minutes marked FAILED so later parts can go.
    Frames sent while the user was offline are missedPub/sub is at-most-once on purpose. MySQL is the source of truth, and the client resyncs with ?after=<newest id> on every connect. Offline replies can also go out as push (FCM or a webhook).
    Client reaches another user's conversationThe session token names one conversation. Every client route and socket channel takes the conversation from the token, never from input.
    A deploy drops in-flight workSIGTERM drains queue consumers and sockets (server.shutdown tells clients to reconnect elsewhere). Anything unacknowledged is redelivered.