Aller au contenu principal

Rétention du Storage

Par défaut, le Storage croît sans limite. La rétention est un système de nettoyage facultatif fondé sur l'âge : vous déclarez des politiques maxAge par table dans la configuration retention, puis appelez storage.prune() pour supprimer les lignes plus anciennes que l'âge configuré. Tout ce que vous ne configurez pas est conservé indéfiniment ; aucun comportement ne change donc tant que vous n'activez pas cette fonctionnalité.

prune() supprime des lignes. Cette méthode limite la croissance et peut être exécutée en toute sécurité sur de grandes tables (par lots, bornée, reprenable et annulable). Elle ne récupère jamais l'espace disque : sur SQLite/libSQL, les pages libérées sont réutilisées par les écritures ultérieures, ce qui arrête la croissance du fichier, mais la restitution de l'espace disque au système d'exploitation (par exemple au moyen d'un VACUUM) reste à la charge de la base de données sous-jacente et de l'opérateur.

La rétention couvre uniquement les tables de croissance : les tables qui accumulent des lignes sans limite du fait de leur fonctionnement normal (historique des conversations, télémétrie, enregistrements de Jobs et de Runs, historique des déclenchements planifiés, flux d'événements). Les artefacts et la configuration créés par les utilisateurs (Agents, Skills, Workspaces, blocs de prompts, datasets, définitions de planifications, installations de Channels, etc.) croissent selon l'intention de l'utilisateur et sont modifiés ou supprimés explicitement ; ils ne constituent donc pas des clés de rétention valides.

Les implémentations de référence sont libSQL, PostgreSQL et MongoDB. Les autres adaptateurs conservent les lignes indéfiniment jusqu'à ce qu'ils implémentent la rétention.

Exemple d'utilisation
Lien direct vers Exemple d'utilisation

Déclarez retention sur n'importe quel MastraCompositeStore (ou sur un adaptateur qui l'étend, tel que LibSQLStore), puis appelez prune() depuis votre propre planificateur.

src/mastra/index.ts
import { LibSQLStore } from '@mastra/libsql'

const storage = new LibSQLStore({
id: 'mastra-storage',
url: 'file:./mastra.db',
retention: {
memory: {
messages: { maxAge: '30d' },
threads: { maxAge: '90d', batchSize: 500 },
},
observability: {
spans: { maxAge: '7d' },
},
},
})

// Wire this to your own cron/scheduler: Mastra never runs it for you.
const results = await storage.prune()

retention est entièrement typé. Les clés doivent correspondre à de véritables clés de domaine et chaque clé de table doit être déclarée par le domaine comme compatible avec la rétention. La transmission directe de l'objet dans la configuration d'un Store déclenche sa vérification de type ; si vous le construisez séparément, utilisez satisfies RetentionConfig afin que les domaines ou tables inconnus produisent des erreurs de compilation :

import type { RetentionConfig } from '@mastra/core/storage'

const retention = {
memory: {
messages: { maxAge: '30d' }, // ok
bogus: { maxAge: '30d' }, // Error: not a memory retention table
},
bogusDomain: {}, // Error: not a storage domain
} satisfies RetentionConfig

Configuration de la rétention
Lien direct vers Configuration de la rétention

Définissez le champ retention dans la configuration du Store.

retention?:

RetentionConfig
Politiques d’âge par domaine et par table. Les domaines et tables non définis sont conservés indéfiniment.
RetentionConfig

[domain]?:

Record<TableKey, TableRetentionPolicy>
Clé réelle d'un domaine de Storage (par exemple, memory ou observability). Associe les clés de tables de ce domaine compatibles avec la rétention à leurs politiques.

TableRetentionPolicy
Lien direct vers TableRetentionPolicy

maxAge:

Duration
Âge maximal de conservation des lignes. Les lignes dont l'horodatage de référence est strictement antérieur à Date.now() - maxAge peuvent être supprimées. Un nombre représente des millisecondes ; une chaîne peut comporter un suffixe d'unité : ms, s, m, h, d, w (par exemple, '30d' ou '12h').

