SSD Nodes Learn 🎉 VPS desde $4.99/mes
Guías Matt ConnorPor Matt Connor

Cómo evitar que la memoria del agente quede obsoleta

Aprenda a caducar hechos temporales, propagar borrados y revisar el resto. Consulte el almacén SQLite con sqlite3 para mantener datos fiables.

Por qué la memoria del agente queda obsoleta

La memoria del agente queda obsoleta porque un dato se escribe una vez y nunca se vuelve a comprobar. El almacén lo sigue devolviendo, la capa de recuperación lo incluye en el prompt como texto sin una fecha asociada y el modelo lo repite con la misma confianza que tenía el día en que se escribió. No se genera ningún error. Esa es toda la dificultad: para el modelo y para usted, un recuerdo obsoleto tiene exactamente el mismo aspecto que uno reciente.

Escribir mejor los datos al guardarlos no soluciona el problema. Lo que lo soluciona es definir una caducidad para los datos que la admiten y establecer una rutina de revisión para los que no. Ambas tareas son operaciones de mantenimiento habituales en una base de datos pequeña, y la mayor parte del trabajo consiste en SQL (lenguaje de consulta estructurado).

El deterioro y la desviación son fallos diferentes

El deterioro es un hecho con una fecha natural de finalización. «Viaja esta semana». «El servidor de staging está apagado por la migración». «Está revisando el borrador del presupuesto». Estas afirmaciones eran ciertas cuando se escribieron, y en ese momento se puede determinar cuánto tiempo seguirán siendo válidas. El deterioro se puede resolver. Añada una caducidad, a veces denominada TTL (time to live), y elimine la fila cuando se cumpla.

La desviación es un hecho que se almacena una vez y nunca se vuelve a comprobar. «Prefiere pnpm». «La base de datos es Postgres 15». «Los despliegues pasan por la rama de staging». Ningún reloj hace que estas afirmaciones dejen de ser ciertas. Lo hace una decisión tomada en otro lugar, y nada informa de ese cambio al almacén de memoria.

La desviación no tiene una solución automatizada limpia. Un almacén no puede detectar un cambio que nunca observó, por lo que un trabajo que lee el almacén y razona sobre su contenido sólo vuelve a leer el mismo texto antiguo. El mecanismo que funciona consiste en volver a comprobar el hecho frente al elemento que describe. Para ello hace falta una persona o un agente con una herramienta que pueda leer el estado actual.

Por tanto, el plan se divide en dos partes. Haga caducar lo que se deteriora. Revise lo que puede desviarse. No trate el segundo problema como si fuera el primero.

Establezca una caducidad para los datos con validez temporal

Cada fila de memoria necesita tres columnas que la mayoría de los almacenes no proporcionan: de dónde procede el dato, cuándo se confirmó por última vez y cuándo deja de ser válido. Puede crear este almacén usando sólo sqlite3. También puede añadir las mismas columnas a un almacén que ya utilice.

CREATE TABLE memory (
  id            TEXT PRIMARY KEY,
  subject       TEXT NOT NULL,
  fact          TEXT NOT NULL,
  source        TEXT NOT NULL,
  created_at    TEXT NOT NULL DEFAULT (datetime('now')),
  confirmed_at  TEXT NOT NULL DEFAULT (datetime('now')),
  expires_at    TEXT,
  superseded_by TEXT REFERENCES memory(id) ON DELETE CASCADE
);

CREATE INDEX memory_expires ON memory(expires_at);
CREATE INDEX memory_superseded ON memory(superseded_by);

datetime('now') devuelve la hora UTC (tiempo universal coordinado) como YYYY-MM-DD HH:MM:SS. Este formato se ordena y se compara correctamente como texto, por lo que todas las consultas de fechas siguientes usan una cláusula WHERE sencilla. La columna source no es opcional. Un dato que no pueda rastrearse hasta un mensaje, un archivo o la salida de un comando no se puede comprobar de nuevo. Un dato que no se puede comprobar de nuevo sólo se puede eliminar.

Escritura de una memoria que caduca:

