Skip to content

Message Flow: End-to-End โ€‹

TL;DR: User types. Socket.IO receives message:send, validates, authenticates, rate-limits, saves the user message. The server spawns the LangGraph agentic-RAG pipeline, streams tokens back as message:ai:chunk, streams stage updates as rag:stage, then emits message:ai:end with sources. Three non-blocking post-processing jobs run in parallel: title generation, memory extraction, and APDR observations.

Code path: socket.ts โ†’ ai-service.ts โ†’ langchain.ts โ†’ agentic-rag.ts.

The full sequence โ€‹

Layer boundaries โ€‹

The WebSocket handler does transport and persistence. The AIService does access control and write-through persistence. LangChainService manages LLM config and picks between agentic RAG and the (effectively unused) basic-RAG fallback. Agentic-RAG is the actual brain.

Files and responsibilities:

FileLinesRole
packages/api/src/chat/socket.ts~680WebSocket handler. Rate limit, validate, sanitise, save user message, emit events, spawn AI generation, post-processing.
packages/api/src/chat/ai-service.ts~200Thin orchestration. Access verification (IDOR), crisis helpline safety net, persist AI message, resolve school URNs to human-readable names.
packages/api/src/chat/langchain.ts~1450LLM client. Loads LLM settings from the database (60-second cache), routes between agentic and basic RAG, buffers streaming to avoid splitting security markers.
packages/api/src/chat/agentic-rag.ts~3800The graph. State definition, nine nodes, routing edges, execution. See Agentic RAG.

WebSocket events โ€‹

Client to server:

EventPayloadPurpose
message:send{ conversationId?, content, attachments? }Send a message (creates conversation if needed)
conversations:list(n/a)Request conversation list
conversation:history{ conversationId }Request message history
conversation:delete{ conversationId }Soft-delete a conversation
typing:start / typing:stop{ conversationId }Typing indicator
message:feedback{ messageId, feedback }Thumbs up (1), thumbs down (0), or clear (null)

Server to client:

EventPayloadWhen
message:created{ conversationId, message }User message saved
message:ai:start{ conversationId }AI generation about to start
message:ai:chunk{ conversationId, content }Streaming token chunk
rag:stage{ conversationId, stage }Graph stage transition (see below)
message:ai:end{ conversationId, message }AI response complete with sources[]
conversations:listConversation[]Response to list request
conversation:history{ conversationId, messages }Response to history request
conversation:deleted{ conversationId }Deletion confirmed
conversation:title:updated{ conversationId, title }Auto-generated title arrived
error{ message }Error notification

RAG stage events โ€‹

The graph emits stage updates via onStage(stage) so the UI can render a live progress indicator. The stages:

greeting
retrieve_start            retrieve_complete
transform_start           transform_complete     (only if query transform enabled)
generate_start            generate_complete
hallucination_check_start hallucination_check_complete   (only if enabled)
tool_execution_start      tool_execution_complete        (only if a tool is called)

Most production messages emit four events: retrieve_start, retrieve_complete, generate_start, generate_complete. The hallucination check and query transform are both disabled by default. See Agentic RAG.

Entry point: socket.ts โ€‹

The Socket.IO handler for message:send sits in packages/api/src/chat/socket.ts around lines 83โ€“292. It:

  1. Enforces per-user WebSocket rate limiting.
  2. Validates the payload through validateUserMessage(). 8 000-character maximum, 20+ prompt-injection regex patterns, Unicode sanitisation. See Safety & Grounding.
  3. Creates the conversation or verifies the user owns it.
  4. Persists the user message.
  5. Runs the deterministic crisis check. If the content matches the crisis regex set, the helpline block is emitted as the first message:ai:chunk before any LLM output runs. This is the safety net.
  6. Calls aiService.generateResponse(conversationId, userId, content, onStream, attachments, onStage).
  7. Emits message:ai:end with the full message and resolved source objects when the call returns.
  8. Kicks off three async post-processing jobs in parallel: title generation, memory extraction, APDR observations. None of them block the user response.

