Chat Lifecycle
Conversation Model
Every conversation is a tree of messages connected by parent_id. The client tracks the “active leaf” — the currently selected branch tip — per conversation in activeMessageId.
interface ChatMessage {
id: string
role: 'user' | 'assistant' | 'system'
content: string
sources?: SourceChunk[]
created_at: number
parent_id?: string // enables tree branching
}
interface SendMessageRequest {
message: string
config?: { temperature?, max_tokens?, use_rag?, model? }
parent_id?: string // parent in message tree
user_message_id?: string // pre-assigned ID for optimistic update
assistant_message_id?: string // pre-assigned ID for the response
regenerate_user_msg_id?: string // for regeneration flow
}Chat Store State Machine
Anonymous Trial & Free Tier Limit
Unauthenticated guests can test chat without signing up, bounded by a daily message limit (default 5 free messages):
- Server-side enforcement: Evaluated at
chat_quota_middlewarevia an atomic 24-hour sliding Redis window (anon_budget_allow) with PostgreSQLip_usagefallback. - Credit Transition: When an anonymous user exhausts their trial messages, the API responds with
402 Payment Required({"signup_required": true}). Upon registration, new users receive 10 free platform credits ($0.01 value) and transition into the centralized per-token credit billing model.
Streaming vs Non-Streaming
sendMessage(content, useStream?) selects the endpoint based on the useStream flag:
| Flow | Endpoint | Response format |
|---|---|---|
| Streaming | POST /chat/conversations/{id}/stream | SSE (text/event-stream) |
| Non-streaming | POST /chat/conversations/{id}/messages | JSON MessageResponse |
The streaming flow uses an AbortController stored in the Zustand store. stopGeneration() calls abortController.abort(), which closes the SSE connection.
SSE Chat Sequence
Message Branching (Edit & Regenerate)
editMessage(messageId, newContent): Creates a new conversation branch:
- Calls
createNewConversation()— creates a sibling fork - Copies messages up to
messageIdinto the new conversation - Sends
newContentas the new user message withparent_idset appropriately
regenerateLastResponse(): Re-sends the last user message:
- Identifies the last
userrole message - Calls
sendMessage()withregenerate_user_msg_idset to that message’s ID
switchBranch(messageId): Sets activeMessageId for the current conversation, causing the UI to render the message tree from that leaf node upward.
Title Generation
After a successful message exchange, if useAiTitleGeneration is true, the store calls:
POST /chat/conversations/{id}/generate-title
Body: { user_message: string, assistant_message: string }This triggers the Intelligence service to generate a concise title via the LLM. If useAiTitleGeneration is false, a simple title heuristic is used client-side (first N characters of the first user message).
Conversation List Pagination
fetchConversations(reset?) supports cursor-based pagination:
reset = truefetches page 1 (cursor = null)- Subsequent calls pass
nextCursorfrom the previous response - Conversations are sorted by
updated_at DESC totalConversationsCountis tracked for infinite scroll triggering