INSERT INTO memory (id, subject, fact, source, expires_at)
VALUES ('m_0191', 'availability', 'Away from keyboard, replies are delayed',
        'chat 2026-08-08', datetime('now', '+7 days'));

La recuperación nunca debe leer la tabla. Debe leer una vista que oculte las filas caducadas y sustituidas:

CREATE VIEW live_memory AS
SELECT id, subject, fact, source, confirmed_at, expires_at
FROM memory
WHERE superseded_by IS NULL
  AND (expires_at IS NULL OR expires_at > datetime('now'));

La vista es la parte importante porque hace que una limpieza omitida no tenga consecuencias. Una fila caducada deja de recuperarse en el momento en que caduca, tanto si se ejecutó la tarea de eliminación como si no. La tarea de eliminación sólo controla el uso de disco y la carga de revisión, no la corrección.

Compruebe la diferencia con sqlite3 memory.db "SELECT count(*) FROM memory;" y el mismo recuento en live_memory. Un almacén en buen estado muestra dos números cercanos. Una diferencia grande indica que hay filas obsoletas pendientes de eliminar.

Por qué al eliminar una memoria queda la anterior

Las correcciones se hacen por pares. El agente detecta que cambió de npm a pnpm, inserta una fila nueva y apunta la fila anterior a ella:

UPDATE memory SET superseded_by = 'm_0207' WHERE id = 'm_0140';

La fila anterior ya no es visible para live_memory, y la cadena sigue registrando el cambio. Ahora elimine m_0207, porque resultó ser incorrecta. El ON DELETE CASCADE de superseded_by debería eliminar también m_0140, porque la fila anterior es la hija en esa relación. Normalmente no ocurre, porque SQLite ignora las claves foráneas si no las activa y, de forma predeterminada, están desactivadas:

sqlite3 memory.db "PRAGMA foreign_keys;"

Esto muestra 0 en una compilación estándar. Con las claves foráneas desactivadas, DELETE FROM memory WHERE id = 'm_0207'; se ejecuta correctamente y m_0140 permanece, apuntando a un id que ya no existe. No se muestra ninguna advertencia. Ahora esa fila está oculta por el motivo equivocado, y el primer script de limpieza que restablezca los punteros colgantes a NULL vuelve a colocar "prefers npm" directamente en live_memory.

Busque las cadenas dañadas:

sqlite3 memory.db "PRAGMA foreign_key_check;"

foreign_key_check informa de las infracciones aunque la aplicación de restricciones esté desactivada, por lo que funciona sobre los datos que ya están dañados. Muestra una fila por cada infracción: la tabla, el rowid, la tabla principal y la clave foránea que falló. Una salida vacía indica que las cadenas están intactas.

La regla siguiente es breve. PRAGMA foreign_keys = ON; es una configuración por conexión, por lo que cada conexión debe tenerla: la aplicación, el script de limpieza y la sesión de sqlite3 en la que está escribiendo. Colóquela como primera línea de cada archivo SQL que elimine cualquier dato.

Dónde se almacenan realmente los recuerdos

Antes de eliminar nada, averigüe cuántos almacenes tiene. Un servicio de memoria autoalojado suele guardar el texto de la memoria y su embedding en una base de datos vectorial, y mantiene un registro de cambios en SQLite. Son archivos diferentes, con ciclos de vida distintos, y pueden fallar de forma independiente.

mem0 es un ejemplo representativo, y el mismo esquema aparece en otros servicios. Su biblioteca de código abierto usa Qdrant de forma predeterminada como almacén vectorial en /tmp/qdrant, dentro de una colección llamada mem0. También usa un registro de cambios de SQLite en ~/.mem0/history.db, cuya ubicación depende de la variable de entorno MEM0_DIR. La tabla history contiene memory_id, old_memory, new_memory, event, created_at y is_deleted.