batchSize?:

number
= 1000
Nombre de lignes supprimées par lot. Chaque lot possède sa propre transaction, ce qui limite la durée du verrouillage et la croissance du WAL sur les grandes tables.

Tables compatibles avec la rétention
Lien direct vers Tables compatibles avec la rétention

Chaque domaine déclare les tables pouvant être nettoyées selon leur âge et la colonne d'horodatage servant de référence à la comparaison. Cette référence est choisie pour que maxAge corresponde au sens attendu pour les données concernées. Les journaux en ajout seul utilisent l'heure de création et l'état actif utilise la dernière activité. Les Jobs et les Runs utilisent l'heure de fin ; les travaux en cours ne sont donc jamais supprimés.

DomaineClé de tableColonne de référenceMesure de maxAge
memorythreadscreatedAtÂge du fil de discussion
memorymessagescreatedAtÂge du message
memoryresourcescreatedAtÂge de la ressource
threadStatethreadStateupdatedAtInactivité : l'état des fils encore actifs est conservé
observabilityspansstartedAtÂge du span
observabilitymetricstimestampÂge de l'événement de métrique (v-next uniquement)
observabilitylogstimestampÂge de l'événement de journal (v-next uniquement)
observabilityscorestimestampÂge de l'événement de score (v-next uniquement)
observabilityfeedbacktimestampÂge de l'événement de feedback (v-next uniquement)
scoresscorerscreatedAtÂge de l'enregistrement de score
workflowsworkflowSnapshotupdatedAtInactivité ; les Workflows suspendus ou de longue durée sont conservés
backgroundTasksbackgroundTaskscompletedAtTemps écoulé depuis la fin ; les tâches en cours (NULL) ne sont jamais supprimées
experimentsexperimentscompletedAtTemps écoulé depuis la fin ; les expériences en cours ne sont jamais supprimées
notificationsnotificationscreatedAtÂge de la notification
harnesssessionscreatedAtÂge de l'enregistrement de session
schedulestriggersactual_fire_atÂge de l'historique des déclenchements (colonne epoch-ms)
remarque
  • La table observational_memory de Memory ne possède aucun horodatage de référence ; elle ne peut donc pas être nettoyée selon son âge et ne constitue pas une clé de rétention valide.
  • Les expériences sont supprimées comme des unités entières : les lignes de résultats d'une expérience ancienne sont supprimées avec elle (les résultats sont supprimés en cascade avec leur parent), afin qu'un Run ne reste jamais partiellement supprimé. La rétention ne possède pas de clé results distincte.
  • Pour schedules, la table de croissance est l'historique des déclenchements (schedule_triggers, une ligne par déclenchement) : les définitions de planifications sont de la configuration et ne sont pas supprimées.
  • Sur PostgreSQL, les horodatages de référence utilisent les colonnes miroirs sensibles au fuseau horaire (par exemple createdAtZ et completedAtZ).
  • LibSQL et PostgreSQL prennent en charge tous les domaines ci-dessus à l'exception de harness, que PostgreSQL n'implémente pas. MongoDB les prend tous en charge à l'exception de threadState et harness.
  • Le domaine Observability v-next de PostgreSQL stocke les événements de signaux dans des tables partitionnées par jour (spans, metrics, logs, scores, feedback). Pour ce domaine, prune() supprime des partitions quotidiennes entières (ou des chunks TimescaleDB) entièrement antérieures à la date limite au lieu de supprimer des lignes : le niveau de détail effectif est d'un jour et une partition est supprimée uniquement lorsque sa journée entière est antérieure à maxAge. PruneResult.deleted indique le nombre de lignes des partitions supprimées.

Méthodes
Lien direct vers Méthodes

Rétention
Lien direct vers Rétention

prune(options?)
Lien direct vers pruneoptions

