Tres días después de lanzar retención por niveles y búsqueda híbrida, nos topamos exactamente con el modo de fallo que justificó construir un servidor de memoria persistente: la base de datos SQLite en una máquina estaba vacía, mientras que Qdrant Cloud todavía tenía cada vector. Todo el contenido — el texto real de cada recuerdo — vivía solo en SQLite. Los vectores estaban intactos pero inútiles sin el texto que codificaban.
Ese incidente impulsó BHGBrain 1.3. Esta versión hace que el servidor de memoria sea resistente a fallos de almacenamiento, añade soporte multi-dispositivo para equipos que ejecutan BHGBrain en más de una máquina, e incluye una herramienta de recuperación ante desastres que puede reconstruir toda tu base de datos local desde el almacén de vectores compartido.
El Problema: Doble Almacenamiento, Punto Único de Fallo
La arquitectura de BHGBrain utiliza dos almacenes: SQLite para contenido, metadatos y búsqueda de texto completo; Qdrant para embeddings vectoriales y similitud semántica. Cada escritura va a ambos. Cada búsqueda se une entre ambos.
El diseño asumía que SQLite era duradero. Lo es — sql.js escribe atómicamente en disco mediante write-to-temp-then-rename. Pero si el archivo de la base de datos se pierde, se recrea o comienza fresco en una máquina diferente, todo el contenido desaparece. Qdrant tiene los embeddings y algunos metadatos (tags, tipo, importancia), pero no el texto real del recuerdo. Los resultados de búsqueda que dependen de la unión con SQLite retornan silenciosamente nada.
Esto es exactamente lo que sucedió. Dos instancias de BHGBrain — una en una estación de trabajo principal, otra en una Windows 365 Cloud PC — apuntaban al mismo clúster de Qdrant Cloud. El SQLite de la Cloud PC estaba vacío. Cada recuperación retornaba cero resultados, aunque Qdrant tenía docenas de vectores con coincidencias de alta confianza.
Solución 1: Contenido en Payloads de Qdrant
El primer cambio es directo: almacenar el contenido completo del recuerdo en el payload de Qdrant junto al vector.
Antes de 1.3, el payload de upsert de Qdrant contenía solo metadatos — namespace, type, tags, importance, retention_tier, decay_eligible, expires_at. El texto del contenido era solo de SQLite.
Ahora, writeMemory y updateMemory incluyen content, summary, category, source, created_at y device_id en cada upsert de Qdrant. Qdrant se convierte en la copia de seguridad redundante para el contenido. SQLite sigue siendo el almacén principal para consultas, búsqueda de texto completo y gestión del ciclo de vida — pero perderlo ya no significa perder datos.
Solución 2: Fallback Automático de Búsqueda
El método buildSearchResults del servicio de búsqueda solía buscar cada resultado de Qdrant en SQLite. Si un ID de recuerdo no se encontraba localmente, se descartaba. Este era el fallo silencioso que hacía tan confuso el bug — Qdrant devolvía coincidencias, pero quien llamaba veía resultados vacíos.
Ahora, cuando un resultado de Qdrant no tiene una fila SQLite correspondiente, el servicio de búsqueda construye el resultado directamente desde el payload de Qdrant:
Qdrant devuelve coincidencia → búsqueda en SQLite → ¿encontrado? → devuelve registro completo
→ ¿no encontrado? → ¿el payload tiene contenido?
→ sí → devuelve desde payload
→ no → saltar (recuerdo pre-1.3)
Esto significa que una instancia nueva de BHGBrain apuntando a un clúster Qdrant existente devolverá inmediatamente resultados de búsqueda — sin necesidad de datos SQLite. Los resultados incluyen todo lo almacenado en el payload: contenido, resumen, tipo, tags, nivel de retención, ID de dispositivo y marca de tiempo de creación.
Solución 3: La Herramienta de Reparación
Para recuperación completa — reconstruir SQLite para que la búsqueda de texto completo, la gestión del ciclo de vida y el seguimiento de accesos funcionen correctamente — ahora existe una herramienta MCP repair:
{
"tool": "bhgbrain.repair",
"params": {
"dry_run": true
}
}
La herramienta de reparación:
- Descubre todas las colecciones
bhgbrain_*en Qdrant a través de la API de colecciones - Recorre cada punto en cada colección (paginación por lotes)
- Para cada punto: verifica si el ID existe en SQLite local
- Si falta y el payload de Qdrant contiene
content: inserta unMemoryRecordcompleto en SQLite - Si falta y no hay contenido en el payload: lo salta (recuerdo pre-1.3, contenido irrecuperable)
- Reporta estadísticas: colecciones escaneadas, puntos escaneados, recuperados, saltados, errores
Usa dry_run: true para previsualizar lo que se recuperaría sin escribir nada. Usa device_id para filtrar la recuperación a recuerdos de un dispositivo específico.
Este es el camino de recuperación ante desastres. También es el camino de incorporación para nuevos dispositivos — apunta una instancia nueva de BHGBrain a tu clúster Qdrant existente, ejecuta repair, y el SQLite local se puebla con todo lo del almacén compartido.
Memoria Multi-Dispositivo
El cambio arquitectónico más grande en 1.3 es el soporte de primera clase para múltiples instancias de BHGBrain compartiendo un solo backend Qdrant.
La Arquitectura
Dispositivo A (Estación de trabajo) Dispositivo B (Cloud PC)
┌──────────────────┐ ┌──────────────────┐
│ SQLite (local) │ │ SQLite (local) │
│ device_id: ws-1 │ │ device_id: w365 │
└────────┬─────────┘ └────────┬─────────┘
│ │
└──────────┬───────────────────┘
│
┌──────────▼──────────┐
│ Qdrant Cloud │
│ (backend compartido)│
│ contenido + vectores│
│ índice device_id │
└─────────────────────┘
Cada dispositivo mantiene su propia base de datos SQLite. Qdrant es la capa compartida. Cada escritura almacena contenido en ambos almacenes y etiqueta el recuerdo con el device_id de origen.
Identidad del Dispositivo
Cada instancia resuelve un device_id estable al iniciar:
- Config explícita:
device.idenconfig.json - Variable de entorno:
BHGBRAIN_DEVICE_ID - Generado automáticamente: Derivado de
os.hostname(), en minúsculas y saneado
El ID resuelto se persiste en config.json en la primera ejecución. Aparece en cada payload de Qdrant, cada registro SQLite y cada resultado de búsqueda — para que siempre sepas qué dispositivo creó un recuerdo.
Visibilidad Cruzada entre Dispositivos
Ambos dispositivos ven todos los recuerdos. Cuando el Dispositivo B busca algo que el Dispositivo A almacenó, la búsqueda en Qdrant devuelve la coincidencia. Si el SQLite del Dispositivo B no tiene el registro, el fallback de búsqueda construye el resultado desde el payload de Qdrant. Ningún dato es invisible.
| Origen | El Dispositivo A ve | El Dispositivo B ve |
|---|---|---|
| Recuerdos del Dispositivo A (SQLite) | Registro completo | Fallback de Qdrant |
| Recuerdos del Dispositivo B (SQLite) | Fallback de Qdrant | Registro completo |
Para funcionalidad local completa (búsqueda de texto completo, seguimiento de ciclo de vida, conteos de acceso), ejecuta repair en el dispositivo para poblar su SQLite desde Qdrant.
Configuración
Apunta ambos dispositivos al mismo clúster Qdrant con diferentes IDs de dispositivo:
// Dispositivo A
{
"device": { "id": "workstation" },
"qdrant": {
"mode": "external",
"external_url": "https://your-cluster.cloud.qdrant.io",
"api_key_env": "QDRANT_API_KEY"
}
}
// Dispositivo B
{
"device": { "id": "cloud-pc" },
"qdrant": {
"mode": "external",
"external_url": "https://your-cluster.cloud.qdrant.io",
"api_key_env": "QDRANT_API_KEY"
}
}
Eso es todo. Ambas instancias comparten el mismo pool de memoria. Ambas etiquetan sus escrituras con procedencia. Ambas pueden ver todo.
Documentación: 8 Diagramas Mermaid
El README ahora incluye diagramas Mermaid detallados que cubren cada subsistema principal:
- Arquitectura — diagrama de componentes mostrando el stack completo del servidor
- Topología Multi-Dispositivo — Qdrant compartido con SQLite local por dispositivo
- Pipeline de Escritura — diagrama de flujo completo de decisión de deduplicación
- Asignación de Niveles — lógica de clasificación ordenada por prioridad
- Ciclo de Vida de Niveles — diagrama de estados con promociones, TTLs y flujo de caducidad
- Búsqueda Híbrida — rutas semánticas/texto completo paralelas a través de fusión RRF
- Copia de Seguridad y Restauración — diagrama de secuencia para flujos de creación y restauración
- Flujo de Reparación — diagrama de flujo de recuperación ante desastres
Las cuatro traducciones (Alemán, Español, Francés, Chino Simplificado) están actualizadas con paridad total con el README en inglés, incluyendo todos los diagramas y la nueva sección multi-dispositivo.
Actualizando a 1.3
No se requiere migración manual. En el primer inicio después de la actualización:
- SQLite gana una columna
device_idnullable (los recuerdos existentes permanecennull) - Las colecciones de Qdrant obtienen un índice de palabras clave
device_id - La configuración recibe un campo
device.idauto-resuelto desde el hostname - Todas las nuevas escrituras almacenan contenido en payloads de Qdrant
Los recuerdos pre-1.3 sin contenido en Qdrant continúan funcionando normalmente a través de SQLite. Simplemente no pueden recuperarse mediante repair si SQLite se pierde — el contenido no estaba en Qdrant cuando fueron escritos.
La Conclusión
La lección más grande del incidente de SQLite: si tu sistema tiene dos almacenes, y uno de ellos se degrada silenciosamente, necesitas que el otro tome el relevo sin intervención del operador. BHGBrain 1.3 hace de Qdrant la red de seguridad para SQLite y viceversa. La búsqueda cae automáticamente. La recuperación es una sola llamada a una herramienta. La compartición multi-dispositivo funciona porque ambos almacenes contienen la imagen completa.
Si estás ejecutando BHGBrain en más de una máquina, actualiza a 1.3 y ejecuta repair en cada dispositivo. Si lo estás ejecutando en una sola máquina, actualiza de todas formas — el cambio de contenido-en-Qdrant significa que tu próximo incidente de SQLite será un no-evento en lugar de una pérdida de datos.
BHGBrain es código abierto, licencia MIT, y está disponible en github.com/Big-Hat-Group-Inc/BHGBrain.
Kevin Kaminski es principal en Big Hat Group, enfocado en infraestructura de IA empresarial, Microsoft 365 y Windows 365. Construye herramientas de código abierto para equipos que ejecutan agentes de IA en el trabajo.