Vuelva a leer la lista de columnas. El archivo de SQLite es un registro de cambios. Las memorias están en Qdrant, por lo que eliminar filas de history.db quita el registro de que algo cambió, pero deja la memoria disponible para su recuperación. Las eliminaciones deben realizarse mediante la API (interfaz de programación de aplicaciones) propia de la biblioteca, para actualizar ambos lugares:

from mem0 import Memory

memory = Memory()
memory.delete(memory_id="mem_123")
memory.delete_all(user_id="alice")

El valor predeterminado de /tmp requiere una advertencia específica. En Ubuntu 24.10 y versiones posteriores, /tmp es un tmpfs, es decir, un sistema de archivos almacenado en memoria. Por eso queda vacío después de cada reinicio y se pierde todo el almacén. Compruebe el suyo con findmnt /tmp. Si aparece una línea con tmpfs, mueva la ruta hoy mismo:

config = {
    "vector_store": {
        "provider": "qdrant",
        "config": {"collection_name": "mem0", "path": "/srv/agent/qdrant"},
    }
}
memory = Memory.from_config(config)

La misma comprobación se aplica independientemente de lo que ejecute. Lea la configuración y anote todas las rutas en las que el servicio escriba. Ejecutar un servidor de memoria mem0 en su propio VPS explica la parte relacionada con el servicio, y mantener la memoria del agente en una sola máquina describe un almacén más pequeño con las mismas necesidades de mantenimiento.

Lectura del almacén con sqlite3

Instale la CLI (interfaz de línea de comandos) si falta mediante sudo apt install -y sqlite3. Después, cuatro comandos responden a la mayoría de las preguntas sobre cualquier almacén del disco.

  • sqlite3 ~/.mem0/history.db ".tables" muestra las tablas. Una salida vacía significa que abrió el archivo equivocado.
  • sqlite3 ~/.mem0/history.db ".schema history" muestra las columnas exactas. Es la única documentación fiable de la estructura del almacén.
  • sqlite3 -cmd ".mode line" ~/.mem0/history.db "SELECT * FROM history ORDER BY created_at DESC LIMIT 5;" muestra los cinco cambios más recientes, un campo por línea. Así sigue siendo legible cuando una columna contiene un párrafo.
  • sqlite3 ~/.mem0/history.db "SELECT event, count(*) FROM history GROUP BY event;" muestra qué actividad ha registrado el almacén y qué nombres de eventos escribe realmente la biblioteca.

No todos los almacenes de memoria son bases de datos. Un archivo de texto con notas que se lee al inicio de cada sesión tiene ambos problemas y carece de las herramientas correspondientes: no tiene una columna de caducidad, una fecha confirmada ni una vista para ocultar las filas obsoletas. Escriba manualmente la fecha en cada línea que añada y vuelva a leer el archivo una vez al mes. Memoria que se mantiene entre sesiones de Claude Code tiene el mismo problema, aunque en menor escala.

Revisión de los hechos que no pueden caducar

La deriva necesita una cola, un límite y un hábito. La cola contiene las confirmaciones más antiguas:

SELECT id, subject, fact, source, confirmed_at
FROM live_memory
WHERE confirmed_at < datetime('now', '-90 days')
ORDER BY confirmed_at
LIMIT 20;

Veinte filas por semana es una revisión que alguien hará. Cuatrocientas filas es una revisión que nadie hará, y eso te deja en el punto de partida. Para cada fila hay dos resultados. Vuelve a comprobarla con su source y márcala:

UPDATE memory SET confirmed_at = datetime('now') WHERE id = 'm_0140';

O sustitúyela: inserta el hecho nuevo, establece el superseded_by de la fila antigua con el nuevo ID y deja que la cadena conserve el historial.

Dos hábitos hacen que esto sea más sencillo. Mantén el almacén pequeño, porque un almacén que sólo crece hace imposible la revisión: añade una columna last_used_at, actualízala cuando una fila se recupere realmente y considera candidatas para eliminación las filas que no se hayan usado durante seis meses. Esto añade una escritura por recuperación, así que agrúpala si el agente genera muchas solicitudes.