Supprime les lignes plus anciennes que leur maxAge configuré dans chaque domaine possédant une politique dans retention. Renvoie un PruneResult par table traitée. Si aucune valeur retention n'est configurée, la méthode ne fait rien et renvoie [].

prune() est conçue pour s'exécuter en toute sécurité sur des tables contenant des millions de lignes. Elle effectue la suppression par chunks bornés et regroupés en lots (chaque lot possède sa propre transaction), sans verrouillage prolongé ni gonflement du journal des transactions. Elle n'exécute jamais de VACUUM.

Transmettez options.retention pour remplacer les politiques configurées uniquement pour cet appel : par exemple, pour ignorer un domaine (conserver l'historique des conversations) ou effectuer un nettoyage plus agressif que la configuration permanente. La valeur retention configurée du Store reste inchangée.

Les index des colonnes de référence sont créés à la demande lors du premier appel à prune() pour chaque table possédant une politique (jamais pendant init()). Les déploiements qui ne configurent pas la rétention ne subissent donc aucun coût supplémentaire d'écriture d'index ou d'espace disque. Le premier nettoyage d'une grande table existante implique la création ponctuelle de l'index. Les nettoyages suivants réutilisent cet index.

const results = await storage.prune({
maxRows: 50_000, // cap work this call
pauseMs: 50, // breathe between batches
})

for (const r of results) {
console.log(`${r.domain}.${r.table}: deleted ${r.deleted}, done=${r.done}`)
}

// One-off pass with different policies (configured retention untouched):
await storage.prune({
retention: {
observability: { spans: { maxAge: '1d' } },
},
})

Renvoie : Promise<PruneResult[]>

PruneOptions
Lien direct vers PruneOptions

maxBatches?:

number
Nombre maximal de lots de suppression par table et par appel. Lorsque cette limite est atteinte, le résultat de la table est renvoyé avec done: false.

maxRows?:

number
Nombre maximal de lignes supprimées par table et par appel. Lorsque cette limite est atteinte, le résultat de la table est renvoyé avec done: false.

pauseMs?:

number
Délai en millisecondes entre les lots afin de ne pas priver le trafic actif de ressources.

signal?:

AbortSignal
Annulation coopérative. La boucle de traitement vérifie ce signal entre les lots et s'arrête proprement en renvoyant des résultats partiels avec done: false.

retention?:

RetentionConfig
Remplace les politiques de rétention configurées du Store uniquement pour cet appel, par exemple afin d'ignorer un domaine ou d'effectuer un nettoyage plus agressif. La valeur retention configurée reste inchangée.
PruneResult
Lien direct vers PruneResult

Chaque résultat décrit la progression d'une table :

interface PruneResult {
domain: string // e.g. 'memory'
table: string // physical table name, e.g. 'mastra_messages'
deleted: number // rows deleted during this call
done: boolean // false => eligible rows remain; call prune() again
}

Exécution planifiée de prune
Lien direct vers Exécution planifiée de prune

prune() ne possède aucun planificateur intégré : vous décidez de son exécution. Comme son travail est borné, un seul appel peut ne pas tout supprimer. Lorsqu'un résultat contient done: false, il reste des lignes admissibles et vous devez rappeler la méthode au prochain tick. Chaque invocation reste ainsi courte et un backlog important peut être résorbé sur plusieurs Runs.

// Runs on your own cron (node-cron, a workflow schedule, an external job, etc.).
async function retentionTick() {
const results = await storage.prune({ maxRows: 100_000, pauseMs: 25 })
const incomplete = results.filter(r => !r.done)
if (incomplete.length) {
// Rows remain; the next scheduled tick will continue where this one stopped.
console.log(
'retention still draining:',
incomplete.map(r => `${r.domain}.${r.table}`),
)
}
}

Vous pouvez également annuler un nettoyage de longue durée avec un AbortSignal : la boucle s'arrête entre les lots et renvoie des résultats partiels avec done: false, ce qui permet au Run suivant de reprendre proprement.

Index TTL MongoDB (solution alternative à prune)
Lien direct vers Index TTL MongoDB (solution alternative à prune)

MongoDB fournit des index TTL (Time-To-Live) natifs qui suppriment automatiquement les documents expirés sans nécessiter d'appels manuels à prune(). Cette fonctionnalité de la base de données s'exécute dans un thread d'arrière-plan.

Quand utiliser TTL ou prune()

Utilisez les index TTL MongoDB lorsque :

  • Vous souhaitez une suppression automatique sans maintenance
  • Vos périodes de rétention sont fixes (par exemple, « toujours 30 jours »)
  • Vous préférez les solutions natives de la base de données

Utilisez prune() lorsque :

  • Vous avez besoin d'un contrôle précis du moment de la suppression
  • Vous souhaitez limiter le débit de suppression pendant les heures ouvrées
  • Vous avez besoin d'opérations de nettoyage reprenables et annulables
  • Vous utilisez un Storage composite avec plusieurs bases de données

Les deux approches sont valides. TTL est plus simple, tandis que prune() offre davantage de contrôle.

Configuration des index TTL sur MongoDB
Lien direct vers Configuration des index TTL sur MongoDB

Les index TTL fonctionnent sur les champs de date. MongoDB vérifie l'index toutes les 60 secondes et supprime les documents pour lesquels le champ de date + la durée TTL est antérieur à l'heure actuelle.

import { MongoDBStore } from '@mastra/mongodb'

const storage = new MongoDBStore({
id: 'mongodb-storage',
uri: process.env.MONGODB_URI!,
dbName: process.env.MONGODB_DB_NAME!,
indexes: [
// Messages expire after 30 days
{
collection: 'mastra_messages',
keys: { createdAt: 1 },
options: { expireAfterSeconds: 30 * 24 * 60 * 60 }, // 30 days
},
// Threads expire after 90 days
{
collection: 'mastra_threads',
keys: { createdAt: 1 },
options: { expireAfterSeconds: 90 * 24 * 60 * 60 }, // 90 days
},
// Spans expire after 7 days
{
collection: 'mastra_ai_spans',
keys: { startedAt: 1 },
options: { expireAfterSeconds: 7 * 24 * 60 * 60 }, // 7 days
},
],
})
astuce

Les index TTL suppriment les documents peu après leur expiration (le thread d'arrière-plan s'exécute environ toutes les 60 secondes), mais le moment exact n'est pas garanti. Pour un nettoyage précis et immédiat, utilisez plutôt prune().

Récupération de l'espace disque
Lien direct vers Récupération de l'espace disque

prune() supprime des lignes, mais ne réduit pas le fichier de base de données. Sur SQLite/libSQL, les pages libérées sont placées dans une liste libre et réutilisées par les écritures ultérieures ; le fichier cesse donc de croître. Pour la plupart des utilisateurs, cela suffit à résoudre le problème de croissance illimitée.

La restitution de cet espace libre au système d'exploitation constitue une opération distincte que Mastra ne gère pas. Si vous devez spécifiquement réduire le fichier, exécutez vous-même la compaction de la base de données sous-jacente (par exemple, VACUUM sur une instance libSQL auto-hébergée) pendant une fenêtre de maintenance. Un VACUUM complet verrouille le fichier et nécessite environ deux fois sa taille en espace disque libre. Sur PostgreSQL, autovacuum récupère automatiquement les tuples morts afin de les réutiliser ; un VACUUM FULL manuel est uniquement nécessaire si vous devez rendre l'espace disque au système d'exploitation.

Pour MongoDB, les documents supprimés sont réutilisés par les insertions ultérieures. Pour récupérer l'espace disque, exécutez db.runCommand({ compact: "collection_name" }) pendant une fenêtre de maintenance.

libSQL et Turso

Turso Cloud gère la compaction du Storage pour vous ; aucune récupération manuelle n'est donc nécessaire. Cela s'applique uniquement aux fichiers libSQL auto-hébergés.