# Research: Conversaciones múltiples del asistente con contexto

**Feature**: `004-chat-conversations`  
**Date**: 2026-05-25

## R1 — Modelo de sesión: ¿tabla nueva o ampliar `session_id`?

**Decision**: Nueva tabla `chat_conversations` con FK `chat_messages.conversation_id`; eliminar `session_id` tras migración.

**Rationale**: `session_id` string (`api-user-{id}`) no permite múltiples hilos ni metadatos (título, `updated_at` para ordenar listado). Una FK entera simplifica autorización y queries.

**Alternatives considered**:
- *Prefijo en session_id* (`api-user-1-thread-2`): frágil, sin título, difícil listar.
- *JSON en User*: no escala, sin integridad referencial.

---

## R2 — API: rutas anidadas vs. header `X-Conversation-Id`

**Decision**: Rutas REST anidadas `/chat/conversations/{id}/message(/stream)`.

**Rationale**: Explícito en URL, cacheable, fácil de autorizar en route model binding, alineado con Constitution V (contrato claro). El frontend ya usa `environment.apiUrl` por recurso.

**Alternatives considered**:
- *Header en endpoints actuales*: menos cambio de rutas pero fácil olvidar en cliente; peor trazabilidad en logs.
- *Query param*: mismo problema; peor para caches intermedios.

---

## R3 — Conversaciones vacías

**Decision**: `POST /chat/conversations` crea fila con título placeholder «Nueva conversación»; si no recibe mensajes y el usuario cambia de hilo, eliminar en `selectConversation` cuando `messages_count === 0`.

**Rationale**: Cumple spec (no listar hilos vacíos abandonados) manteniendo UX de «Nueva conversación» instantánea con id estable para stream.

**Alternatives considered**:
- *Solo id en cliente hasta primer mensaje*: complica stream/autorización sin id servidor.
- *Persistir vacías siempre*: viola Assumptions del spec.

---

## R4 — Servicio de historial compartido

**Decision**: `ConversationHistoryService` central con métodos `recentMessages()` y `toLlmMessageArray()`.

**Rationale**: Evita duplicar lógica en OpenAI, classifier y DB handler; un solo lugar para ventana de 24 mensajes y fix duplicado streaming.

**Alternatives considered**:
- *Copiar query en cada handler*: viola VIII Simplicidad; riesgo de inconsistencia.

---

## R5 — Contexto en `AgentDatabaseQueryCapabilityHandler`

**Decision**: Turno 1 (selección herramienta): incluir historial reciente. Turno 2 (redacción): incluir último intercambio assistant con datos. Persistir `meta.query` en mensaje assistant.

**Rationale**: Resuelve SC-002 (seguimientos «¿y en Barcelona?») sin re-ejecutar SQL ciegamente cuando el follow-up es interpretativo.

---

## R6 — Migración de datos existentes

**Decision**: Schema + comando artisan idempotente `chat:migrate-legacy-sessions`.

**Rationale**: Cumple FR-007; re-ejecutable en staging.

---

## R7 — Rutas legacy `/chat/conversation`

**Decision**: Mantener GET/DELETE una release como proxy a conversación más reciente; frontend migra en mismo PR.

**Rationale**: Reduce riesgo en despliegues parciales.

---

## R8 — Restaurar conversación activa (FR-010)

**Decision**: `sessionStorage` clave `assistant_active_conversation_id`; fallback a `updated_at` máximo.

**Rationale**: Sin backend extra; coherente con Constitution VI.

---

## R9 — Autorización

**Decision**: `findOwnedOrFail()` → 404 uniforme para ids ajenos.

**Rationale**: Anti-enumeración (FR-009).

---

## R10 — Título automático (FR-012)

**Decision**: Primer mensaje user con texto → `Str::limit(trim($text), 60)`. Solo adjunto → «Conversación con adjunto».

**Rationale**: Spec assumption; visible en sidebar.