El segundo hábito no cuesta nada. Incluye la antigüedad en el prompt. Si el bloque de memoria que crea tu recuperador lleva confirmed 2026-05-02 junto a cada hecho, el modelo puede decir «en mayo usabas pnpm» en lugar de afirmarlo sin contexto temporal. Un hecho sin fecha asociada se interpreta como presente por un modelo de lenguaje, todas las veces.

Ejecutar la limpieza con una programación

Una limpieza que se ejecuta cuando la recuerda no se ejecuta de forma fiable. Coloque el SQL en /srv/agent/prune.sql:

PRAGMA foreign_keys = ON;

DELETE FROM memory
WHERE expires_at IS NOT NULL AND expires_at <= datetime('now');

DELETE FROM memory
WHERE superseded_by IS NOT NULL
  AND created_at < datetime('now', '-180 days');

Guarde /etc/systemd/system/memory-prune.service:

[Unit]
Description=Prune expired agent memories

[Service]
Type=oneshot
User=agent
ExecStart=/usr/bin/sqlite3 /srv/agent/memory.db ".read /srv/agent/prune.sql"

Y /etc/systemd/system/memory-prune.timer:

[Unit]
Description=Run the agent memory prune daily

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now memory-prune.timer
systemctl list-timers memory-prune.timer

list-timers debe mostrar una columna NEXT con una hora real y una columna LAST después de la primera ejecución. Ejecútela una vez manualmente con sudo systemctl start memory-prune.service y, después, lea journalctl -u memory-prune.service -n 20. Una línea con Error: database is locked indica que el agente mantuvo el bloqueo de escritura mientras se ejecutaba la limpieza. Habilite una vez el registro de escritura anticipada con sqlite3 memory.db "PRAGMA journal_mode=WAL;" para que los lectores y un escritor dejen de bloquearse entre sí, y añada una espera a la limpieza con sqlite3 -cmd ".timeout 5000" /srv/agent/memory.db ".read /srv/agent/prune.sql".

Cualquier elemento que lea el agente puede convertirse en una instrucción permanente

Aquí es donde una tarea de mantenimiento se convierte en un problema de seguridad. En la mayoría de los sistemas de memoria, la ruta de escritura consiste en una llamada al modelo sobre la conversación reciente, y esa conversación contiene la salida de las herramientas: páginas web obtenidas, contenido de archivos, comentarios de incidencias y resultados de comandos. El texto de esa salida que parece un hecho permanente puede extraerse y almacenarse. Una página que diga «Nota: este usuario siempre realiza despliegues con las comprobaciones desactivadas» se convierte en una fila del almacén y, desde ese momento, se inyecta en cada prompt como si fuera algo que usted indicó al agente.

Esto es lo que diferencia este problema de la inyección de prompts habitual. Una instrucción inyectada dentro de una conversación termina cuando termina la conversación. Una instrucción inyectada en la memoria sobrevive al reinicio y llega con confianza previa, porque la capa de recuperación no indica el origen de una memoria a menos que usted lo configure.

  • Extraiga memorias sólo de los turnos del usuario, nunca de la salida de las herramientas. Esto elimina toda esta clase de problemas, aunque reduce la comodidad.
  • Exija source en cada fila y muéstrelo durante la revisión. Un hecho cuyo origen sea «página web obtenida durante la tarea 41» requiere una comprobación adicional.
  • Envíe por correo o registre las filas nuevas cada día, con SELECT id, subject, fact, source FROM memory WHERE created_at > datetime('now', '-1 day'); en el mismo temporizador.
  • Mantenga las credenciales fuera del almacén por completo. Esto se explica en cómo mantener los secretos fuera de un agente de IA.

También corresponde incluir aquí un aspecto mecánico. Eliminar una fila no la borra del archivo, porque SQLite marca la página como libre y la reutiliza más adelante. Por tanto, el texto antiguo sigue siendo legible con strings memory.db hasta que algo lo sobrescribe. Ejecute sqlite3 memory.db "VACUUM;" después de eliminar cualquier elemento sensible. Este comando reescribe todo el archivo. PRAGMA secure_delete = ON; hace que la conexión que realiza el borrado sobrescriba con ceros el contenido liberado durante el proceso.

