@mastra/pg 1.14.3 → 1.15.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +181 -0
  2. package/dist/docs/SKILL.md +2 -1
  3. package/dist/docs/assets/SOURCE_MAP.json +1 -1
  4. package/dist/docs/references/docs-memory-semantic-recall.md +8 -0
  5. package/dist/docs/references/docs-memory-storage.md +6 -0
  6. package/dist/docs/references/docs-memory-working-memory.md +2 -0
  7. package/dist/docs/references/docs-rag-overview.md +2 -0
  8. package/dist/docs/references/docs-rag-retrieval.md +2 -0
  9. package/dist/docs/references/docs-rag-vector-databases.md +2 -0
  10. package/dist/docs/references/reference-memory-memory-class.md +3 -0
  11. package/dist/docs/references/reference-processors-message-history-processor.md +3 -0
  12. package/dist/docs/references/reference-processors-semantic-recall-processor.md +3 -0
  13. package/dist/docs/references/reference-processors-working-memory-processor.md +3 -0
  14. package/dist/docs/references/reference-rag-metadata-filters.md +2 -0
  15. package/dist/docs/references/reference-storage-composite.md +2 -0
  16. package/dist/docs/references/reference-storage-dynamodb.md +2 -0
  17. package/dist/docs/references/reference-storage-postgresql.md +2 -0
  18. package/dist/docs/references/reference-storage-retention.md +180 -0
  19. package/dist/docs/references/reference-tools-vector-query-tool.md +2 -0
  20. package/dist/docs/references/reference-vectors-pg.md +2 -0
  21. package/dist/index.cjs +932 -53
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.js +933 -54
  24. package/dist/index.js.map +1 -1
  25. package/dist/shared/config.d.ts +8 -1
  26. package/dist/shared/config.d.ts.map +1 -1
  27. package/dist/storage/db/index.d.ts +51 -0
  28. package/dist/storage/db/index.d.ts.map +1 -1
  29. package/dist/storage/domains/background-tasks/index.d.ts +19 -1
  30. package/dist/storage/domains/background-tasks/index.d.ts.map +1 -1
  31. package/dist/storage/domains/datasets/index.d.ts +6 -7
  32. package/dist/storage/domains/datasets/index.d.ts.map +1 -1
  33. package/dist/storage/domains/experiments/index.d.ts +39 -5
  34. package/dist/storage/domains/experiments/index.d.ts.map +1 -1
  35. package/dist/storage/domains/memory/index.d.ts +39 -1
  36. package/dist/storage/domains/memory/index.d.ts.map +1 -1
  37. package/dist/storage/domains/notifications/index.d.ts +18 -1
  38. package/dist/storage/domains/notifications/index.d.ts.map +1 -1
  39. package/dist/storage/domains/observability/index.d.ts +22 -1
  40. package/dist/storage/domains/observability/index.d.ts.map +1 -1
  41. package/dist/storage/domains/observability/v-next/index.d.ts +17 -1
  42. package/dist/storage/domains/observability/v-next/index.d.ts.map +1 -1
  43. package/dist/storage/domains/observability/v-next/retention.d.ts +64 -0
  44. package/dist/storage/domains/observability/v-next/retention.d.ts.map +1 -0
  45. package/dist/storage/domains/schedules/index.d.ts +23 -1
  46. package/dist/storage/domains/schedules/index.d.ts.map +1 -1
  47. package/dist/storage/domains/scorer-definitions/index.d.ts.map +1 -1
  48. package/dist/storage/domains/scores/index.d.ts +26 -5
  49. package/dist/storage/domains/scores/index.d.ts.map +1 -1
  50. package/dist/storage/domains/utils.d.ts +20 -1
  51. package/dist/storage/domains/utils.d.ts.map +1 -1
  52. package/dist/storage/domains/workflows/index.d.ts +19 -1
  53. package/dist/storage/domains/workflows/index.d.ts.map +1 -1
  54. package/dist/storage/retention.d.ts +76 -0
  55. package/dist/storage/retention.d.ts.map +1 -0
  56. package/dist/storage/test-utils.d.ts.map +1 -1
  57. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { MastraError, ErrorCategory, ErrorDomain } from '@mastra/core/error';
2
- import { BRANCH_SPAN_TYPES, createVectorErrorId, AgentsStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_SCHEMAS, createStorageErrorId, normalizePerPage, calculatePagination, BackgroundTasksStorage, TABLE_BACKGROUND_TASKS, BlobStore, TABLE_SKILL_BLOBS, ChannelsStorage, TABLE_CHANNEL_INSTALLATIONS, TABLE_CHANNEL_CONFIG, DatasetsStorage, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, DATASETS_SCHEMA, TABLE_CONFIGS, DATASET_ITEMS_SCHEMA, DATASET_VERSIONS_SCHEMA, ensureDate, safelyParseJSON, ExperimentsStorage, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, EXPERIMENTS_SCHEMA, EXPERIMENT_RESULTS_SCHEMA, FavoritesStorage, TABLE_FAVORITES, MCPClientsStorage, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, MCPServersStorage, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, MemoryStorage, TABLE_THREADS, TABLE_MESSAGES, TABLE_RESOURCES, NotificationsStorage, TABLE_NOTIFICATIONS, ObservabilityStorage, TABLE_SPANS, listTracesArgsSchema, toTraceSpans, PromptBlocksStorage, TABLE_PROMPT_BLOCKS, TABLE_PROMPT_BLOCK_VERSIONS, SchedulesStorage, TABLE_SCHEDULES, TABLE_SCHEDULE_TRIGGERS, ScorerDefinitionsStorage, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, ScoresStorage, TABLE_SCORERS, SkillsStorage, TABLE_SKILLS, TABLE_SKILL_VERSIONS, ToolProviderConnectionsStorage, TABLE_TOOL_PROVIDER_CONNECTIONS, WorkflowsStorage, TABLE_WORKFLOW_SNAPSHOT, mergeWorkflowStepResult, WorkspacesStorage, TABLE_WORKSPACES, TABLE_WORKSPACE_VERSIONS, MastraCompositeStore, TraceStatus, getDefaultValue, listBranchesArgsSchema, listLogsArgsSchema, listMetricsArgsSchema, listScoresArgsSchema, listFeedbackArgsSchema, transformScoreRow as transformScoreRow$1, getSqlType, EntityType, METRIC_DISTINCT_COLUMNS } from '@mastra/core/storage';
2
+ import { BRANCH_SPAN_TYPES, createVectorErrorId, AgentsStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_SCHEMAS, createStorageErrorId, normalizePerPage, calculatePagination, BackgroundTasksStorage, TABLE_BACKGROUND_TASKS, BlobStore, TABLE_SKILL_BLOBS, ChannelsStorage, TABLE_CHANNEL_INSTALLATIONS, TABLE_CHANNEL_CONFIG, DatasetsStorage, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, DATASETS_SCHEMA, TABLE_CONFIGS, DATASET_ITEMS_SCHEMA, DATASET_VERSIONS_SCHEMA, ensureDate, safelyParseJSON, ExperimentsStorage, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, EXPERIMENTS_SCHEMA, EXPERIMENT_RESULTS_SCHEMA, FavoritesStorage, TABLE_FAVORITES, MCPClientsStorage, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, MCPServersStorage, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, MemoryStorage, TABLE_THREADS, TABLE_RESOURCES, TABLE_MESSAGES, NotificationsStorage, TABLE_NOTIFICATIONS, ObservabilityStorage, TABLE_SPANS, listTracesArgsSchema, toTraceSpans, PromptBlocksStorage, TABLE_PROMPT_BLOCKS, TABLE_PROMPT_BLOCK_VERSIONS, SchedulesStorage, TABLE_SCHEDULES, TABLE_SCHEDULE_TRIGGERS, ScorerDefinitionsStorage, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, ScoresStorage, TABLE_SCORERS, SkillsStorage, TABLE_SKILLS, TABLE_SKILL_VERSIONS, ToolProviderConnectionsStorage, TABLE_TOOL_PROVIDER_CONNECTIONS, WorkflowsStorage, TABLE_WORKFLOW_SNAPSHOT, mergeWorkflowStepResult, WorkspacesStorage, TABLE_WORKSPACES, TABLE_WORKSPACE_VERSIONS, MastraCompositeStore, TraceStatus, getDefaultValue, parseDuration, listBranchesArgsSchema, listLogsArgsSchema, listMetricsArgsSchema, listScoresArgsSchema, listFeedbackArgsSchema, transformScoreRow as transformScoreRow$1, getSqlType, EntityType, METRIC_DISTINCT_COLUMNS } from '@mastra/core/storage';
3
3
  import { parseSqlIdentifier, parseFieldKey } from '@mastra/core/utils';
4
4
  import { MastraVector, validateTopK, validateUpsertInput } from '@mastra/core/vector';
5
5
  import { Mutex } from 'async-mutex';