Streaming internals โ€‹

Streaming is threaded through four functions. Each takes an onStream(chunk: string) callback:

socket.ts
  โ†“ onStream callback emits socket event
ai-service.ts
  โ†“ onStream pass-through
langchain.ts
  โ†“ buffers to avoid splitting [USER_MESSAGE_START]/[USER_MESSAGE_END] markers
agentic-rag.ts
  โ†“ generate node iterates the LLM stream, calls onStream per token
Azure OpenAI

The security-marker buffer in langchain.ts matters. Token boundaries can split strings like [USER_MESSAGE_ and START], which would leak into the UI if emitted raw. The buffer accumulates until a complete marker can be detected, then strips markers before forwarding the cleaned prefix.

Non-blocking post-processing โ€‹

Three things happen in parallel after message:ai:end is emitted:

Title generation. For a brand-new conversation, a separate LLM call produces a 4โ€“6 word title. Result emitted as conversation:title:updated.

Memory extraction. Identifies durable facts about the user or child ("child has ASD diagnosis", "LA is Lancashire") and stores them in the four-tier memory system.

APDR observations. For teacher or SENCO users with a linked pupil, the system extracts graduated-approach observations (Assess, Plan, Do, Review notes) for the child's timeline.

If any of these fail, the user doesn't see it. It's logged. The user's response is already delivered.

What the user never sees โ€‹

  • The five modules assembled into the system prompt for this specific message.
  • The retrieved chunks, until they arrive as sources[] on message:ai:end.
  • The graph node transitions, apart from the coarse rag:stage events.
  • The grounding-pass hedges applied post-generation.
  • The statutory-fact mismatch metrics.

All of that is logged (with userId and conversationId, no PII) and all of it is inspectable from /admin/rag-workflow, which streams a live trace of graph execution over SSE. Admins can replay any conversation through the pipeline.

Latency budget โ€‹

Typical production happy-path, no crisis, no clarification, no tool call, no hallucination check:

Stagep50 latencyNotes
Rate limit + validate + save user message<20 msPostgres write and Redis counter
Greeting detection (regex, no LLM)<5 msSkipped entirely for non-openers
Ambiguity pre-filter (deterministic)<50 msOnly reaches the LLM classifier if patterns are ambiguous
Retrieve (embed + HNSW + BM25 + RRF)200โ€“500 msSingle round-trip for embedding, single SQL query for hybrid
Generate (streaming LLM)2โ€“5 sTime-to-first-token about 400 ms on GPT-4.1 UK South
Persist AI message + resolve URNs<50 ms
End-to-end first-token~1 s
End-to-end last-token~3โ€“5 sFor a 400-word response

Enabling the hallucination check adds 2โ€“4 seconds. Enabling query transformation on low retrieval recall adds 1โ€“2 seconds per retry (default: disabled).

Security invariants in this path โ€‹

Access control. aiService.generateResponse calls chatService.verifyConversationAccess(conversationId, userId) before anything else. Every read and write is gated on ownership. IDOR defence.

Rate limit. A WebSocket-specific limit sits alongside the HTTP rate limits. Prevents message flooding.

Input validation. validateUserMessage() rejects prompt-injection, template injection, Unicode direction overrides, and attachment mismatches.

Sanitisation. User text is wrapped with [USER_MESSAGE_START]...[USER_MESSAGE_END] markers before entering any prompt context.

Crisis safety net. Double-layered. Deterministic helpline emission from socket.ts, plus a second idempotent check in ai-service.ts before persistence. Either layer alone guarantees the helpline appears.

Further reading โ€‹

  • Prompt System. What's actually in the system prompt assembled for this message.
  • Agentic RAG. The graph nodes above, in detail.
  • Safety & Grounding. Validators, sanitisation, security markers, the crisis regex.
  • Traces. Worked examples of this flow end-to-end.

Confidential ยท Spectrum Dynamics CIC