@mastra/core
Version:
198 lines • 7.56 kB
TypeScript
import type { StorageDomains } from './base.js';
/**
* A human-friendly duration for retention policies.
*
* Either a raw number of **milliseconds**, or a string with a unit suffix:
* - `ms` milliseconds
* - `s` seconds
* - `m` minutes
* - `h` hours
* - `d` days
* - `w` weeks
*
* @example
* '30d' // 30 days
* '12h' // 12 hours
* 604800000 // 7 days in ms
*/
export type Duration = number | `${number}${'ms' | 's' | 'm' | 'h' | 'd' | 'w'}`;
/**
* Age-based retention policy for a single table.
*
* Rows whose anchor timestamp column is strictly older than `maxAge`
* (i.e. `anchor < Date.now() - maxAge`) are eligible for deletion.
*/
export interface TableRetentionPolicy {
/**
* Maximum age to keep rows for. Rows older than this are pruned.
*/
maxAge: Duration;
/**
* Rows deleted per batch (each batch is its own transaction).
* Bounds lock duration and WAL growth on large tables.
*
* @default 1000
*/
batchSize?: number;
}
/**
* Options controlling a single `prune()` invocation.
*
* These bound the work performed so a prune can never run unbounded, and let
* the caller cooperatively cancel and pace deletions against live traffic.
*
* `prune()` only deletes rows — it never reclaims disk. On SQLite/LibSQL freed
* pages are reused by future writes so the file stops growing. Handing disk
* back to the OS (e.g. `VACUUM`) is left to the underlying database and the
* operator to manage.
*/
export interface PruneOptions {
/**
* Maximum number of delete batches per table per call.
* When reached, the table's result is returned with `done: false`.
*/
maxBatches?: number;
/**
* Maximum number of rows deleted per table per call.
* When reached, the table's result is returned with `done: false`.
*/
maxRows?: number;
/**
* Delay in milliseconds between batches, to avoid starving live traffic.
*/
pauseMs?: number;
/**
* Cooperative cancellation. The batch loop checks this between batches and
* stops cleanly, returning partial results with `done: false`.
*/
signal?: AbortSignal;
}
/**
* Result of pruning a single table.
*/
export interface PruneResult {
/** Domain key the pruned table belongs to (e.g. `memory`). */
domain: string;
/** Physical table name that was pruned (e.g. `mastra_messages`). */
table: string;
/** Number of rows deleted during this call. */
deleted: number;
/**
* `false` means eligible rows remain (a cap or the abort signal stopped the
* loop early). Call `prune()` again to continue.
*/
done: boolean;
}
/**
* Per-domain map of the stable retention table keys each domain exposes.
*
* This is the source of truth for which table keys are valid under each
* domain in `RetentionConfig`. It intentionally mirrors each domain's
* `retentionTables` descriptor and is validated against it at runtime by the
* reference implementations.
*
* Only **growth tables** are listed — tables that accumulate rows unbounded as
* a side effect of normal operation (conversation history, telemetry, job/run
* records, schedule fire history, event feeds, per-thread state). User-authored
* artifacts and config (agents, skills, workspaces, prompt blocks, scorer
* definitions, mcp configs, favorites, tool connections, datasets, channel
* config, channel installations, schedule definitions) are deliberately
* excluded: they grow with user intent and are edited/deleted explicitly, so
* age-based retention does not apply.
*
* Note: for the `schedules` domain the growth table is the fire/run history
* (`schedule_triggers`, one row appended per fire), not the schedule
* definitions (`schedules`, one stable row per schedule). Hence the single
* `'triggers'` key.
*
* Note: for the `observability` domain, `spans` is supported by every adapter
* that implements retention, while `metrics` / `logs` / `scores` / `feedback`
* only exist on insert-only v-next adapters (e.g. Postgres v-next), which
* expire them by dropping whole day partitions / chunks rather than deleting
* rows. Adapters skip table keys they don't manage.
*
* Note: for the `experiments` domain, an experiment is pruned as a whole unit —
* the run and all of its `experiment_results` rows are deleted together (results
* cascade with their parent, matching `deleteExperiment`). Results have no
* independent lifespan, so they are not a separate retention key. The anchor is
* `experiments.completedAt`, so in-flight runs (NULL `completedAt`) are never
* pruned. Hence the single `'experiments'` key.
*
* Domains not listed here fall back to `never`, so no table policies can be
* set on them until they declare their retention tables.
*/
export interface DomainRetentionTables {
memory: 'threads' | 'messages' | 'resources';
threadState: 'threadState';
observability: 'spans' | 'metrics' | 'logs' | 'scores' | 'feedback';
scores: 'scorers';
workflows: 'workflowSnapshot';
backgroundTasks: 'backgroundTasks';
experiments: 'experiments';
notifications: 'notifications';
harness: 'sessions';
schedules: 'triggers';
}
/**
* The valid retention table keys for a given storage domain `D`.
*
* Resolves to the domain's declared table-key union when known, otherwise
* `never` (no table policies allowed).
*/
export type RetentionTableKey<D extends keyof StorageDomains> = D extends keyof DomainRetentionTables ? DomainRetentionTables[D] : never;
/**
* Fully-typed retention configuration.
*
* Keys are real domain keys from `StorageDomains`; values map real per-table
* keys (from that domain's `retentionTables`) to their policies. Unknown
* domains or unknown table keys are compile errors.
*
* Anything left unset is kept forever.
*
* @example
* ```typescript
* const retention: RetentionConfig = {
* memory: {
* messages: { maxAge: '30d' },
* threads: { maxAge: '90d', batchSize: 500 },
* },
* observability: {
* spans: { maxAge: '7d' },
* },
* };
* ```
*/
export type RetentionConfig = {
[D in keyof StorageDomains]?: Partial<Record<RetentionTableKey<D>, TableRetentionPolicy>>;
};
/**
* Descriptor entry for a single retention-eligible table.
*
* Each domain exposes a `retentionTables` record mapping a stable table key
* to this descriptor. It is the single source of truth for the physical table
* name, the timestamp anchor column, and whether that column is indexed
* (batched timestamp deletes are only fast with an index on the anchor).
*/
export interface RetentionTableDescriptor {
/** Physical table name (e.g. `mastra_messages`). */
table: string;
/** Anchor column used for the age comparison (e.g. `createdAt`). */
column: string;
/** Whether `column` is indexed. Unindexed anchors make batched deletes slow. */
indexed: boolean;
/**
* Storage type of the anchor column, which determines how the age cutoff is
* bound in the delete query:
* - `timestamp` (default): compared as an ISO-8601 string / `Date`.
* - `epoch-ms`: compared as a raw number of milliseconds since the Unix epoch
* (e.g. `schedules.created_at`, stored as `bigint`).
*
* @default 'timestamp'
*/
anchorType?: 'timestamp' | 'epoch-ms';
}
/**
* A domain's `retentionTables` descriptor: stable table key → descriptor.
*/
export type RetentionTablesDescriptor = Record<string, RetentionTableDescriptor>;
//# sourceMappingURL=retention.d.ts.map