@@ -2517,6 +2517,11 @@ CREATE TRIGGER ${triggerName}
2517
2517
  EXECUTE FUNCTION ${functionName}();`;
2518
2518
  }
2519
2519
  var schemaSetupRegistry = /* @__PURE__ */ new Map();
2520
+ function assertPositiveLimit(limit) {
2521
+ if (!Number.isSafeInteger(limit) || limit <= 0) {
2522
+ throw new Error(`prune limit must be a positive integer; received ${limit}`);
2523
+ }
2524
+ }
2520
2525
  var PgDB = class extends MastraBase {
2521
2526
  client;
2522
2527
  schemaName;
@@ -3553,6 +3558,96 @@ Note: This migration may take some time for large tables.
3553
3558
  async deleteData({ tableName }) {
3554
3559
  return this.clearTable({ tableName });
3555
3560
  }
3561
+ // ---------------------------------------------------------------------------
3562
+ // Retention helpers (prune)
3563
+ // ---------------------------------------------------------------------------
3564
+ /**
3565
+ * Deletes up to `limit` rows from `tableName` whose `column` value is strictly
3566
+ * older than `cutoff`, in a single bounded statement. Returns the number of
3567
+ * rows deleted so the caller's batch loop can decide whether the table is
3568
+ * drained.
3569
+ *
3570
+ * PostgreSQL has no `DELETE ... LIMIT`, so this targets a bounded set of
3571
+ * physical rows via the `ctid` system column (PG's row identity, analogous to
3572
+ * SQLite's `rowid`). `cutoff` is bound as a parameter — a `Date`/ISO-8601
3573
+ * string compared against a `timestamptz` anchor column, or a `number`
3574
+ * compared against a `bigint` epoch-ms anchor column.
3575
+ */
3576
+ async pruneBatch({
3577
+ tableName,
3578
+ column,
3579
+ cutoff,
3580
+ limit
3581
+ }) {
3582
+ assertPositiveLimit(limit);
3583
+ const fullTableName = getTableName({ indexName: tableName, schemaName: getSchemaName(this.schemaName) });
3584
+ const parsedColumn = `"${parseSqlIdentifier(column, "column name")}"`;
3585
+ const sql = `
3586
+ DELETE FROM ${fullTableName}
3587
+ WHERE ctid IN (
3588
+ SELECT ctid FROM ${fullTableName}
3589
+ WHERE ${parsedColumn} < $1
3590
+ LIMIT $2
3591
+ )
3592
+ `;
3593
+ const result = await this.client.query(sql, [cutoff, limit]);
3594
+ return result.rowCount ?? 0;
3595
+ }
3596
+ /**
3597
+ * Deletes up to `limit` aged parent rows *and* their child rows together, in
3598
+ * a single transaction (used by whole-unit pruning such as experiments →
3599
+ * experiment_results). The aged parent IDs are selected first and both
3600
+ * deletes target that exact ID set, so a bound or abort between batches never
3601
+ * leaves a parent hollow (kept, but with its children gone) or children
3602
+ * orphaned.
3603
+ */
3604
+ async pruneUnitsBatch({
3605
+ parentTable,
3606
+ parentKey,
3607
+ parentColumn,
3608
+ childTable,
3609
+ childForeignKey,
3610
+ cutoff,
3611
+ limit
3612
+ }) {
3613
+ assertPositiveLimit(limit);
3614
+ const schemaName = getSchemaName(this.schemaName);
3615
+ const fullChildTable = getTableName({ indexName: childTable, schemaName });
3616
+ const fullParentTable = getTableName({ indexName: parentTable, schemaName });
3617
+ const childFk = `"${parseSqlIdentifier(childForeignKey, "column name")}"`;
3618
+ const parentPk = `"${parseSqlIdentifier(parentKey, "column name")}"`;
3619
+ const parentCol = `"${parseSqlIdentifier(parentColumn, "column name")}"`;
3620
+ return this.client.tx(async (t) => {
3621
+ const rows = await t.manyOrNone(
3622
+ `SELECT ${parentPk} AS id FROM ${fullParentTable} WHERE ${parentCol} < $1 LIMIT $2 FOR UPDATE SKIP LOCKED`,
3623
+ [cutoff, limit]
3624
+ );
3625
+ const ids = rows.map((r) => r.id);
3626
+ if (ids.length === 0) return { parents: 0, children: 0 };
3627
+ const childResult = await t.query(`DELETE FROM ${fullChildTable} WHERE ${childFk} = ANY($1)`, [ids]);
3628
+ const parentResult = await t.query(`DELETE FROM ${fullParentTable} WHERE ${parentPk} = ANY($1)`, [ids]);
3629
+ return {
3630
+ parents: parentResult.rowCount ?? 0,
3631
+ children: childResult.rowCount ?? 0
3632
+ };
3633
+ });
3634
+ }
3635
+ /**
3636
+ * Creates a btree index on `column` for `tableName` if it does not already
3637
+ * exist, so age-based prune deletes stay fast. Delegates to {@link createIndex}
3638
+ * (which is a no-op when the index is present).
3639
+ *
3640
+ * The name is lowercased and truncated to Postgres' 63-byte identifier limit
3641
+ * (schema-prefixed names can exceed it), mirroring {@link buildConstraintName}.
3642
+ */
3643
+ async ensureIndex({
3644
+ indexName,
3645
+ tableName,
3646
+ column
3647
+ }) {
3648
+ const name = buildConstraintName({ baseName: indexName });
3649
+ await this.createIndex({ name, table: tableName, columns: [column] });
3650
+ }
3556
3651
  async executeUpdate(client, {
3557
3652
  tableName,
3558
3653
  keys,
@@ -3628,6 +3723,20 @@ function transformFromSqlRow({
3628
3723
  });
3629
3724
  return result;
3630
3725
  }
3726
+ function tenancyWhere(filters, startIndex) {
3727
+ const conditions = [];
3728
+ const params = [];
3729
+ let idx = startIndex;
3730
+ if (filters?.organizationId !== void 0) {
3731
+ conditions.push(`"organizationId" = $${idx++}`);
3732
+ params.push(filters.organizationId);
3733
+ }
3734
+ if (filters?.projectId !== void 0) {
3735
+ conditions.push(`"projectId" = $${idx++}`);
3736
+ params.push(filters.projectId);
3737
+ }
3738
+ return { conditions, params, nextIndex: idx };
3739
+ }
3631
3740
 
3632
3741
  // src/storage/domains/agents/index.ts
3633
3742
  var AgentsPG = class _AgentsPG extends AgentsStorage {
@@ -4511,6 +4620,98 @@ var AgentsPG = class _AgentsPG extends AgentsStorage {
4511
4620
  };
4512
4621
  }
4513
4622
  };
4623
+ var DEFAULT_BATCH_SIZE = 1e3;
4624
+ async function sleep(ms, signal) {
4625
+ if (ms <= 0 || signal?.aborted) return;
4626
+ await new Promise((resolve) => {
4627
+ const timer = setTimeout(() => {
4628
+ signal?.removeEventListener("abort", onAbort);
4629
+ resolve();
4630
+ }, ms);
4631
+ const onAbort = () => {
4632
+ clearTimeout(timer);
4633
+ resolve();
4634
+ };
4635
+ signal?.addEventListener("abort", onAbort, { once: true });
4636
+ });
4637
+ }
4638
+ function cutoffFor(policy, anchorType, now = Date.now()) {
4639
+ const cutoffMs = now - parseDuration(policy.maxAge);
4640
+ return anchorType === "epoch-ms" ? cutoffMs : new Date(cutoffMs);
4641
+ }
4642
+ async function runBatchedDelete({
4643
+ deleteBatch,
4644
+ batchSize,
4645
+ options
4646
+ }) {
4647
+ if (!Number.isSafeInteger(batchSize) || batchSize <= 0) {
4648
+ throw new Error(`retention batchSize must be a positive integer; received ${batchSize}`);
4649
+ }
4650
+ let deleted = 0;
4651
+ let batches = 0;
4652
+ while (true) {
4653
+ if (options?.signal?.aborted) return { deleted, done: false };
4654
+ if (options?.maxBatches !== void 0 && batches >= options.maxBatches) return { deleted, done: false };
4655
+ let limit = batchSize;
4656
+ if (options?.maxRows !== void 0) {
4657
+ const remaining = options.maxRows - deleted;
4658
+ if (remaining <= 0) return { deleted, done: false };
4659
+ limit = Math.min(limit, remaining);
4660
+ }
4661
+ const affected = await deleteBatch(limit);
4662
+ deleted += affected;
4663
+ batches += 1;
4664
+ if (affected < limit) return { deleted, done: true };
4665
+ if (options?.pauseMs) {
4666
+ await sleep(options.pauseMs, options.signal);
4667
+ }
4668
+ }
4669
+ }
4670
+ async function runPrune({
4671
+ db,
4672
+ domain,
4673
+ targets,
4674
+ options
4675
+ }) {
4676
+ const results = [];
4677
+ const now = Date.now();
4678
+ for (const target of targets) {
4679
+ if (options?.signal?.aborted) {
4680
+ results.push({ domain, table: target.table, deleted: 0, done: false });
4681
+ continue;
4682
+ }
4683
+ const cutoff = cutoffFor(target.policy, target.anchorType, now);
4684
+ const batchSize = target.policy.batchSize ?? DEFAULT_BATCH_SIZE;
4685
+ const { deleted, done } = await runBatchedDelete({
4686
+ deleteBatch: (limit) => db.pruneBatch({ tableName: target.table, column: target.column, cutoff, limit }),
4687
+ batchSize,
4688
+ options
4689
+ });
4690
+ results.push({ domain, table: target.table, deleted, done });
4691
+ }
4692
+ return results;
4693
+ }
4694
+ function resolveTargets({
4695
+ policies,
4696
+ descriptor,
4697
+ order
4698
+ }) {
4699
+ const targets = [];
4700
+ for (const key of order) {
4701
+ const policy = policies[key];
4702
+ const entry = descriptor[key];
4703
+ if (!policy || !entry) continue;
4704
+ targets.push({
4705
+ table: entry.table,
4706
+ column: entry.column,
4707
+ anchorType: entry.anchorType ?? "timestamp",
4708
+ policy
4709
+ });
4710
+ }
4711
+ return targets;
4712
+ }
4713
+
4714
+ // src/storage/domains/background-tasks/index.ts
4514
4715
  function getSchemaName3(schema) {
4515
4716
  return schema ? `"${schema}"` : '"public"';
4516
4717
  }
@@ -4565,6 +4766,14 @@ var BackgroundTasksPG = class _BackgroundTasksPG extends BackgroundTasksStorage
4565
4766
  #skipDefaultIndexes;
4566
4767
  #indexes;
4567
4768
  static MANAGED_TABLES = [TABLE_BACKGROUND_TASKS];
4769
+ /**
4770
+ * Completed background tasks accumulate as dead weight. Anchored on the
4771
+ * timezone-aware `completedAtZ` mirror column: NULL (in-flight) rows are
4772
+ * excluded automatically since `completedAtZ < cutoff` is false for NULL.
4773
+ */
4774
+ static retentionTables = {
4775
+ backgroundTasks: { table: TABLE_BACKGROUND_TASKS, column: "completedAtZ", indexed: true }
4776
+ };
4568
4777
  constructor(config) {
4569
4778
  super();
4570
4779
  const { client, schemaName, skipDefaultIndexes, indexes } = resolvePgConfig(config);
@@ -4586,6 +4795,40 @@ var BackgroundTasksPG = class _BackgroundTasksPG extends BackgroundTasksStorage
4586
4795
  await this.createDefaultIndexes();
4587
4796
  await this.createCustomIndexes();
4588
4797
  }
4798
+ /**
4799
+ * Lazily ensures a btree index exists on each configured policy's retention
4800
+ * anchor column so age-based `prune()` deletes stay fast on large tables.
4801
+ * Called from the prune path (not init) so only deployments that configure
4802
+ * retention pay the index's write/disk overhead. Best-effort: failures are
4803
+ * logged and pruning proceeds (correct, just slower).
4804
+ * Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
4805
+ * so its supporting index is not part of the default index set.
4806
+ */
4807
+ async ensureRetentionIndexes(policies) {
4808
+ const prefix = this.#schema !== "public" ? `${this.#schema}_` : "";
4809
+ for (const [key, entry] of Object.entries(_BackgroundTasksPG.retentionTables)) {
4810
+ if (!entry.indexed || !policies[key]) continue;
4811
+ try {
4812
+ await this.#db.ensureIndex({
4813
+ indexName: `${prefix}mastra_${key}_retention_idx`,
4814
+ tableName: entry.table,
4815
+ column: entry.column
4816
+ });
4817
+ } catch (error) {
4818
+ this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
4819
+ }
4820
+ }
4821
+ }
4822
+ /** Delete completed tasks older than the `backgroundTasks` policy's `maxAge`, batched. */
4823
+ async prune(policies, options) {
4824
+ await this.ensureRetentionIndexes(policies);
4825
+ const targets = resolveTargets({
4826
+ policies,
4827
+ descriptor: _BackgroundTasksPG.retentionTables,
4828
+ order: ["backgroundTasks"]
4829
+ });
4830
+ return runPrune({ db: this.#db, domain: "backgroundTasks", targets, options });
4831
+ }
4589
4832
  static getDefaultIndexDefs(schemaPrefix) {
4590
4833
  return [
4591
4834
  {
@@ -5349,8 +5592,8 @@ var DatasetsPG = class _DatasetsPG extends DatasetsStorage {
5349
5592
  groundTruthSchema: input.groundTruthSchema ?? null,
5350
5593
  requestContextSchema: input.requestContextSchema ?? null,
5351
5594
  targetType: input.targetType ?? null,
5352
- targetIds: input.targetIds !== void 0 ? JSON.stringify(input.targetIds) : null,
5353
- scorerIds: input.scorerIds ? JSON.stringify(input.scorerIds) : null,
5595
+ targetIds: input.targetIds ?? null,
5596
+ scorerIds: input.scorerIds ?? null,
5354
5597
  organizationId: input.organizationId ?? null,
5355
5598
  projectId: input.projectId ?? null,
5356
5599
  candidateKey: input.candidateKey ?? null,
@@ -5390,10 +5633,15 @@ var DatasetsPG = class _DatasetsPG extends DatasetsStorage {
5390
5633
  );
5391
5634
  }
5392
5635
  }
5393
- async getDatasetById({ id }) {
5636
+ async getDatasetById({
5637
+ id,
5638
+ filters
5639
+ }) {
5394
5640
  try {
5395
5641
  const tableName = getTableName2({ indexName: TABLE_DATASETS, schemaName: getSchemaName2(this.#schema) });
5396
- const result = await this.#db.client.oneOrNone(`SELECT * FROM ${tableName} WHERE "id" = $1`, [id]);
5642
+ const { conditions, params } = tenancyWhere(filters, 2);
5643
+ const whereSql = ['"id" = $1', ...conditions].join(" AND ");
5644
+ const result = await this.#db.client.oneOrNone(`SELECT * FROM ${tableName} WHERE ${whereSql}`, [id, ...params]);
5397
5645
  return result ? this.transformDatasetRow(result) : null;
5398
5646
  } catch (error) {
5399
5647
  throw new MastraError(
@@ -5408,7 +5656,7 @@ var DatasetsPG = class _DatasetsPG extends DatasetsStorage {
5408
5656
  }
5409
5657
  async _doUpdateDataset(args) {
5410
5658
  try {
5411
- const existing = await this.getDatasetById({ id: args.id });
5659
+ const existing = await this.getDatasetById({ id: args.id, filters: args.filters });
5412
5660
  if (!existing) {
5413
5661
  throw new MastraError({
5414
5662
  id: createStorageErrorId("PG", "UPDATE_DATASET", "NOT_FOUND"),
@@ -5497,7 +5745,7 @@ var DatasetsPG = class _DatasetsPG extends DatasetsStorage {
5497
5745
  );
5498
5746
  }
5499
5747
  }
5500
- async deleteDataset({ id }) {
5748
+ async deleteDataset({ id, filters }) {
5501
5749
  try {
5502
5750
  const datasetsTable = getTableName2({ indexName: TABLE_DATASETS, schemaName: getSchemaName2(this.#schema) });
5503
5751
  const itemsTable = getTableName2({ indexName: TABLE_DATASET_ITEMS, schemaName: getSchemaName2(this.#schema) });
@@ -5510,24 +5758,31 @@ var DatasetsPG = class _DatasetsPG extends DatasetsStorage {
5510
5758
  indexName: TABLE_EXPERIMENT_RESULTS,
5511
5759
  schemaName: getSchemaName2(this.#schema)
5512
5760
  });
5513
- try {
5514
- await this.#db.client.none(
5515
- `DELETE FROM ${experimentResultsTable} WHERE "experimentId" IN (SELECT "id" FROM ${experimentsTable} WHERE "datasetId" = $1)`,
5516
- [id]
5517
- );
5518
- } catch {
5519
- }
5520
- try {
5521
- await this.#db.client.none(
5522
- `UPDATE ${experimentsTable} SET "datasetId" = NULL, "datasetVersion" = NULL WHERE "datasetId" = $1`,
5523
- [id]
5524
- );
5525
- } catch {
5526
- }
5761
+ const { conditions, params } = tenancyWhere(filters, 2);
5762
+ const scopedWhere = ['"id" = $1', ...conditions].join(" AND ");
5527
5763
  await this.#db.client.tx(async (t) => {
5764
+ const locked = await t.oneOrNone(`SELECT "id" FROM ${datasetsTable} WHERE ${scopedWhere} FOR UPDATE`, [
5765
+ id,
5766
+ ...params
5767
+ ]);
5768
+ if (!locked) return;
5769
+ const experimentTablesExist = await t.oneOrNone(
5770
+ `SELECT to_regclass($1) IS NOT NULL AND to_regclass($2) IS NOT NULL AS exists`,
5771
+ [experimentResultsTable, experimentsTable]
5772
+ );
5773
+ if (experimentTablesExist?.exists) {
5774
+ await t.none(
5775
+ `DELETE FROM ${experimentResultsTable} WHERE "experimentId" IN (SELECT "id" FROM ${experimentsTable} WHERE "datasetId" = $1)`,
5776
+ [id]
5777
+ );
5778
+ await t.none(
5779
+ `UPDATE ${experimentsTable} SET "datasetId" = NULL, "datasetVersion" = NULL WHERE "datasetId" = $1`,
5780
+ [id]
5781
+ );
5782
+ }
5528
5783
  await t.none(`DELETE FROM ${versionsTable} WHERE "datasetId" = $1`, [id]);
5529
5784
  await t.none(`DELETE FROM ${itemsTable} WHERE "datasetId" = $1`, [id]);
5530
- await t.none(`DELETE FROM ${datasetsTable} WHERE "id" = $1`, [id]);
5785
+ await t.none(`DELETE FROM ${datasetsTable} WHERE ${scopedWhere}`, [id, ...params]);
5531
5786
  });
5532
5787
  } catch (error) {
5533
5788
  if (error instanceof MastraError) throw error;
@@ -5549,7 +5804,7 @@ var DatasetsPG = class _DatasetsPG extends DatasetsStorage {
5549
5804
  const queryParams = [];
5550
5805
  let paramIndex = 1;
5551
5806
  if (args.filters) {
5552
- const { organizationId, projectId, candidateKey, candidateId } = args.filters;
5807
+ const { organizationId, projectId, candidateKey, candidateId, targetType, targetIds, name } = args.filters;
5553
5808
  if (organizationId !== void 0) {
5554
5809
  conditions.push(`"organizationId" = $${paramIndex++}`);
5555
5810
  queryParams.push(organizationId);
@@ -5566,6 +5821,18 @@ var DatasetsPG = class _DatasetsPG extends DatasetsStorage {
5566
5821
  conditions.push(`"candidateId" = $${paramIndex++}`);
5567
5822
  queryParams.push(candidateId);
5568
5823
  }
5824
+ if (targetType !== void 0) {
5825
+ conditions.push(`"targetType" = $${paramIndex++}`);
5826
+ queryParams.push(targetType);
5827
+ }
5828
+ if (targetIds !== void 0 && targetIds.length > 0) {
5829
+ conditions.push(`"targetIds" ?| $${paramIndex++}::text[]`);
5830
+ queryParams.push(targetIds);
5831
+ }
5832
+ if (name !== void 0 && name.length > 0) {
5833
+ conditions.push(`"name" ILIKE $${paramIndex++}`);
5834
+ queryParams.push(`%${name}%`);
5835
+ }
5569
5836
  }
5570
5837
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
5571
5838
  const countResult = await this.#db.client.one(
@@ -6223,12 +6490,24 @@ var DatasetsPG = class _DatasetsPG extends DatasetsStorage {
6223
6490
  await this.#db.clearTable({ tableName: TABLE_DATASETS });
6224
6491
  }
6225
6492
  };
6493
+ var DEFAULT_PRUNE_BATCH_SIZE = 1e3;
6226
6494
  var ExperimentsPG = class _ExperimentsPG extends ExperimentsStorage {
6227
6495
  #db;
6228
6496
  #schema;
6229
6497
  #skipDefaultIndexes;
6230
6498
  #indexes;
6231
6499
  static MANAGED_TABLES = [TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS];
6500
+ /**
6501
+ * Experiments prune as whole units: an aged experiment and its result rows go
6502
+ * together, mirroring `deleteExperiment`. Anchored on `completedAt` (not the
6503
+ * `completedAtZ` mirror, which carries a `DEFAULT NOW()` this domain never
6504
+ * overrides — it holds insert time even for running rows). `completedAt` is
6505
+ * written as a UTC ISO string and stays NULL while running, so
6506
+ * `completedAt < cutoff` is false for in-flight experiments.
6507
+ */
6508
+ static retentionTables = {
6509
+ experiments: { table: TABLE_EXPERIMENTS, column: "completedAt", indexed: true }
6510
+ };
6232
6511
  constructor(config) {
6233
6512
  super();
6234
6513
  const { client, schemaName, skipDefaultIndexes, indexes } = resolvePgConfig(config);
@@ -6267,6 +6546,75 @@ var ExperimentsPG = class _ExperimentsPG extends ExperimentsStorage {
6267
6546
  await this.createDefaultIndexes();
6268
6547
  await this.createCustomIndexes();
6269
6548
  }
6549
+ /**
6550
+ * Lazily ensures a btree index exists on each configured policy's retention
6551
+ * anchor column so age-based `prune()` deletes stay fast on large tables.
6552
+ * Called from the prune path (not init) so only deployments that configure
6553
+ * retention pay the index's write/disk overhead. Best-effort: failures are
6554
+ * logged and pruning proceeds (correct, just slower).
6555
+ * Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
6556
+ * so its supporting index is not part of the default index set.
6557
+ */
6558
+ async ensureRetentionIndexes(policies) {
6559
+ const prefix = this.#schema !== "public" ? `${this.#schema}_` : "";
6560
+ for (const [key, entry] of Object.entries(_ExperimentsPG.retentionTables)) {
6561
+ if (!entry.indexed || !policies[key]) continue;
6562
+ try {
6563
+ await this.#db.ensureIndex({
6564
+ indexName: `${prefix}mastra_${key}_retention_idx`,
6565
+ tableName: entry.table,
6566
+ column: entry.column
6567
+ });
6568
+ } catch (error) {
6569
+ this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
6570
+ }
6571
+ }
6572
+ }
6573
+ /**
6574
+ * Delete experiments whose `completedAt` is older than the policy's `maxAge`.
6575
+ *
6576
+ * Each batch selects up to `batchSize` aged experiments and deletes their
6577
+ * `experiment_results` rows and the experiment rows in one transaction —
6578
+ * mirroring `deleteExperiment` — so hitting `maxBatches`/`maxRows` or the
6579
+ * abort signal between batches never leaves a run hollow (parent kept,
6580
+ * results gone). NULL `completedAt` (still running) is excluded by the
6581
+ * `< cutoff` predicate. Bounds count whole experiments, not rows.
6582
+ */
6583
+ async prune(policies, options) {
6584
+ const policy = policies["experiments"];
6585
+ if (!policy || options?.signal?.aborted) {
6586
+ return policy ? [
6587
+ { domain: "experiments", table: TABLE_EXPERIMENT_RESULTS, deleted: 0, done: false },
6588
+ { domain: "experiments", table: TABLE_EXPERIMENTS, deleted: 0, done: false }
6589
+ ] : [];
6590
+ }
6591
+ await this.ensureRetentionIndexes(policies);
6592
+ const rawCutoff = cutoffFor(policy, "timestamp");
6593
+ const cutoff = rawCutoff instanceof Date ? rawCutoff.toISOString() : rawCutoff;
6594
+ const batchSize = policy.batchSize ?? DEFAULT_PRUNE_BATCH_SIZE;
6595
+ let childDeleted = 0;
6596
+ const parent = await runBatchedDelete({
6597
+ deleteBatch: async (limit) => {
6598
+ const { parents, children } = await this.#db.pruneUnitsBatch({
6599
+ parentTable: TABLE_EXPERIMENTS,
6600
+ parentKey: "id",
6601
+ parentColumn: "completedAt",
6602
+ childTable: TABLE_EXPERIMENT_RESULTS,
6603
+ childForeignKey: "experimentId",
6604
+ cutoff,
6605
+ limit
6606
+ });
6607
+ childDeleted += children;
6608
+ return parents;
6609
+ },
6610
+ batchSize,
6611
+ options
6612
+ });
6613
+ return [
6614
+ { domain: "experiments", table: TABLE_EXPERIMENT_RESULTS, deleted: childDeleted, done: parent.done },
6615
+ { domain: "experiments", table: TABLE_EXPERIMENTS, deleted: parent.deleted, done: parent.done }
6616
+ ];
6617
+ }
6270
6618
  getDefaultIndexDefinitions() {
6271
6619
  return [
6272
6620
  { name: "idx_experiments_datasetid", table: TABLE_EXPERIMENTS, columns: ["datasetId"] },
@@ -6496,10 +6844,15 @@ var ExperimentsPG = class _ExperimentsPG extends ExperimentsStorage {
6496
6844
  );
6497
6845
  }
6498
6846
  }
6499
- async getExperimentById({ id }) {
6847
+ async getExperimentById({
6848
+ id,
6849
+ filters
6850
+ }) {
6500
6851
  try {
6501
6852
  const tableName = getTableName2({ indexName: TABLE_EXPERIMENTS, schemaName: getSchemaName2(this.#schema) });
6502
- const result = await this.#db.client.oneOrNone(`SELECT * FROM ${tableName} WHERE "id" = $1`, [id]);
6853
+ const { conditions, params } = tenancyWhere(filters, 2);
6854
+ const whereSql = ['"id" = $1', ...conditions].join(" AND ");
6855
+ const result = await this.#db.client.oneOrNone(`SELECT * FROM ${tableName} WHERE ${whereSql}`, [id, ...params]);
6503
6856
  return result ? this.transformExperimentRow(result) : null;
6504
6857
  } catch (error) {
6505
6858
  throw new MastraError(
@@ -6586,15 +6939,21 @@ var ExperimentsPG = class _ExperimentsPG extends ExperimentsStorage {
6586
6939
  );
6587
6940
  }
6588
6941
  }
6589
- async deleteExperiment({ id }) {
6942
+ async deleteExperiment({ id, filters }) {
6590
6943
  try {
6591
6944
  const resultsTable = getTableName2({
6592
6945
  indexName: TABLE_EXPERIMENT_RESULTS,
6593
6946
  schemaName: getSchemaName2(this.#schema)
6594
6947
  });
6595
6948
  const experimentsTable = getTableName2({ indexName: TABLE_EXPERIMENTS, schemaName: getSchemaName2(this.#schema) });
6596
- await this.#db.client.none(`DELETE FROM ${resultsTable} WHERE "experimentId" = $1`, [id]);
6597
- await this.#db.client.none(`DELETE FROM ${experimentsTable} WHERE "id" = $1`, [id]);
6949
+ const { conditions, params } = tenancyWhere(filters, 2);
6950
+ const gateSql = `SELECT "id" FROM ${experimentsTable} WHERE ${['"id" = $1', ...conditions].join(" AND ")} FOR UPDATE`;
6951
+ await this.#db.client.tx(async (t) => {
6952
+ const parent = await t.oneOrNone(gateSql, [id, ...params]);
6953
+ if (!parent) return;
6954
+ await t.none(`DELETE FROM ${resultsTable} WHERE "experimentId" = $1`, [id]);
6955
+ await t.none(`DELETE FROM ${experimentsTable} WHERE "id" = $1`, [id]);
6956
+ });
6598
6957
  } catch (error) {
6599
6958
  throw new MastraError(
6600
6959
  {
@@ -6724,10 +7083,15 @@ var ExperimentsPG = class _ExperimentsPG extends ExperimentsStorage {
6724
7083
  );
6725
7084
  }
6726
7085
  }
6727
- async getExperimentResultById({ id }) {
7086
+ async getExperimentResultById({
7087
+ id,
7088
+ filters
7089
+ }) {
6728
7090
  try {
6729
7091
  const tableName = getTableName2({ indexName: TABLE_EXPERIMENT_RESULTS, schemaName: getSchemaName2(this.#schema) });
6730
- const result = await this.#db.client.oneOrNone(`SELECT * FROM ${tableName} WHERE "id" = $1`, [id]);
7092
+ const { conditions, params } = tenancyWhere(filters, 2);
7093
+ const whereSql = ['"id" = $1', ...conditions].join(" AND ");
7094
+ const result = await this.#db.client.oneOrNone(`SELECT * FROM ${tableName} WHERE ${whereSql}`, [id, ...params]);
6731
7095
  return result ? this.transformExperimentResultRow(result) : null;
6732
7096
  } catch (error) {
6733
7097
  throw new MastraError(
@@ -6802,9 +7166,23 @@ var ExperimentsPG = class _ExperimentsPG extends ExperimentsStorage {
6802
7166
  );
6803
7167
  }
6804
7168
  }
6805
- async deleteExperimentResults({ experimentId }) {
7169
+ async deleteExperimentResults({
7170
+ experimentId,
7171
+ filters
7172
+ }) {
6806
7173
  try {
6807
7174
  const tableName = getTableName2({ indexName: TABLE_EXPERIMENT_RESULTS, schemaName: getSchemaName2(this.#schema) });
7175
+ const experimentsTable = getTableName2({ indexName: TABLE_EXPERIMENTS, schemaName: getSchemaName2(this.#schema) });
7176
+ if (filters?.organizationId !== void 0 || filters?.projectId !== void 0) {
7177
+ const { conditions, params } = tenancyWhere(filters, 2);
7178
+ const gateSql = `SELECT "id" FROM ${experimentsTable} WHERE ${['"id" = $1', ...conditions].join(" AND ")} FOR UPDATE`;
7179
+ await this.#db.client.tx(async (t) => {
7180
+ const parent = await t.oneOrNone(gateSql, [experimentId, ...params]);
7181
+ if (!parent) return;
7182
+ await t.none(`DELETE FROM ${tableName} WHERE "experimentId" = $1`, [experimentId]);
7183
+ });
7184
+ return;
7185
+ }
6808
7186
  await this.#db.client.none(`DELETE FROM ${tableName} WHERE "experimentId" = $1`, [experimentId]);
6809
7187
  } catch (error) {
6810
7188
  throw new MastraError(
@@ -8435,6 +8813,18 @@ function dedupeMessagesForSave(messages) {
8435
8813
  }
8436
8814
  var MemoryPG = class _MemoryPG extends MemoryStorage {
8437
8815
  supportsObservationalMemory = true;
8816
+ /**
8817
+ * Retention-eligible tables. `threads`, `messages`, and `resources` all anchor
8818
+ * on the timezone-aware `createdAtZ` mirror column (kept in sync by triggers),
8819
+ * and are indexed for fast batched deletes. Cascade order is enforced in
8820
+ * `prune()` (children before threads), not here. Observational memory has no
8821
+ * timestamp anchor and is deliberately excluded.
8822
+ */
8823
+ static retentionTables = {
8824
+ messages: { table: TABLE_MESSAGES, column: "createdAtZ", indexed: true },
8825
+ resources: { table: TABLE_RESOURCES, column: "createdAtZ", indexed: true },
8826
+ threads: { table: TABLE_THREADS, column: "createdAtZ", indexed: true }
8827
+ };
8438
8828
  #db;
8439
8829
  #schema;
8440
8830
  #skipDefaultIndexes;
@@ -8480,6 +8870,30 @@ var MemoryPG = class _MemoryPG extends MemoryStorage {
8480
8870
  await this.createDefaultIndexes();
8481
8871
  await this.createCustomIndexes();
8482
8872
  }
8873
+ /**
8874
+ * Lazily ensures a btree index exists on each configured policy's retention
8875
+ * anchor column so age-based `prune()` deletes stay fast on large tables.
8876
+ * Called from the prune path (not init) so only deployments that configure
8877
+ * retention pay the index's write/disk overhead. Best-effort: failures are
8878
+ * logged and pruning proceeds (correct, just slower).
8879
+ * Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
8880
+ * so its supporting index is not part of the default index set.
8881
+ */
8882
+ async ensureRetentionIndexes(policies) {
8883
+ const prefix = this.#schema && this.#schema !== "public" ? `${this.#schema}_` : "";
8884
+ for (const [key, entry] of Object.entries(_MemoryPG.retentionTables)) {
8885
+ if (!entry.indexed || !policies[key]) continue;
8886
+ try {
8887
+ await this.#db.ensureIndex({
8888
+ indexName: `${prefix}mastra_${key}_retention_idx`,
8889
+ tableName: entry.table,
8890
+ column: entry.column
8891
+ });
8892
+ } catch (error) {
8893
+ this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
8894
+ }
8895
+ }
8896
+ }
8483
8897
  /**
8484
8898
  * Returns default index definitions for the memory domain tables.
8485
8899
  * @param schemaPrefix - Prefix for index names (e.g. "my_schema_" or "")
@@ -8580,6 +8994,76 @@ var MemoryPG = class _MemoryPG extends MemoryStorage {
8580
8994
  await this.#db.clearTable({ tableName: TABLE_THREADS });
8581
8995
  await this.#db.clearTable({ tableName: TABLE_RESOURCES });
8582
8996
  }
8997
+ /**
8998
+ * Deletes rows older than the configured `maxAge` per table, in bounded,
8999
+ * batched, cancellable chunks. Tables are pruned children-first (messages and
9000
+ * resources before threads) since PostgreSQL has no FK cascade in this schema.
9001
+ * Unset tables are kept forever.
9002
+ *
9003
+ * When a `messages` policy is set, semantic-recall embeddings for pruned
9004
+ * messages are also swept from same-schema `memory_messages*` vector tables
9005
+ * (best-effort, mirroring `deleteThread`). Embeddings held in an external
9006
+ * vector store are out of reach and must be pruned by the operator.
9007
+ */
9008
+ async prune(policies, options) {
9009
+ await this.ensureRetentionIndexes(policies);
9010
+ const targets = resolveTargets({
9011
+ policies,
9012
+ descriptor: _MemoryPG.retentionTables,
9013
+ order: ["messages", "resources", "threads"]
9014
+ });
9015
+ const results = await runPrune({ db: this.#db, domain: "memory", targets, options });
9016
+ if (policies["messages"]) {
9017
+ await this.pruneOrphanedVectorRows(policies["messages"], options);
9018
+ }
9019
+ return results;
9020
+ }
9021
+ /**
9022
+ * Best-effort sweep of semantic-recall vector rows whose source message no
9023
+ * longer exists (e.g. it was just pruned), so recall doesn't keep returning
9024
+ * embeddings that resolve to nothing. Only same-schema default vector tables
9025
+ * (`memory_messages*`) are covered — the same set `deleteThread` cleans up.
9026
+ * Failures are logged, never thrown: vector cleanup must not fail the prune.
9027
+ */
9028
+ async pruneOrphanedVectorRows(policy, options) {
9029
+ try {
9030
+ const schemaName = this.#schema || "public";
9031
+ const vectorTables = await this.#db.client.manyOrNone(
9032
+ `
9033
+ SELECT tablename
9034
+ FROM pg_tables
9035
+ WHERE schemaname = $1
9036
+ AND (tablename = 'memory_messages' OR tablename LIKE 'memory_messages_%')
9037
+ `,
9038
+ [schemaName]
9039
+ );
9040
+ const messagesTable = getTableName4({ indexName: TABLE_MESSAGES, schemaName: getSchemaName4(this.#schema) });
9041
+ for (const { tablename } of vectorTables) {
9042
+ const vectorTableName = getTableName4({ indexName: tablename, schemaName: getSchemaName4(this.#schema) });
9043
+ await runBatchedDelete({
9044
+ deleteBatch: async (limit) => {
9045
+ const result = await this.#db.client.query(
9046
+ `
9047
+ DELETE FROM ${vectorTableName}
9048
+ WHERE ctid IN (
9049
+ SELECT v.ctid FROM ${vectorTableName} v
9050
+ WHERE v.metadata->>'message_id' IS NOT NULL
9051
+ AND NOT EXISTS (SELECT 1 FROM ${messagesTable} m WHERE m.id = v.metadata->>'message_id')
9052
+ LIMIT $1
9053
+ )
9054
+ `,
9055
+ [limit]
9056
+ );
9057
+ return result.rowCount ?? 0;
9058
+ },
9059
+ batchSize: policy.batchSize ?? 1e3,
9060
+ options
9061
+ });
9062
+ }
9063
+ } catch (error) {
9064
+ this.logger?.warn?.("Failed to sweep orphaned semantic-recall vector rows after prune:", error);
9065
+ }
9066
+ }
8583
9067
  /**
8584
9068
  * Normalizes message row from database by applying createdAtZ fallback
8585
9069
  */
@@ -10891,6 +11375,13 @@ var NotificationsPG = class _NotificationsPG extends NotificationsStorage {
10891
11375
  #skipDefaultIndexes;
10892
11376
  #indexes;
10893
11377
  static MANAGED_TABLES = [TABLE_NOTIFICATIONS];
11378
+ /**
11379
+ * Notifications are an append-only event feed that grows unbounded. Single
11380
+ * table, anchored on the timezone-aware `createdAtZ` mirror column.
11381
+ */
11382
+ static retentionTables = {
11383
+ notifications: { table: TABLE_NOTIFICATIONS, column: "createdAtZ", indexed: true }
11384
+ };
10894
11385
  constructor(config) {
10895
11386
  super();
10896
11387
  const { client, schemaName, skipDefaultIndexes, indexes } = resolvePgConfig(config);
@@ -10907,6 +11398,40 @@ var NotificationsPG = class _NotificationsPG extends NotificationsStorage {
10907
11398
  await this.createDefaultIndexes();
10908
11399
  await this.createCustomIndexes();
10909
11400
  }
11401
+ /**
11402
+ * Lazily ensures a btree index exists on each configured policy's retention
11403
+ * anchor column so age-based `prune()` deletes stay fast on large tables.
11404
+ * Called from the prune path (not init) so only deployments that configure
11405
+ * retention pay the index's write/disk overhead. Best-effort: failures are
11406
+ * logged and pruning proceeds (correct, just slower).
11407
+ * Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
11408
+ * so its supporting index is not part of the default index set.
11409
+ */
11410
+ async ensureRetentionIndexes(policies) {
11411
+ const prefix = this.#schema && this.#schema !== "public" ? `${this.#schema}_` : "";
11412
+ for (const [key, entry] of Object.entries(_NotificationsPG.retentionTables)) {
11413
+ if (!entry.indexed || !policies[key]) continue;
11414
+ try {
11415
+ await this.#db.ensureIndex({
11416
+ indexName: `${prefix}mastra_${key}_retention_idx`,
11417
+ tableName: entry.table,
11418
+ column: entry.column
11419
+ });
11420
+ } catch (error) {
11421
+ this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
11422
+ }
11423
+ }
11424
+ }
11425
+ /** Delete notifications older than the `notifications` policy's `maxAge`, batched. */
11426
+ async prune(policies, options) {
11427
+ await this.ensureRetentionIndexes(policies);
11428
+ const targets = resolveTargets({
11429
+ policies,
11430
+ descriptor: _NotificationsPG.retentionTables,
11431
+ order: ["notifications"]
11432
+ });
11433
+ return runPrune({ db: this.#db, domain: "notifications", targets, options });
11434
+ }
10910
11435
  static getDefaultIndexDefs(schemaPrefix) {
10911
11436
  return [
10912
11437
  {
@@ -11148,6 +11673,14 @@ var NotificationsPG = class _NotificationsPG extends NotificationsStorage {
11148
11673
  }
11149
11674
  };
11150
11675
  var ObservabilityPG = class _ObservabilityPG extends ObservabilityStorage {
11676
+ /**
11677
+ * Retention-eligible tables. The observability domain has a single physical
11678
+ * table (`mastra_ai_spans`); spans anchor on the timezone-aware `startedAtZ`
11679
+ * mirror column and are indexed for fast batched deletes.
11680
+ */
11681
+ static retentionTables = {
11682
+ spans: { table: TABLE_SPANS, column: "startedAtZ", indexed: true }
11683
+ };
11151
11684
  #db;
11152
11685
  #schema;
11153
11686
  #skipDefaultIndexes;
@@ -11172,6 +11705,30 @@ var ObservabilityPG = class _ObservabilityPG extends ObservabilityStorage {
11172
11705
  await this.createDefaultIndexes();
11173
11706
  await this.createCustomIndexes();
11174
11707
  }
11708
+ /**
11709
+ * Lazily ensures a btree index exists on each configured policy's retention
11710
+ * anchor column so age-based `prune()` deletes stay fast on large span tables.
11711
+ * Called from the prune path (not init) so only deployments that configure
11712
+ * retention pay the index's write/disk overhead. Best-effort: failures are
11713
+ * logged and pruning proceeds (correct, just slower).
11714
+ * Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
11715
+ * so its supporting index is not part of the default index set.
11716
+ */
11717
+ async ensureRetentionIndexes(policies) {
11718
+ const prefix = this.#schema !== "public" ? `${this.#schema}_` : "";
11719
+ for (const [key, entry] of Object.entries(_ObservabilityPG.retentionTables)) {
11720
+ if (!entry.indexed || !policies[key]) continue;
11721
+ try {
11722
+ await this.#db.ensureIndex({
11723
+ indexName: `${prefix}mastra_${key}_retention_idx`,
11724
+ tableName: entry.table,
11725
+ column: entry.column
11726
+ });
11727
+ } catch (error) {
11728
+ this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
11729
+ }
11730
+ }
11731
+ }
11175
11732
  /**
11176
11733
  * Returns default index definitions for the observability domain tables.
11177
11734
  * @param schemaPrefix - Prefix for index names (e.g. "my_schema_" or "")
@@ -11316,6 +11873,19 @@ var ObservabilityPG = class _ObservabilityPG extends ObservabilityStorage {
11316
11873
  async dangerouslyClearAll() {
11317
11874
  await this.#db.clearTable({ tableName: TABLE_SPANS });
11318
11875
  }
11876
+ /**
11877
+ * Deletes spans older than the configured `maxAge`, in bounded, batched,
11878
+ * cancellable chunks. Unset = keep forever.
11879
+ */
11880
+ async prune(policies, options) {
11881
+ await this.ensureRetentionIndexes(policies);
11882
+ const targets = resolveTargets({
11883
+ policies,
11884
+ descriptor: _ObservabilityPG.retentionTables,
11885
+ order: ["spans"]
11886
+ });
11887
+ return runPrune({ db: this.#db, domain: "observability", targets, options });
11888
+ }
11319
11889
  get tracingStrategy() {
11320
11890
  return {
11321
11891
  preferred: "batch-with-updates",
@@ -14012,6 +14582,78 @@ async function setupPartitioning(client, schema, options = {}) {
14012
14582
  return "native";
14013
14583
  }
14014
14584
  }
14585
+ function retentionCutoff(policy, now = Date.now()) {
14586
+ return new Date(now - parseDuration(policy.maxAge));
14587
+ }
14588
+ async function listChildPartitions(client, schema, table) {
14589
+ const rows = await client.manyOrNone(
14590
+ `SELECT c.relname AS "name", pg_get_expr(c.relpartbound, c.oid) AS "bound"
14591
+ FROM pg_inherits i
14592
+ JOIN pg_class c ON c.oid = i.inhrelid
14593
+ JOIN pg_class p ON p.oid = i.inhparent
14594
+ JOIN pg_namespace n ON n.oid = p.relnamespace
14595
+ WHERE n.nspname = $1 AND p.relname = $2`,
14596
+ [schema, table]
14597
+ );
14598
+ const partitions = [];
14599
+ for (const row of rows ?? []) {
14600
+ const match = /TO \('([^']+)'\)/.exec(row.bound ?? "");
14601
+ if (!match?.[1]) continue;
14602
+ const upperBound = new Date(match[1]);
14603
+ if (Number.isNaN(upperBound.getTime())) continue;
14604
+ partitions.push({ name: row.name, upperBound });
14605
+ }
14606
+ return partitions.sort((a, b) => a.upperBound.getTime() - b.upperBound.getTime());
14607
+ }
14608
+ async function prunePartitionedTable({
14609
+ client,
14610
+ schema,
14611
+ table,
14612
+ cutoff,
14613
+ options
14614
+ }) {
14615
+ const partitions = await listChildPartitions(client, schema, table);
14616
+ const droppable = partitions.filter((p) => p.upperBound.getTime() <= cutoff.getTime());
14617
+ let deleted = 0;
14618
+ let batches = 0;
14619
+ for (const partition of droppable) {
14620
+ if (options?.signal?.aborted) return { deleted, done: false };
14621
+ if (options?.maxBatches !== void 0 && batches >= options.maxBatches) return { deleted, done: false };
14622
+ if (options?.maxRows !== void 0 && deleted >= options.maxRows) return { deleted, done: false };
14623
+ const child = qualifiedName(schema, partition.name);
14624
+ const parent = qualifiedTable(schema, table);
14625
+ const row = await client.one(`SELECT count(*)::int AS "n" FROM ${child}`);
14626
+ await client.none(`ALTER TABLE ${parent} DETACH PARTITION ${child}`);
14627
+ await client.none(`DROP TABLE IF EXISTS ${child}`);
14628
+ deleted += row.n;
14629
+ batches += 1;
14630
+ }
14631
+ return { deleted, done: true };
14632
+ }
14633
+ async function pruneTimescaleTable({
14634
+ client,
14635
+ schema,
14636
+ table,
14637
+ cutoff,
14638
+ options
14639
+ }) {
14640
+ if (options?.signal?.aborted || options?.maxBatches === 0) return { deleted: 0, done: false };
14641
+ const tableExpr = qualifiedTable(schema, table);
14642
+ const chunks = await client.manyOrNone(
14643
+ `SELECT show_chunks($1::regclass, older_than => $2::timestamptz)::text AS "chunk"`,
14644
+ [tableExpr, cutoff.toISOString()]
14645
+ );
14646
+ let deleted = 0;
14647
+ for (const { chunk } of chunks ?? []) {
14648
+ const row = await client.one(`SELECT count(*)::int AS "n" FROM ${chunk}`);
14649
+ deleted += row.n;
14650
+ }
14651
+ await client.none(`SELECT drop_chunks($1::regclass, older_than => $2::timestamptz)`, [
14652
+ tableExpr,
14653
+ cutoff.toISOString()
14654
+ ]);
14655
+ return { deleted, done: true };
14656
+ }
14015
14657
  function applyScoreFilters(acc, filters) {
14016
14658
  applyCommonFilters(acc, filters);
14017
14659
  applySingleOrArrayFilter(acc, "scorerId", filters?.scorerId);
@@ -14758,7 +15400,7 @@ function wrapError(op, error, details) {
14758
15400
  error
14759
15401
  );
14760
15402
  }
14761
- var ObservabilityStoragePostgresVNext = class extends ObservabilityStorage {
15403
+ var ObservabilityStoragePostgresVNext = class _ObservabilityStoragePostgresVNext extends ObservabilityStorage {
14762
15404
  #client;
14763
15405
  #schema;
14764
15406
  #partitioning;
@@ -14842,6 +15484,50 @@ var ObservabilityStoragePostgresVNext = class extends ObservabilityStorage {
14842
15484
  get partitionMode() {
14843
15485
  return this.#partitionMode;
14844
15486
  }
15487
+ // -------------------------------------------------------------------------
15488
+ // Retention
15489
+ // -------------------------------------------------------------------------
15490
+ /**
15491
+ * All five signal tables are insert-only growth tables. The anchor column is
15492
+ * each table's partition / chunk key, so age-based expiry drops whole day
15493
+ * partitions instead of deleting rows (see `./retention.ts`). `indexed: true`
15494
+ * reflects that expiry never scans — it acts on partition bounds.
15495
+ */
15496
+ static retentionTables = {
15497
+ spans: { table: TABLE_SPAN_EVENTS, column: "endedAt", indexed: true },
15498
+ metrics: { table: TABLE_METRIC_EVENTS, column: "timestamp", indexed: true },
15499
+ logs: { table: TABLE_LOG_EVENTS, column: "timestamp", indexed: true },
15500
+ scores: { table: TABLE_SCORE_EVENTS, column: "timestamp", indexed: true },
15501
+ feedback: { table: TABLE_FEEDBACK_EVENTS, column: "timestamp", indexed: true }
15502
+ };
15503
+ /**
15504
+ * Expire signal events older than each table's `maxAge` by dropping whole
15505
+ * day partitions (native / pg_partman) or chunks (Timescale). Only
15506
+ * partitions wholly older than the cutoff are dropped, so the effective
15507
+ * granularity is one day. Each partition drop counts as one batch for
15508
+ * `maxBatches` / `maxRows` / abort-signal purposes; `deleted` reports the
15509
+ * row count of the dropped partitions.
15510
+ */
15511
+ async prune(policies, options) {
15512
+ return this.#run("VNEXT_PRUNE", async () => {
15513
+ const mode = this.#partitionMode ??= await resolveMode(this.#client, this.#partitioning);
15514
+ const results = [];
15515
+ const now = Date.now();
15516
+ for (const [key, entry] of Object.entries(_ObservabilityStoragePostgresVNext.retentionTables)) {
15517
+ const policy = policies[key];
15518
+ if (!policy) continue;
15519
+ if (options?.signal?.aborted) {
15520
+ results.push({ domain: "observability", table: entry.table, deleted: 0, done: false });
15521
+ continue;
15522
+ }
15523
+ const cutoff = retentionCutoff(policy, now);
15524
+ const args = { client: this.#client, schema: this.#schema, table: entry.table, cutoff, options };
15525
+ const outcome = mode === "timescale" ? await pruneTimescaleTable(args) : await prunePartitionedTable(args);
15526
+ results.push({ domain: "observability", table: entry.table, deleted: outcome.deleted, done: outcome.done });
15527
+ }
15528
+ return results;
15529
+ });
15530
+ }
14845
15531
  get observabilityStrategy() {
14846
15532
  return { preferred: "insert-only", supported: ["insert-only"] };
14847
15533
  }
@@ -15786,6 +16472,15 @@ var SchedulesPG = class _SchedulesPG extends SchedulesStorage {
15786
16472
  #indexes;
15787
16473
  /** Tables managed by this domain */
15788
16474
  static MANAGED_TABLES = [TABLE_SCHEDULES, TABLE_SCHEDULE_TRIGGERS];
16475
+ /**
16476
+ * The fire/run history (`schedule_triggers`, one row per fire) is the growth
16477
+ * table; schedule definitions are config and excluded. Anchored on
16478
+ * `actual_fire_at`, a bigint epoch-ms column (numeric comparison, not
16479
+ * timestamptz).
16480
+ */
16481
+ static retentionTables = {
16482
+ triggers: { table: TABLE_SCHEDULE_TRIGGERS, column: "actual_fire_at", indexed: true, anchorType: "epoch-ms" }
16483
+ };
15789
16484
  constructor(config) {
15790
16485
  super();
15791
16486
  const { client, schemaName, skipDefaultIndexes, indexes } = resolvePgConfig(config);
@@ -15807,6 +16502,43 @@ var SchedulesPG = class _SchedulesPG extends SchedulesStorage {
15807
16502
  await this.createDefaultIndexes();
15808
16503
  await this.createCustomIndexes();
15809
16504
  }
16505
+ /**
16506
+ * Lazily ensures a btree index exists on each configured policy's retention
16507
+ * anchor column so age-based `prune()` deletes stay fast. The default
16508
+ * composite index leads with `schedule_id`, so a bare `actual_fire_at` range
16509
+ * scan can't use it. Called from the prune path (not init) so only
16510
+ * deployments that configure retention pay the index's write/disk overhead.
16511
+ * Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
16512
+ * so its supporting index is not part of the default index set.
16513
+ */
16514
+ async ensureRetentionIndexes(policies) {
16515
+ const prefix = this.#schema !== "public" ? `${this.#schema}_` : "";
16516
+ for (const [key, entry] of Object.entries(_SchedulesPG.retentionTables)) {
16517
+ if (!entry.indexed || !policies[key]) continue;
16518
+ try {
16519
+ await this.#db.ensureIndex({
16520
+ indexName: `${prefix}mastra_${key}_retention_idx`,
16521
+ tableName: entry.table,
16522
+ column: entry.column
16523
+ });
16524
+ } catch (error) {
16525
+ this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
16526
+ }
16527
+ }
16528
+ }
16529
+ /**
16530
+ * Delete trigger (fire history) rows whose `actual_fire_at` is older than the
16531
+ * `triggers` policy's `maxAge`, batched. Schedule definitions are never pruned.
16532
+ */
16533
+ async prune(policies, options) {
16534
+ await this.ensureRetentionIndexes(policies);
16535
+ const targets = resolveTargets({
16536
+ policies,
16537
+ descriptor: _SchedulesPG.retentionTables,
16538
+ order: ["triggers"]
16539
+ });
16540
+ return runPrune({ db: this.#db, domain: "schedules", targets, options });
16541
+ }
15810
16542
  /**
15811
16543
  * Returns default index definitions for the schedules domain.
15812
16544
  * @param schemaPrefix - Prefix for index names (e.g. "my_schema_" or "")
@@ -16139,6 +16871,11 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends ScorerDefinitionsSt
16139
16871
  tableName: TABLE_SCORER_DEFINITIONS,
16140
16872
  schema: TABLE_SCHEMAS[TABLE_SCORER_DEFINITIONS]
16141
16873
  });
16874
+ await this.#db.alterTable({
16875
+ tableName: TABLE_SCORER_DEFINITIONS,
16876
+ schema: TABLE_SCHEMAS[TABLE_SCORER_DEFINITIONS],
16877
+ ifNotExists: ["organizationId", "projectId"]
16878
+ });
16142
16879
  await this.#db.createTable({
16143
16880
  tableName: TABLE_SCORER_DEFINITION_VERSIONS,
16144
16881
  schema: TABLE_SCHEMAS[TABLE_SCORER_DEFINITION_VERSIONS]
@@ -16194,14 +16931,16 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends ScorerDefinitionsSt
16194
16931
  const nowIso = now.toISOString();
16195
16932
  await this.#db.client.none(
16196
16933
  `INSERT INTO ${tableName} (
16197
- id, status, "activeVersionId", "authorId", metadata,
16934
+ id, status, "activeVersionId", "authorId", "organizationId", "projectId", metadata,
16198
16935
  "createdAt", "createdAtZ", "updatedAt", "updatedAtZ"
16199
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
16936
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
16200
16937
  [
16201
16938
  scorerDefinition.id,
16202
16939
  "draft",
16203
16940
  null,
16204
16941
  scorerDefinition.authorId ?? null,
16942
+ scorerDefinition.organizationId ?? null,
16943
+ scorerDefinition.projectId ?? null,
16205
16944
  scorerDefinition.metadata ? JSON.stringify(scorerDefinition.metadata) : null,
16206
16945
  nowIso,
16207
16946
  nowIso,
@@ -16209,7 +16948,14 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends ScorerDefinitionsSt
16209
16948
  nowIso
16210
16949
  ]
16211
16950
  );
16212
- const { id: _id, authorId: _authorId, metadata: _metadata, ...snapshotConfig } = scorerDefinition;
16951
+ const {
16952
+ id: _id,
16953
+ authorId: _authorId,
16954
+ organizationId: _organizationId,
16955
+ projectId: _projectId,
16956
+ metadata: _metadata,
16957
+ ...snapshotConfig
16958
+ } = scorerDefinition;
16213
16959
  const versionId = crypto.randomUUID();
16214
16960
  await this.createVersion({
16215
16961
  id: versionId,
@@ -16224,6 +16970,8 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends ScorerDefinitionsSt
16224
16970
  status: "draft",
16225
16971
  activeVersionId: void 0,
16226
16972
  authorId: scorerDefinition.authorId,
16973
+ organizationId: scorerDefinition.organizationId,
16974
+ projectId: scorerDefinition.projectId,
16227
16975
  metadata: scorerDefinition.metadata,
16228
16976
  createdAt: now,
16229
16977
  updatedAt: now
@@ -16337,7 +17085,16 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends ScorerDefinitionsSt
16337
17085
  }
16338
17086
  }
16339
17087
  async list(args) {
16340
- const { page = 0, perPage: perPageInput, orderBy, authorId, metadata, status } = args || {};
17088
+ const {
17089
+ page = 0,
17090
+ perPage: perPageInput,
17091
+ orderBy,
17092
+ authorId,
17093
+ organizationId,
17094
+ projectId,
17095
+ metadata,
17096
+ status
17097
+ } = args || {};
16341
17098
  const { field, direction } = this.parseOrderBy(orderBy);
16342
17099
  if (page < 0) {
16343
17100
  throw new MastraError(
@@ -16365,6 +17122,14 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends ScorerDefinitionsSt
16365
17122
  conditions.push(`"authorId" = $${paramIdx++}`);
16366
17123
  queryParams.push(authorId);
16367
17124
  }
17125
+ if (organizationId !== void 0) {
17126
+ conditions.push(`"organizationId" = $${paramIdx++}`);
17127
+ queryParams.push(organizationId);
17128
+ }
17129
+ if (projectId !== void 0) {
17130
+ conditions.push(`"projectId" = $${paramIdx++}`);
17131
+ queryParams.push(projectId);
17132
+ }
16368
17133
  if (metadata && Object.keys(metadata).length > 0) {
16369
17134
  conditions.push(`metadata @> $${paramIdx++}::jsonb`);
16370
17135
  queryParams.push(JSON.stringify(metadata));
@@ -16691,6 +17456,8 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends ScorerDefinitionsSt
16691
17456
  status: row.status,
16692
17457
  activeVersionId: row.activeVersionId,
16693
17458
  authorId: row.authorId,
17459
+ organizationId: row.organizationId,
17460
+ projectId: row.projectId,
16694
17461
  metadata: parseJsonResilient(row.metadata),
16695
17462
  createdAt: new Date(row.createdAtZ || row.createdAt),
16696
17463
  updatedAt: new Date(row.updatedAtZ || row.updatedAt)
@@ -16715,6 +17482,18 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends ScorerDefinitionsSt
16715
17482
  };
16716
17483
  }
16717
17484
  };
17485
+ function applyTenancyFilters(conditions, queryParams, startIndex, filters) {
17486
+ let paramIndex = startIndex;
17487
+ if (filters?.organizationId !== void 0) {
17488
+ conditions.push(`"organizationId" = $${paramIndex++}`);
17489
+ queryParams.push(filters.organizationId);
17490
+ }
17491
+ if (filters?.projectId !== void 0) {
17492
+ conditions.push(`"projectId" = $${paramIndex++}`);
17493
+ queryParams.push(filters.projectId);
17494
+ }
17495
+ return paramIndex;
17496
+ }
16718
17497
  function getSchemaName6(schema) {
16719
17498
  return schema ? `"${schema}"` : '"public"';
16720
17499
  }
@@ -16737,6 +17516,13 @@ var ScoresPG = class _ScoresPG extends ScoresStorage {
16737
17516
  #indexes;
16738
17517
  /** Tables managed by this domain */
16739
17518
  static MANAGED_TABLES = [TABLE_SCORERS];
17519
+ /**
17520
+ * Scorer results accumulate as evals run. Single table, anchored on the
17521
+ * timezone-aware `createdAtZ` mirror column (kept in sync by triggers).
17522
+ */
17523
+ static retentionTables = {
17524
+ scorers: { table: TABLE_SCORERS, column: "createdAtZ", indexed: true }
17525
+ };
16740
17526
  constructor(config) {
16741
17527
  super();
16742
17528
  const { client, schemaName, skipDefaultIndexes, indexes } = resolvePgConfig(config);
@@ -16750,11 +17536,35 @@ var ScoresPG = class _ScoresPG extends ScoresStorage {
16750
17536
  await this.#db.alterTable({
16751
17537
  tableName: TABLE_SCORERS,
16752
17538
  schema: TABLE_SCHEMAS[TABLE_SCORERS],
16753
- ifNotExists: ["spanId", "requestContext"]
17539
+ ifNotExists: ["spanId", "requestContext", "organizationId", "projectId", "batchId", "datasetId", "datasetItemId"]
16754
17540
  });
16755
17541
  await this.createDefaultIndexes();
16756
17542
  await this.createCustomIndexes();
16757
17543
  }
17544
+ /**
17545
+ * Lazily ensures a btree index exists on each configured policy's retention
17546
+ * anchor column so age-based `prune()` deletes stay fast on large tables.
17547
+ * Called from the prune path (not init) so only deployments that configure
17548
+ * retention pay the index's write/disk overhead. Best-effort: failures are
17549
+ * logged and pruning proceeds (correct, just slower).
17550
+ * Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
17551
+ * so its supporting index is not part of the default index set.
17552
+ */
17553
+ async ensureRetentionIndexes(policies) {
17554
+ const prefix = this.#schema && this.#schema !== "public" ? `${this.#schema}_` : "";
17555
+ for (const [key, entry] of Object.entries(_ScoresPG.retentionTables)) {
17556
+ if (!entry.indexed || !policies[key]) continue;
17557
+ try {
17558
+ await this.#db.ensureIndex({
17559
+ indexName: `${prefix}mastra_${key}_retention_idx`,
17560
+ tableName: entry.table,
17561
+ column: entry.column
17562
+ });
17563
+ } catch (error) {
17564
+ this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
17565
+ }
17566
+ }
17567
+ }
16758
17568
  /**
16759
17569
  * Returns default index definitions for the scores domain tables.
16760
17570
  * @param schemaPrefix - Prefix for index names (e.g. "my_schema_" or "")
@@ -16829,6 +17639,16 @@ var ScoresPG = class _ScoresPG extends ScoresStorage {
16829
17639
  async dangerouslyClearAll() {
16830
17640
  await this.#db.clearTable({ tableName: TABLE_SCORERS });
16831
17641
  }
17642
+ /** Delete scorer results older than the `scorers` policy's `maxAge`, batched. */
17643
+ async prune(policies, options) {
17644
+ await this.ensureRetentionIndexes(policies);
17645
+ const targets = resolveTargets({
17646
+ policies,
17647
+ descriptor: _ScoresPG.retentionTables,
17648
+ order: ["scorers"]
17649
+ });
17650
+ return runPrune({ db: this.#db, domain: "scores", targets, options });
17651
+ }
16832
17652
  async getScoreById({ id }) {
16833
17653
  try {
16834
17654
  const result = await this.#db.client.oneOrNone(
@@ -16852,7 +17672,8 @@ var ScoresPG = class _ScoresPG extends ScoresStorage {
16852
17672
  pagination,
16853
17673
  entityId,
16854
17674
  entityType,
16855
- source
17675
+ source,
17676
+ filters
16856
17677
  }) {
16857
17678
  try {
16858
17679
  const conditions = [`"scorerId" = $1`];
@@ -16870,6 +17691,7 @@ var ScoresPG = class _ScoresPG extends ScoresStorage {
16870
17691
  conditions.push(`"source" = $${paramIndex++}`);
16871
17692
  queryParams.push(source);
16872
17693
  }
17694
+ paramIndex = applyTenancyFilters(conditions, queryParams, paramIndex, filters);
16873
17695
  const whereClause = conditions.join(" AND ");
16874
17696
  const total = await this.#db.client.oneOrNone(
16875
17697
  `SELECT COUNT(*) FROM ${getTableName6({ indexName: TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE ${whereClause}`,
@@ -16983,12 +17805,17 @@ var ScoresPG = class _ScoresPG extends ScoresStorage {
16983
17805
  }
16984
17806
  async listScoresByRunId({
16985
17807
  runId,
16986
- pagination
17808
+ pagination,
17809
+ filters
16987
17810
  }) {
16988
17811
  try {
17812
+ const conditions = [`"runId" = $1`];
17813
+ const queryParams = [runId];
17814
+ let paramIndex = applyTenancyFilters(conditions, queryParams, 2, filters);
17815
+ const whereClause = conditions.join(" AND ");
16989
17816
  const total = await this.#db.client.oneOrNone(
16990
- `SELECT COUNT(*) FROM ${getTableName6({ indexName: TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE "runId" = $1`,
16991
- [runId]
17817
+ `SELECT COUNT(*) FROM ${getTableName6({ indexName: TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE ${whereClause}`,
17818
+ queryParams
16992
17819
  );
16993
17820
  const { page, perPage: perPageInput } = pagination;
16994
17821
  const perPage = normalizePerPage(perPageInput, 100);
@@ -17007,8 +17834,8 @@ var ScoresPG = class _ScoresPG extends ScoresStorage {
17007
17834
  const limitValue = perPageInput === false ? Number(total?.count) : perPage;
17008
17835
  const end = perPageInput === false ? Number(total?.count) : start + perPage;
17009
17836
  const result = await this.#db.client.manyOrNone(
17010
- `SELECT * FROM ${getTableName6({ indexName: TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE "runId" = $1 LIMIT $2 OFFSET $3`,
17011
- [runId, limitValue, start]
17837
+ `SELECT * FROM ${getTableName6({ indexName: TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE ${whereClause} ORDER BY "createdAt" DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
17838
+ [...queryParams, limitValue, start]
17012
17839
  );
17013
17840
  return {
17014
17841
  pagination: {
@@ -17033,12 +17860,17 @@ var ScoresPG = class _ScoresPG extends ScoresStorage {
17033
17860
  async listScoresByEntityId({
17034
17861
  entityId,
17035
17862
  entityType,
17036
- pagination
17863
+ pagination,
17864
+ filters
17037
17865
  }) {
17038
17866
  try {
17867
+ const conditions = [`"entityId" = $1`, `"entityType" = $2`];
17868
+ const queryParams = [entityId, entityType];
17869
+ let paramIndex = applyTenancyFilters(conditions, queryParams, 3, filters);
17870
+ const whereClause = conditions.join(" AND ");
17039
17871
  const total = await this.#db.client.oneOrNone(
17040
- `SELECT COUNT(*) FROM ${getTableName6({ indexName: TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE "entityId" = $1 AND "entityType" = $2`,
17041
- [entityId, entityType]
17872
+ `SELECT COUNT(*) FROM ${getTableName6({ indexName: TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE ${whereClause}`,
17873
+ queryParams
17042
17874
  );
17043
17875
  const { page, perPage: perPageInput } = pagination;
17044
17876
  const perPage = normalizePerPage(perPageInput, 100);
@@ -17057,8 +17889,8 @@ var ScoresPG = class _ScoresPG extends ScoresStorage {
17057
17889
  const limitValue = perPageInput === false ? Number(total?.count) : perPage;
17058
17890
  const end = perPageInput === false ? Number(total?.count) : start + perPage;
17059
17891
  const result = await this.#db.client.manyOrNone(
17060
- `SELECT * FROM ${getTableName6({ indexName: TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE "entityId" = $1 AND "entityType" = $2 LIMIT $3 OFFSET $4`,
17061
- [entityId, entityType, limitValue, start]
17892
+ `SELECT * FROM ${getTableName6({ indexName: TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE ${whereClause} ORDER BY "createdAt" DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
17893
+ [...queryParams, limitValue, start]
17062
17894
  );
17063
17895
  return {
17064
17896
  pagination: {
@@ -17083,13 +17915,18 @@ var ScoresPG = class _ScoresPG extends ScoresStorage {
17083
17915
  async listScoresBySpan({
17084
17916
  traceId,
17085
17917
  spanId,
17086
- pagination
17918
+ pagination,
17919
+ filters
17087
17920
  }) {
17088
17921
  try {
17089
17922
  const tableName = getTableName6({ indexName: TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) });
17923
+ const conditions = [`"traceId" = $1`, `"spanId" = $2`];
17924
+ const queryParams = [traceId, spanId];
17925
+ let paramIndex = applyTenancyFilters(conditions, queryParams, 3, filters);
17926
+ const whereClause = conditions.join(" AND ");
17090
17927
  const countSQLResult = await this.#db.client.oneOrNone(
17091
- `SELECT COUNT(*) as count FROM ${tableName} WHERE "traceId" = $1 AND "spanId" = $2`,
17092
- [traceId, spanId]
17928
+ `SELECT COUNT(*) as count FROM ${tableName} WHERE ${whereClause}`,
17929
+ queryParams
17093
17930
  );
17094
17931
  const total = Number(countSQLResult?.count ?? 0);
17095
17932
  const { page, perPage: perPageInput } = pagination;
@@ -17098,8 +17935,8 @@ var ScoresPG = class _ScoresPG extends ScoresStorage {
17098
17935
  const limitValue = perPageInput === false ? total : perPage;
17099
17936
  const end = perPageInput === false ? total : start + perPage;
17100
17937
  const result = await this.#db.client.manyOrNone(
17101
- `SELECT * FROM ${tableName} WHERE "traceId" = $1 AND "spanId" = $2 ORDER BY "createdAt" DESC LIMIT $3 OFFSET $4`,
17102
- [traceId, spanId, limitValue, start]
17938
+ `SELECT * FROM ${tableName} WHERE ${whereClause} ORDER BY "createdAt" DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
17939
+ [...queryParams, limitValue, start]
17103
17940
  );
17104
17941
  const hasMore = end < total;
17105
17942
  const scores = result.map((row) => transformScoreRow(row)) ?? [];
@@ -18150,6 +18987,14 @@ var WorkflowsPG = class _WorkflowsPG extends WorkflowsStorage {
18150
18987
  #indexes;
18151
18988
  /** Tables managed by this domain */
18152
18989
  static MANAGED_TABLES = [TABLE_WORKFLOW_SNAPSHOT];
18990
+ /**
18991
+ * Workflow run snapshots accumulate as runs execute. Anchored on the
18992
+ * timezone-aware `updatedAtZ` mirror column (last activity) so suspended or
18993
+ * long-running runs are not pruned by start age.
18994
+ */
18995
+ static retentionTables = {
18996
+ workflowSnapshot: { table: TABLE_WORKFLOW_SNAPSHOT, column: "updatedAtZ", indexed: true }
18997
+ };
18153
18998
  constructor(config) {
18154
18999
  super();
18155
19000
  const { client, schemaName, skipDefaultIndexes, indexes } = resolvePgConfig(config);
@@ -18221,6 +19066,40 @@ var WorkflowsPG = class _WorkflowsPG extends WorkflowsStorage {
18221
19066
  await this.createDefaultIndexes();
18222
19067
  await this.createCustomIndexes();
18223
19068
  }
19069
+ /**
19070
+ * Lazily ensures a btree index exists on each configured policy's retention
19071
+ * anchor column so age-based `prune()` deletes stay fast on large tables.
19072
+ * Called from the prune path (not init) so only deployments that configure
19073
+ * retention pay the index's write/disk overhead. Best-effort: failures are
19074
+ * logged and pruning proceeds (correct, just slower).
19075
+ * Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
19076
+ * so its supporting index is not part of the default index set.
19077
+ */
19078
+ async ensureRetentionIndexes(policies) {
19079
+ const prefix = this.#schema && this.#schema !== "public" ? `${this.#schema}_` : "";
19080
+ for (const [key, entry] of Object.entries(_WorkflowsPG.retentionTables)) {
19081
+ if (!entry.indexed || !policies[key]) continue;
19082
+ try {
19083
+ await this.#db.ensureIndex({
19084
+ indexName: `${prefix}mastra_${key}_retention_idx`,
19085
+ tableName: entry.table,
19086
+ column: entry.column
19087
+ });
19088
+ } catch (error) {
19089
+ this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
19090
+ }
19091
+ }
19092
+ }
19093
+ /** Delete workflow run snapshots older than the `workflowSnapshot` policy's `maxAge`, batched. */
19094
+ async prune(policies, options) {
19095
+ await this.ensureRetentionIndexes(policies);
19096
+ const targets = resolveTargets({
19097
+ policies,
19098
+ descriptor: _WorkflowsPG.retentionTables,
19099
+ order: ["workflowSnapshot"]
19100
+ });
19101
+ return runPrune({ db: this.#db, domain: "workflows", targets, options });
19102
+ }
18224
19103
  /**
18225
19104
  * Creates custom user-defined indexes for this domain's tables.
18226
19105
  */
@@ -19334,7 +20213,7 @@ var PostgresStore = class extends MastraCompositeStore {
19334
20213
  constructor(config) {
19335
20214
  try {
19336
20215
  validateConfig("PostgresStore", config);
19337
- super({ id: config.id, name: "PostgresStore", disableInit: config.disableInit });
20216
+ super({ id: config.id, name: "PostgresStore", disableInit: config.disableInit, retention: config.retention });
19338
20217
  this.schema = parseSqlIdentifier(config.schemaName || "public", "schema name");
19339
20218
  if (isPoolConfig(config)) {
19340
20219
  this.#pool = config.pool;