Saltar al contenido principal

0008 — La memoria de los bots vive en Postgres

Estado: aceptada · Fecha: 2026-09-19

Contexto​

La memoria de cada conversación con un bot vivía en MemorySaver, en la RAM del proceso: el historial, las variables, por dónde va el flujo y un "Esperar respuesta" pendiente. La documentación de LangGraph lo presenta como solo para desarrollo, y en la práctica:

  • cada deploy o reinicio la borraba, así que todas las conversaciones volvían a empezar desde Inicio y el bot saludaba otra vez;
  • no se compartía entre instancias, con lo que dos réplicas del backend no sabrían la una de la otra;
  • crecía sin límite en RAM, porque guarda una copia por paso del grafo.

Con la reanudación (ADR 0006) esto pasó a ser grave: un "Esperar respuesta" puede estar pendiente durante horas.

Decisión​

  1. @langchain/langgraph-checkpoint-postgres con un único checkpointer compartido (BotCheckpointerService). Los hilos van por <botId>:<conversationId>.

  2. Esquema propio langgraph, cerrado (20261020000003_langgraph_checkpoints.sql):

    • fuera de los esquemas que expone PostgREST;
    • sin USAGE para anon, authenticated ni service_role;
    • RLS como segunda barrera.

    Los checkpoints contienen el texto de las conversaciones, que es PHI. El backend entra por conexión directa (LANGGRAPH_CHECKPOINT_DATABASE_URL).

  3. Las tablas las crea la migración, no el setup() del paquete. Así nacen cerradas y revisadas. setup() solo registra sus versiones, porque todas sus sentencias son IF NOT EXISTS.

  4. Un checkpoint por turno (durability: 'exit') en lugar de uno por paso del grafo.

  5. Poda diaria con langgraph.prune_checkpoints:

    • borra las conversaciones inactivas más de 90 días;
    • en las vivas deja los últimos 20 checkpoints y los blobs que siguen referenciados;
    • no toca hilos con actividad en la última hora, para no chocar con un turno en curso.

    Hace falta porque cada turno guarda el historial entero.

  6. Si Postgres no está, el bot sigue funcionando. Sin la variable, o si setup() falla, se usa MemorySaver y queda registrado en el log.

Alternativas descartadas​

  • Redis (@langchain/langgraph-checkpoint-redis). Exige Redis Stack (RedisJSON y RediSearch) en lugar del redis:7-alpine actual, y persistencia AOF bien configurada para no perder memoria en un reinicio de Redis. Postgres ya tiene copias de seguridad y está en el flujo de migraciones.
  • El esquema public, que es el valor por defecto del paquete. Lo expondría PostgREST y dependería de que cada GRANT estuviera bien.
  • Recortar el historial en el estado (RemoveMessage) en lugar de podar checkpoints. Cambia lo que ve el modelo, y la poda de checkpoints ya contiene el crecimiento sin tocar la conversación.

Consecuencias​

  • Hay que poner LANGGRAPH_CHECKPOINT_DATABASE_URL en el .env de cada servidor y aplicar antes la migración (ver Despliegue).
  • La prueba bot-checkpointer.e2e.spec.ts corre el PostgresSaver real contra un Postgres desechable (en local, PGlite por socket), con LANGGRAPH_E2E_DATABASE_URL. Comprueba que el saludo no se repite tras un reinicio, que una espera pendiente sobrevive y que podar un hilo vivo no lo rompe.