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'utilisationLien 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.
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étentionLien direct vers Configuration de la rétention
Définissez le champ retention dans la configuration du Store.
retention?:
[domain]?:
memory ou observability). Associe les clés de tables de ce domaine compatibles avec la rétention à leurs politiques.TableRetentionPolicyLien direct vers TableRetentionPolicy
maxAge:
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?:
Tables compatibles avec la rétentionLien 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.
| Domaine | Clé de table | Colonne de référence | Mesure de maxAge |
|---|---|---|---|
memory | threads | createdAt | Âge du fil de discussion |
memory | messages | createdAt | Âge du message |
memory | resources | createdAt | Âge de la ressource |
threadState | threadState | updatedAt | Inactivité : l'état des fils encore actifs est conservé |
observability | spans | startedAt | Âge du span |
observability | metrics | timestamp | Âge de l'événement de métrique (v-next uniquement) |
observability | logs | timestamp | Âge de l'événement de journal (v-next uniquement) |
observability | scores | timestamp | Âge de l'événement de score (v-next uniquement) |
observability | feedback | timestamp | Âge de l'événement de feedback (v-next uniquement) |
scores | scorers | createdAt | Âge de l'enregistrement de score |
workflows | workflowSnapshot | updatedAt | Inactivité ; les Workflows suspendus ou de longue durée sont conservés |
backgroundTasks | backgroundTasks | completedAt | Temps écoulé depuis la fin ; les tâches en cours (NULL) ne sont jamais supprimées |
experiments | experiments | completedAt | Temps écoulé depuis la fin ; les expériences en cours ne sont jamais supprimées |
notifications | notifications | createdAt | Âge de la notification |
harness | sessions | createdAt | Âge de l'enregistrement de session |
schedules | triggers | actual_fire_at | Âge de l'historique des déclenchements (colonne epoch-ms) |
- La table
observational_memoryde 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é
resultsdistincte. - 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
createdAtZetcompletedAtZ). - 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 dethreadStateetharness. - 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.deletedindique le nombre de lignes des partitions supprimées.
MéthodesLien direct vers Méthodes
RétentionLien 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[]>
PruneOptionsLien direct vers PruneOptions
maxBatches?:
done: false.maxRows?:
done: false.pauseMs?:
signal?:
done: false.retention?:
retention configurée reste inchangée.PruneResultLien 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 pruneLien 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.
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 MongoDBLien 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
},
],
})
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 disqueLien 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.
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.