# Data Model: Streaming híbrido en chat de conversación

**Feature**: `003-chat-streaming`

## Overview

No se añaden tablas en v1. El streaming opera sobre entidades existentes (`ChatMessage`, sesión por usuario) y eventos efímeros en tránsito (SSE).

## Entities

### ChatSession (existente)

| Attribute | Description |
|-----------|-------------|
| `session_id` | `api-user-{user_id}` — agrupa mensajes del operador |
| `user_id` | Implícito vía JWT |

**Behavior**: Sin cambios. `clearConversation` borra todos los `ChatMessage` de la sesión (FR-010).

### ConversationMessage (existente — `chat_messages`)

| Field | Type | Notes |
|-------|------|-------|
| `id` | int | PK; devuelto en evento `done` |
| `session_id` | string | FK lógica a sesión |
| `role` | enum | `user` \| `assistant` |
| `content` | text | Contenido **completo** final |
| `meta` | json nullable | Adjuntos en mensajes user |
| `created_at` | timestamp | Orden cronológico (FR-009) |

**Streaming rules**:
- User message: INSERT al **inicio** del stream.
- Assistant message: INSERT solo tras stream **exitoso** con contenido acumulado.
- Historial GET `/chat/conversation`: siempre mensajes completos (nunca estado parcial).

### StreamEvent (efímero — no persistido)

Unidad emitida en el canal SSE. Serializada como JSON en campo `data` de cada frame SSE.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | yes | `status` \| `chunk` \| `done` \| `error` |
| `phase` | string | if status | `analyzing` \| `querying` \| `generating` |
| `message` | string | if status/error | Texto UI en español |
| `content` | string | if chunk | Fragmento de respuesta assistant |
| `assistant_id` | int | if done | ID del registro persistido |
| `user_content` | string | if done (opcional) | Eco del mensaje user guardado |

## State Machine (stream de un turno)

```mermaid
stateDiagram-v2
    [*] --> PersistUser
    PersistUser --> EmitAnalyzing: user saved
    EmitAnalyzing --> Processing: status analyzing
    Processing --> EmitQuerying: agent_database query
    Processing --> EmitGenerating: before LLM stream
    EmitQuerying --> EmitGenerating: query done
    EmitGenerating --> StreamingChunks: chatStream
    StreamingChunks --> StreamingChunks: chunk
    StreamingChunks --> PersistAssistant: done
    PersistAssistant --> [*]
    Processing --> EmitError: failure
    EmitQuerying --> EmitError: failure
    StreamingChunks --> EmitError: failure
    EmitError --> [*]: no assistant persist
```

## Validation Rules

| Rule | Source |
|------|--------|
| Stream body MUST have non-empty `message` | FR-001 |
| Stream MUST reject requests with attachment (use sync endpoint) | FR-006 |
| `content` acumulado MUST equal persisted assistant `content` | SC-003 |
| `error.message` MUST NOT contain SQL, stack traces, tool names | FR-008 |

## Relationships

```mermaid
erDiagram
    User ||--o| ChatSession : owns
    ChatSession ||--o{ ConversationMessage : contains
    StreamEvent }o--|| ConversationMessage : "done creates assistant"
```

`StreamEvent` no se almacena; solo media la creación del `ConversationMessage` assistant.