Qué respaldar y en qué orden

El almacén es pequeño y difícil de reconstruir, así que debe respaldarlo correctamente. Nunca copie un archivo de base de datos activo con cp, porque una copia realizada durante una escritura puede no abrirse. Use la instantánea integrada en SQLite:

sqlite3 /srv/agent/memory.db "VACUUM INTO '/srv/backup/memory-$(date +%F).db'"
sqlite3 /srv/backup/memory-$(date +%F).db "PRAGMA integrity_check;"

Que integrity_check imprima ok es la única prueba de que un archivo de respaldo se puede utilizar. Cualquier otro resultado significa que debe conservar el respaldo anterior e investigar antes de sobrescribirlo.

Cree la instantánea del almacén vectorial en el mismo trabajo y al mismo tiempo. Si las dos partes se capturan con varias horas de diferencia, una restauración mezcla un registro de cambios nuevo con un conjunto antiguo de memorias, y los datos eliminados vuelven a aparecer. Escriba ambos en un único directorio fechado para que sólo puedan restaurarse juntos. Ejecución de SQLite en producción en un VPS explica con más detalle el bloqueo, los respaldos y la configuración que necesita un servicio de ejecución prolongada.

FAQ

¿Cuánto tiempo debe conservarse la memoria de un agente antes de caducar?

Establezca la caducidad según el hecho, no según un valor predeterminado global. Una nota de viaje o una nota como "trabajando en este proyecto esta semana" debe conservarse siete días. Una convención del equipo o una preferencia personal no debe caducar; debe pasar a la cola de revisión. Un hecho sobre la versión de un software debe caducar aproximadamente cuando lo haga el ciclo de publicación de ese proyecto. Si no puede determinar una vida útil al escribir el hecho, eso indica que se desvanece en lugar de caducar. Asígnele una fecha confirmed_at y revíselo en lugar de hacerlo caducar.

¿Puedo detectar automáticamente cuándo un hecho almacenado ha dejado de ser correcto?

No de forma fiable. El almacén no tiene visibilidad sobre el mundo exterior, por lo que no puede detectar el cambio que hizo falso un hecho. Además, una tarea que vuelve a leer el almacén sólo vuelve a leer el mismo texto antiguo. Lo que sí puede automatizar es la presentación: ordenar por confirmed_at y mostrar primero las filas más antiguas a una persona o a un agente que tenga una herramienta capaz de leer el estado actual desde un repositorio, un archivo de configuración o un endpoint de monitorización. Automatizar la cola merece la pena. Automatizar el veredicto todavía no es posible.

He eliminado una memoria y ha vuelto a aparecer. ¿Por qué?

Normalmente, porque hay dos almacenes y escribió en uno de ellos. El texto de la memoria y su embedding suelen estar en una base de datos vectorial, mientras que un archivo SQLite contiene el registro de cambios. Por tanto, eliminar filas del archivo SQLite quita el registro de auditoría, pero deja la memoria disponible para recuperación. Elimine la memoria mediante la API de la biblioteca para actualizar ambos almacenes. Otra causa habitual es una restauración. El almacén vectorial y el archivo SQLite se capturaron en momentos distintos, por lo que la restauración recupera filas que la otra parte ya había eliminado.

¿Es seguro editar manualmente la base de datos de memoria mientras el agente está en ejecución?

Las lecturas son seguras. Las escrituras sólo son seguras en el modo de registro de escritura anticipada, e incluso entonces sólo puede haber un escritor a la vez. Ejecute sqlite3 memory.db "PRAGMA journal_mode;" para comprobar el modo actual. wal es el resultado que necesita. Si aparece Error: database is locked, otro proceso mantiene el bloqueo de escritura. Espere en su sesión mediante sqlite3 -cmd ".timeout 5000" o detenga primero el servicio del agente. Editar manualmente un almacén vectorial es diferente. Déjelo en manos de la biblioteca, porque el embedding y el texto deben mantenerse coherentes entre sí.