Repository navigation
Message Flow
Canonical source:
docs/03-architecture/message-flow.md— this page is rendered from it on every publish; edit the source.
This document traces the complete lifecycle of a user message from the moment it is sent in the frontend to the final audit log entry. The flow involves several services, 3 communication patterns (HTTP, RabbitMQ, SSE), and multiple async pipelines.
User Frontend Nginx Chat Svc Routing Svc Ollama Svc Provider
| | | | | | |
|--send msg--->| | | | | |
| |--POST----->| | | | |
| | |--forward-->| | | |
| | | | | | |
| | | |--store USER--| | |
| | | | message | | |
| | | | | | |
| | | |--publish---->| | |
| | | | msg.created | | |
| | | | | | |
| | | | |--route req->| |
| | | | | (if AUTO) | |
| | | | |<-decision---| |
| | | | | | |
| | | |<--publish----| | |
| | | | msg.routed | | |
| | | | | | |
| | | |--assemble context-------->| |
| | | | (memories, files, packs) | |
| | | | | | |
| | | |--execute-----|-------------|---------->|
| | | | | | (or Ollama)
| | | |<-------------|-------------|--response-|
| | | | | | |
| | | |--store ASST--| | |
| | | | message | | |
| | | | | | |
| |<===========|============|--SSE done----| | |
|<--render-----| | | | | |
| | | | | | |
| | | |--publish---->| | |
| | | | msg.completed| | |
| | | | | | | |
| | | | +----->Memory Svc | |
| | | | +----->Audit Svc | |
Actor: Frontend (Next.js) Action: User types a message and presses send.
The frontend sends a POST request to the chat-messages endpoint:
POST /api/v1/chat-messages
Authorization: Bearer <jwt>
Content-Type: application/json
{
"threadId": "uuid",
"content": "Explain the observer pattern",
"provider": null, // optional: force specific provider
"model": null, // optional: force specific model
"fileIds": ["uuid", ...], // optional: attached files
"quotes": [{ "sourceMessageId": "uuid", "text": "..." }], // optional: ADR-131
"contextPackIds": ["uuid"] // optional: attached context packs
}
The frontend also opens (or maintains) an SSE connection to receive the response:
GET /api/v1/chat-messages/stream?threadId=<uuid>
Service: chat-service (port 4002) Action: Validate input, create USER message, publish event.
-
ChatMessageController.create()validates the request via Zod DTO -
ChatMessageService.create()stores the USER message in theChatMessagetable - The service attaches any referenced files via
MessageAttachmentrecords - Publishes
message.createdevent to RabbitMQ:
Exchange: claw.events
Routing key: message.created
Payload: {
messageId, threadId, userId, content,
forcedProvider, forcedModel, fileIds,
threadRoutingMode, threadPreferredProvider, threadPreferredModel
}
Service: routing-service (port 4004) Action: Consume event, determine optimal provider, publish decision.
The routing service consumes message.created and executes the routing pipeline:
- Load active routing policies (sorted by priority)
- Determine routing mode (thread setting or system default)
- Execute the appropriate routing strategy (see Routing Engine)
- For AUTO mode: call Ollama router model with structured output (Zod schema)
- If Ollama times out (10s): fall back to heuristic rules
- Record the routing decision in
RoutingDecisiontable - Publish
message.routedevent:
Exchange: claw.events
Routing key: message.routed
Payload: {
messageId, threadId,
selectedProvider, selectedModel,
confidence, reasonTags, privacyClass, costClass,
fallbackProvider, fallbackModel
}
Service: chat-service (port 4002) Action: Build the complete prompt with all context sources.
The ContextAssemblyManager orchestrates context gathering:
-
Fetch memories (HTTP GET to memory-service)
- User-scoped memories, limit 20
- Types: FACT, PREFERENCE, INSTRUCTION, SUMMARY
- Only enabled memories included
-
Fetch context pack items (HTTP GET to memory-service)
- For each context pack attached to the thread
- Items sorted by
sortOrder
-
Fetch file chunks (HTTP GET to file-service)
- For each file attached to the message
- Chunks returned in order by
chunkIndex
-
Retrieve thread history (local database query)
- Recent messages from the thread
- Ordered chronologically
-
Build prompt structure:
[System Prompt] <- thread.systemPrompt or default [Memories] <- user memories as context [Context Pack Items] <- knowledge base items [File Chunks] <- attached file content [Thread History] <- previous messages [Current User Message] <- the new message -
Token budget truncation
- Keeps the head (system prompt, memories, current message)
- Drops from the tail (oldest history messages) if over budget
- Respects the thread's
maxTokenssetting
Service: chat-service (port 4002) Action: Call the selected provider with fallback chain.
The ChatExecutionManager handles execution:
-
Primary attempt: Call the selected provider/model
- If provider is
local-ollama: HTTP POST to ollama-service (port 4008) - If provider is cloud (OpenAI, Anthropic, Google, DeepSeek, xAI): HTTP POST to connector-service (port 4003), which proxies to the cloud API
- If provider is
-
Fallback chain (if primary fails):
Primary provider (from routing) --> Fallback provider (from routing decision) --> Local Ollama gemma3:4b (always available) -
Error conditions that trigger fallback:
- HTTP timeout (configurable per provider)
- 429 rate limit response
- 500/502/503 from provider
- Network connectivity failure
- Invalid response format
-
Response capture:
- Content (full text)
- Input token count
- Output token count
- Latency in milliseconds
- Actual provider and model used (may differ from selected if fallback occurred)
Service: chat-service (port 4002) Action: Persist the AI response.
-
Create ASSISTANT
ChatMessagerecord:-
role: ASSISTANT -
content: full response text -
provider: actual provider used -
model: actual model used -
routingMode: mode that was active -
inputTokens,outputTokens,latencyMs: performance metrics -
metadata: JSON with routing decision details, fallback info
-
-
Update thread metadata:
-
lastProvider,lastModel: track what was last used -
updatedAt: timestamp
-
Service: chat-service (port 4002) Action: Notify the frontend that the response is ready.
The chat service emits an SSE event to the connected frontend:
event: message.done
data: {
"messageId": "uuid",
"threadId": "uuid",
"content": "The observer pattern is...",
"provider": "anthropic",
"model": "claude-sonnet-4",
"inputTokens": 1250,
"outputTokens": 430,
"latencyMs": 2340,
"routingMode": "AUTO",
"confidence": 0.92,
"reasonTags": ["coding", "explanation"],
"fallbackUsed": false
}
Nginx is configured for SSE passthrough:
proxy_buffering offX-Accel-Buffering: no-
proxy_read_timeout 86400s(24 hours) Connection: keep-alive
The frontend receives the event, updates the message list via TanStack Query cache invalidation, and renders the response in the MessageBubble component with provider badge, token counts, and routing transparency details.
Service: chat-service (port 4002) Action: Notify downstream services.
Exchange: claw.events
Routing key: message.completed
Payload: {
messageId, threadId, userId,
userContent, // original user message
assistantContent, // AI response
provider, model, routingMode,
inputTokens, outputTokens, latencyMs,
fallbackUsed, fallbackProvider, fallbackModel
}
This event fans out to multiple consumers asynchronously.
Service: memory-service (port 4005)
Trigger: Consumes message.completed event
- Receive the user message and assistant response
- Call Ollama (memory extraction model, default
gemma3:4b) with a structured extraction prompt - Ollama returns extracted facts in a Zod-validated schema:
[ { "type": "FACT", "content": "User is learning design patterns" }, { "type": "PREFERENCE", "content": "User prefers code examples in TypeScript" } ] -
Deduplication check: Compare each extracted memory against existing memories for the user
- Skip if a substantially similar memory already exists
- Store new
MemoryRecordentries:-
userId: from the event -
type: FACT / PREFERENCE / INSTRUCTION / SUMMARY -
content: extracted text -
sourceThreadId,sourceMessageId: provenance tracking -
isEnabled: true by default
-
- Publish
memory.extractedevent for audit trail
Service: audit-service (port 4007)
Trigger: Consumes message.completed event
-
Create
AuditLogentry:-
action: MESSAGE_COMPLETED -
entityType: ChatMessage -
entityId: messageId -
severity: INFO -
details: provider, model, token counts, latency
-
-
Create
UsageLedgerentry:-
resourceType: TOKEN -
action: CONSUME -
quantity: inputTokens + outputTokens -
unit: TOKENS -
metadata: provider, model, userId
-
| Step | Error Scenario | Handling |
|---|---|---|
| 1. Frontend POST | Network failure | Retry with exponential backoff, show error toast |
| 2. Chat stores message | Database error | BusinessException, 500 response, no event published |
| 3. Routing | Ollama timeout | Heuristic fallback (no user-visible delay beyond 10s) |
| 3. Routing | No healthy providers | Return local-ollama as default, low confidence |
| 4. Context assembly | Memory service unavailable | Continue without memories, log warning |
| 4. Context assembly | File service unavailable | Continue without file chunks, log warning |
| 5. LLM execution | Primary provider fails | Automatic fallback to secondary, then local Ollama |
| 5. LLM execution | All providers fail | BusinessException with clear error message to user |
| 6. Store response | Database error | Log error, SSE error event to frontend |
| 7. SSE delivery | Connection dropped | Frontend reconnects, polls for missed messages |
| 8. Memory extraction | Ollama unavailable | Event remains in RabbitMQ, retried 3x with backoff |
| 8. Audit logging | MongoDB unavailable | Event goes to DLQ after 3 retries |
| Phase | Typical Duration | Notes |
|---|---|---|
| Frontend to Nginx | < 5ms | Local network |
| Nginx to chat-service | < 5ms | Docker network |
| Store USER message | 5-20ms | PostgreSQL write |
| RabbitMQ publish + consume | 10-50ms | Async, non-blocking |
| Routing decision (AUTO) | 500ms-10s | Ollama inference, 10s timeout |
| Context assembly | 50-200ms | Parallel HTTP calls |
| LLM execution (local) | 1-30s | Depends on model size and prompt length |
| LLM execution (cloud) | 500ms-60s | Depends on provider and model |
| Store ASSISTANT message | 5-20ms | PostgreSQL write |
| SSE delivery | < 5ms | Already connected |
| Memory extraction | 2-15s | Async, not user-blocking |
| Audit logging | 5-20ms | Async, MongoDB write |
Total user-perceived latency: Routing (up to 10s) + LLM execution (0.5-60s) = typically 2-15 seconds for most requests.
ClawAI Wiki · source-controlled under wiki/ · generated/mirrored from the current repository knowledge layer.
ClawAI Wiki
Product
- Product-Vision
- Flagship-Features
- Business-Overview
- Requirements-Register
- Drift-Log
- Runtime-Managed-Connector-Providers
- Threads
- ADR-Index
Architecture
- System-at-a-Glance
- Architecture-Map
- System-Architecture
- Message-Flow
- End-to-End-Data-Flow
- Routing-Engine
- Event-Bus
- Security-Architecture
Platform
- Backend-Services
- Frontend-Architecture
- Shared-Packages
- Database-Reference
- API-Reference
- Provider-Catalog
- Memory-and-Context-Architecture
- File-Support-Architecture
- Research-and-Web-Crawling
Runtime & Ops
- Docker-Guide
- Nginx-Reference
- CI-CD-Pipeline
- Logging-and-Observability
- Operational-Runbooks
- Troubleshooting
Quality
Engineering OS
- AI-Native-Engineering-OS
- Generated-Manifests
- Rules-Catalog
- Skills-Catalog
- Context-Layer
- Engineering-Memory
- Tooling-and-Automation
- Scripts-and-Operations
- ESLint-and-Architecture-Enforcement
- Extended-Work-Skills
Agents
Reference