@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.cjs CHANGED
@@ -2542,6 +2542,11 @@ CREATE TRIGGER ${triggerName}
2542
2542
  EXECUTE FUNCTION ${functionName}();`;
2543
2543
  }
2544
2544
  var schemaSetupRegistry = /* @__PURE__ */ new Map();
2545
+ function assertPositiveLimit(limit) {
2546
+ if (!Number.isSafeInteger(limit) || limit <= 0) {
2547
+ throw new Error(`prune limit must be a positive integer; received ${limit}`);
2548
+ }
2549
+ }
2545
2550
  var PgDB = class extends base.MastraBase {
2546
2551
  client;
2547
2552
  schemaName;
@@ -3578,6 +3583,96 @@ Note: This migration may take some time for large tables.
3578
3583
  async deleteData({ tableName }) {
3579
3584
  return this.clearTable({ tableName });
3580
3585
  }
3586
+ // ---------------------------------------------------------------------------
3587
+ // Retention helpers (prune)
3588
+ // ---------------------------------------------------------------------------
3589
+ /**
3590
+ * Deletes up to `limit` rows from `tableName` whose `column` value is strictly
3591
+ * older than `cutoff`, in a single bounded statement. Returns the number of
3592
+ * rows deleted so the caller's batch loop can decide whether the table is
3593
+ * drained.
3594
+ *
3595
+ * PostgreSQL has no `DELETE ... LIMIT`, so this targets a bounded set of
3596
+ * physical rows via the `ctid` system column (PG's row identity, analogous to
3597
+ * SQLite's `rowid`). `cutoff` is bound as a parameter — a `Date`/ISO-8601
3598
+ * string compared against a `timestamptz` anchor column, or a `number`
3599
+ * compared against a `bigint` epoch-ms anchor column.
3600
+ */
3601
+ async pruneBatch({
3602
+ tableName,
3603
+ column,
3604
+ cutoff,
3605
+ limit
3606
+ }) {
3607
+ assertPositiveLimit(limit);
3608
+ const fullTableName = getTableName({ indexName: tableName, schemaName: getSchemaName(this.schemaName) });
3609
+ const parsedColumn = `"${utils.parseSqlIdentifier(column, "column name")}"`;
3610
+ const sql = `
3611
+ DELETE FROM ${fullTableName}
3612
+ WHERE ctid IN (
3613
+ SELECT ctid FROM ${fullTableName}
3614
+ WHERE ${parsedColumn} < $1
3615
+ LIMIT $2
3616
+ )
3617
+ `;
3618
+ const result = await this.client.query(sql, [cutoff, limit]);
3619
+ return result.rowCount ?? 0;
3620
+ }
3621
+ /**
3622
+ * Deletes up to `limit` aged parent rows *and* their child rows together, in
3623
+ * a single transaction (used by whole-unit pruning such as experiments →
3624
+ * experiment_results). The aged parent IDs are selected first and both
3625
+ * deletes target that exact ID set, so a bound or abort between batches never
3626
+ * leaves a parent hollow (kept, but with its children gone) or children
3627
+ * orphaned.
3628
+ */
3629
+ async pruneUnitsBatch({
3630
+ parentTable,
3631
+ parentKey,
3632
+ parentColumn,
3633
+ childTable,
3634
+ childForeignKey,
3635
+ cutoff,
3636
+ limit
3637
+ }) {
3638
+ assertPositiveLimit(limit);
3639
+ const schemaName = getSchemaName(this.schemaName);
3640
+ const fullChildTable = getTableName({ indexName: childTable, schemaName });
3641
+ const fullParentTable = getTableName({ indexName: parentTable, schemaName });
3642
+ const childFk = `"${utils.parseSqlIdentifier(childForeignKey, "column name")}"`;
3643
+ const parentPk = `"${utils.parseSqlIdentifier(parentKey, "column name")}"`;
3644
+ const parentCol = `"${utils.parseSqlIdentifier(parentColumn, "column name")}"`;
3645
+ return this.client.tx(async (t) => {
3646
+ const rows = await t.manyOrNone(
3647
+ `SELECT ${parentPk} AS id FROM ${fullParentTable} WHERE ${parentCol} < $1 LIMIT $2 FOR UPDATE SKIP LOCKED`,
3648
+ [cutoff, limit]
3649
+ );
3650
+ const ids = rows.map((r) => r.id);
3651
+ if (ids.length === 0) return { parents: 0, children: 0 };
3652
+ const childResult = await t.query(`DELETE FROM ${fullChildTable} WHERE ${childFk} = ANY($1)`, [ids]);
3653
+ const parentResult = await t.query(`DELETE FROM ${fullParentTable} WHERE ${parentPk} = ANY($1)`, [ids]);
3654
+ return {
3655
+ parents: parentResult.rowCount ?? 0,
3656
+ children: childResult.rowCount ?? 0
3657
+ };
3658
+ });
3659
+ }
3660
+ /**
3661
+ * Creates a btree index on `column` for `tableName` if it does not already
3662
+ * exist, so age-based prune deletes stay fast. Delegates to {@link createIndex}
3663
+ * (which is a no-op when the index is present).
3664
+ *
3665
+ * The name is lowercased and truncated to Postgres' 63-byte identifier limit
3666
+ * (schema-prefixed names can exceed it), mirroring {@link buildConstraintName}.
3667
+ */
3668
+ async ensureIndex({
3669
+ indexName,
3670
+ tableName,
3671
+ column
3672
+ }) {
3673
+ const name = buildConstraintName({ baseName: indexName });
3674
+ await this.createIndex({ name, table: tableName, columns: [column] });
3675
+ }
3581
3676
  async executeUpdate(client, {
3582
3677
  tableName,
3583
3678
  keys,
@@ -3653,6 +3748,20 @@ function transformFromSqlRow({
3653
3748
  });
3654
3749
  return result;
3655
3750
  }
3751
+ function tenancyWhere(filters, startIndex) {
3752
+ const conditions = [];
3753
+ const params = [];
3754
+ let idx = startIndex;
3755
+ if (filters?.organizationId !== void 0) {
3756
+ conditions.push(`"organizationId" = $${idx++}`);
3757
+ params.push(filters.organizationId);
3758
+ }
3759
+ if (filters?.projectId !== void 0) {
3760
+ conditions.push(`"projectId" = $${idx++}`);
3761
+ params.push(filters.projectId);
3762
+ }
3763
+ return { conditions, params, nextIndex: idx };
3764
+ }
3656
3765
 
3657
3766
  // src/storage/domains/agents/index.ts
3658
3767
  var AgentsPG = class _AgentsPG extends storage.AgentsStorage {
@@ -4536,6 +4645,98 @@ var AgentsPG = class _AgentsPG extends storage.AgentsStorage {
4536
4645
  };
4537
4646
  }
4538
4647
  };
4648
+ var DEFAULT_BATCH_SIZE = 1e3;
4649
+ async function sleep(ms, signal) {
4650
+ if (ms <= 0 || signal?.aborted) return;
4651
+ await new Promise((resolve) => {
4652
+ const timer = setTimeout(() => {
4653
+ signal?.removeEventListener("abort", onAbort);
4654
+ resolve();
4655
+ }, ms);
4656
+ const onAbort = () => {
4657
+ clearTimeout(timer);
4658
+ resolve();
4659
+ };
4660
+ signal?.addEventListener("abort", onAbort, { once: true });
4661
+ });
4662
+ }
4663
+ function cutoffFor(policy, anchorType, now = Date.now()) {
4664
+ const cutoffMs = now - storage.parseDuration(policy.maxAge);
4665
+ return anchorType === "epoch-ms" ? cutoffMs : new Date(cutoffMs);
4666
+ }
4667
+ async function runBatchedDelete({
4668
+ deleteBatch,
4669
+ batchSize,
4670
+ options
4671
+ }) {
4672
+ if (!Number.isSafeInteger(batchSize) || batchSize <= 0) {
4673
+ throw new Error(`retention batchSize must be a positive integer; received ${batchSize}`);
4674
+ }
4675
+ let deleted = 0;
4676
+ let batches = 0;
4677
+ while (true) {
4678
+ if (options?.signal?.aborted) return { deleted, done: false };
4679
+ if (options?.maxBatches !== void 0 && batches >= options.maxBatches) return { deleted, done: false };
4680
+ let limit = batchSize;
4681
+ if (options?.maxRows !== void 0) {
4682
+ const remaining = options.maxRows - deleted;
4683
+ if (remaining <= 0) return { deleted, done: false };
4684
+ limit = Math.min(limit, remaining);
4685
+ }
4686
+ const affected = await deleteBatch(limit);
4687
+ deleted += affected;
4688
+ batches += 1;
4689
+ if (affected < limit) return { deleted, done: true };
4690
+ if (options?.pauseMs) {
4691
+ await sleep(options.pauseMs, options.signal);
4692
+ }
4693
+ }
4694
+ }
4695
+ async function runPrune({
4696
+ db,
4697
+ domain,
4698
+ targets,
4699
+ options
4700
+ }) {
4701
+ const results = [];
4702
+ const now = Date.now();
4703
+ for (const target of targets) {
4704
+ if (options?.signal?.aborted) {
4705
+ results.push({ domain, table: target.table, deleted: 0, done: false });
4706
+ continue;
4707
+ }
4708
+ const cutoff = cutoffFor(target.policy, target.anchorType, now);
4709
+ const batchSize = target.policy.batchSize ?? DEFAULT_BATCH_SIZE;
4710
+ const { deleted, done } = await runBatchedDelete({
4711
+ deleteBatch: (limit) => db.pruneBatch({ tableName: target.table, column: target.column, cutoff, limit }),
4712
+ batchSize,
4713
+ options
4714
+ });
4715
+ results.push({ domain, table: target.table, deleted, done });
4716
+ }
4717
+ return results;
4718
+ }
4719
+ function resolveTargets({
4720
+ policies,
4721
+ descriptor,
4722
+ order
4723
+ }) {
4724
+ const targets = [];
4725
+ for (const key of order) {
4726
+ const policy = policies[key];
4727
+ const entry = descriptor[key];
4728
+ if (!policy || !entry) continue;
4729
+ targets.push({
4730
+ table: entry.table,
4731
+ column: entry.column,
4732
+ anchorType: entry.anchorType ?? "timestamp",
4733
+ policy
4734
+ });
4735
+ }
4736
+ return targets;
4737
+ }
4738
+
4739
+ // src/storage/domains/background-tasks/index.ts
4539
4740
  function getSchemaName3(schema) {
4540
4741
  return schema ? `"${schema}"` : '"public"';
4541
4742
  }
@@ -4590,6 +4791,14 @@ var BackgroundTasksPG = class _BackgroundTasksPG extends storage.BackgroundTasks
4590
4791
  #skipDefaultIndexes;
4591
4792
  #indexes;
4592
4793
  static MANAGED_TABLES = [storage.TABLE_BACKGROUND_TASKS];
4794
+ /**
4795
+ * Completed background tasks accumulate as dead weight. Anchored on the
4796
+ * timezone-aware `completedAtZ` mirror column: NULL (in-flight) rows are
4797
+ * excluded automatically since `completedAtZ < cutoff` is false for NULL.
4798
+ */
4799
+ static retentionTables = {
4800
+ backgroundTasks: { table: storage.TABLE_BACKGROUND_TASKS, column: "completedAtZ", indexed: true }
4801
+ };
4593
4802
  constructor(config) {
4594
4803
  super();
4595
4804
  const { client, schemaName, skipDefaultIndexes, indexes } = resolvePgConfig(config);
@@ -4611,6 +4820,40 @@ var BackgroundTasksPG = class _BackgroundTasksPG extends storage.BackgroundTasks
4611
4820
  await this.createDefaultIndexes();
4612
4821
  await this.createCustomIndexes();
4613
4822
  }
4823
+ /**
4824
+ * Lazily ensures a btree index exists on each configured policy's retention
4825
+ * anchor column so age-based `prune()` deletes stay fast on large tables.
4826
+ * Called from the prune path (not init) so only deployments that configure
4827
+ * retention pay the index's write/disk overhead. Best-effort: failures are
4828
+ * logged and pruning proceeds (correct, just slower).
4829
+ * Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
4830
+ * so its supporting index is not part of the default index set.
4831
+ */
4832
+ async ensureRetentionIndexes(policies) {
4833
+ const prefix = this.#schema !== "public" ? `${this.#schema}_` : "";
4834
+ for (const [key, entry] of Object.entries(_BackgroundTasksPG.retentionTables)) {
4835
+ if (!entry.indexed || !policies[key]) continue;
4836
+ try {
4837
+ await this.#db.ensureIndex({
4838
+ indexName: `${prefix}mastra_${key}_retention_idx`,
4839
+ tableName: entry.table,
4840
+ column: entry.column
4841
+ });
4842
+ } catch (error) {
4843
+ this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
4844
+ }
4845
+ }
4846
+ }
4847
+ /** Delete completed tasks older than the `backgroundTasks` policy's `maxAge`, batched. */
4848
+ async prune(policies, options) {
4849
+ await this.ensureRetentionIndexes(policies);
4850
+ const targets = resolveTargets({
4851
+ policies,
4852
+ descriptor: _BackgroundTasksPG.retentionTables,
4853
+ order: ["backgroundTasks"]
4854
+ });
4855
+ return runPrune({ db: this.#db, domain: "backgroundTasks", targets, options });
4856
+ }
4614
4857
  static getDefaultIndexDefs(schemaPrefix) {
4615
4858
  return [
4616
4859
  {
@@ -5374,8 +5617,8 @@ var DatasetsPG = class _DatasetsPG extends storage.DatasetsStorage {
5374
5617
  groundTruthSchema: input.groundTruthSchema ?? null,
5375
5618
  requestContextSchema: input.requestContextSchema ?? null,
5376
5619
  targetType: input.targetType ?? null,
5377
- targetIds: input.targetIds !== void 0 ? JSON.stringify(input.targetIds) : null,
5378
- scorerIds: input.scorerIds ? JSON.stringify(input.scorerIds) : null,
5620
+ targetIds: input.targetIds ?? null,
5621
+ scorerIds: input.scorerIds ?? null,
5379
5622
  organizationId: input.organizationId ?? null,
5380
5623
  projectId: input.projectId ?? null,
5381
5624
  candidateKey: input.candidateKey ?? null,
@@ -5415,10 +5658,15 @@ var DatasetsPG = class _DatasetsPG extends storage.DatasetsStorage {
5415
5658
  );
5416
5659
  }
5417
5660
  }
5418
- async getDatasetById({ id }) {
5661
+ async getDatasetById({
5662
+ id,
5663
+ filters
5664
+ }) {
5419
5665
  try {
5420
5666
  const tableName = getTableName2({ indexName: storage.TABLE_DATASETS, schemaName: getSchemaName2(this.#schema) });
5421
- const result = await this.#db.client.oneOrNone(`SELECT * FROM ${tableName} WHERE "id" = $1`, [id]);
5667
+ const { conditions, params } = tenancyWhere(filters, 2);
5668
+ const whereSql = ['"id" = $1', ...conditions].join(" AND ");
5669
+ const result = await this.#db.client.oneOrNone(`SELECT * FROM ${tableName} WHERE ${whereSql}`, [id, ...params]);
5422
5670
  return result ? this.transformDatasetRow(result) : null;
5423
5671
  } catch (error$1) {
5424
5672
  throw new error.MastraError(
@@ -5433,7 +5681,7 @@ var DatasetsPG = class _DatasetsPG extends storage.DatasetsStorage {
5433
5681
  }
5434
5682
  async _doUpdateDataset(args) {
5435
5683
  try {
5436
- const existing = await this.getDatasetById({ id: args.id });
5684
+ const existing = await this.getDatasetById({ id: args.id, filters: args.filters });
5437
5685
  if (!existing) {
5438
5686
  throw new error.MastraError({
5439
5687
  id: storage.createStorageErrorId("PG", "UPDATE_DATASET", "NOT_FOUND"),
@@ -5522,7 +5770,7 @@ var DatasetsPG = class _DatasetsPG extends storage.DatasetsStorage {
5522
5770
  );
5523
5771
  }
5524
5772
  }
5525
- async deleteDataset({ id }) {
5773
+ async deleteDataset({ id, filters }) {
5526
5774
  try {
5527
5775
  const datasetsTable = getTableName2({ indexName: storage.TABLE_DATASETS, schemaName: getSchemaName2(this.#schema) });
5528
5776
  const itemsTable = getTableName2({ indexName: storage.TABLE_DATASET_ITEMS, schemaName: getSchemaName2(this.#schema) });
@@ -5535,24 +5783,31 @@ var DatasetsPG = class _DatasetsPG extends storage.DatasetsStorage {
5535
5783
  indexName: storage.TABLE_EXPERIMENT_RESULTS,
5536
5784
  schemaName: getSchemaName2(this.#schema)
5537
5785
  });
5538
- try {
5539
- await this.#db.client.none(
5540
- `DELETE FROM ${experimentResultsTable} WHERE "experimentId" IN (SELECT "id" FROM ${experimentsTable} WHERE "datasetId" = $1)`,
5541
- [id]
5542
- );
5543
- } catch {
5544
- }
5545
- try {
5546
- await this.#db.client.none(
5547
- `UPDATE ${experimentsTable} SET "datasetId" = NULL, "datasetVersion" = NULL WHERE "datasetId" = $1`,
5548
- [id]
5549
- );
5550
- } catch {
5551
- }
5786
+ const { conditions, params } = tenancyWhere(filters, 2);
5787
+ const scopedWhere = ['"id" = $1', ...conditions].join(" AND ");
5552
5788
  await this.#db.client.tx(async (t) => {
5789
+ const locked = await t.oneOrNone(`SELECT "id" FROM ${datasetsTable} WHERE ${scopedWhere} FOR UPDATE`, [
5790
+ id,
5791
+ ...params
5792
+ ]);
5793
+ if (!locked) return;
5794
+ const experimentTablesExist = await t.oneOrNone(
5795
+ `SELECT to_regclass($1) IS NOT NULL AND to_regclass($2) IS NOT NULL AS exists`,
5796
+ [experimentResultsTable, experimentsTable]
5797
+ );
5798
+ if (experimentTablesExist?.exists) {
5799
+ await t.none(
5800
+ `DELETE FROM ${experimentResultsTable} WHERE "experimentId" IN (SELECT "id" FROM ${experimentsTable} WHERE "datasetId" = $1)`,
5801
+ [id]
5802
+ );
5803
+ await t.none(
5804
+ `UPDATE ${experimentsTable} SET "datasetId" = NULL, "datasetVersion" = NULL WHERE "datasetId" = $1`,
5805
+ [id]
5806
+ );
5807
+ }
5553
5808
  await t.none(`DELETE FROM ${versionsTable} WHERE "datasetId" = $1`, [id]);
5554
5809
  await t.none(`DELETE FROM ${itemsTable} WHERE "datasetId" = $1`, [id]);
5555
- await t.none(`DELETE FROM ${datasetsTable} WHERE "id" = $1`, [id]);
5810
+ await t.none(`DELETE FROM ${datasetsTable} WHERE ${scopedWhere}`, [id, ...params]);
5556
5811
  });
5557
5812
  } catch (error$1) {
5558
5813
  if (error$1 instanceof error.MastraError) throw error$1;
@@ -5574,7 +5829,7 @@ var DatasetsPG = class _DatasetsPG extends storage.DatasetsStorage {
5574
5829
  const queryParams = [];
5575
5830
  let paramIndex = 1;
5576
5831
  if (args.filters) {
5577
- const { organizationId, projectId, candidateKey, candidateId } = args.filters;
5832
+ const { organizationId, projectId, candidateKey, candidateId, targetType, targetIds, name } = args.filters;
5578
5833
  if (organizationId !== void 0) {
5579
5834
  conditions.push(`"organizationId" = $${paramIndex++}`);
5580
5835
  queryParams.push(organizationId);
@@ -5591,6 +5846,18 @@ var DatasetsPG = class _DatasetsPG extends storage.DatasetsStorage {
5591
5846
  conditions.push(`"candidateId" = $${paramIndex++}`);
5592
5847
  queryParams.push(candidateId);
5593
5848
  }
5849
+ if (targetType !== void 0) {
5850
+ conditions.push(`"targetType" = $${paramIndex++}`);
5851
+ queryParams.push(targetType);
5852
+ }
5853
+ if (targetIds !== void 0 && targetIds.length > 0) {
5854
+ conditions.push(`"targetIds" ?| $${paramIndex++}::text[]`);
5855
+ queryParams.push(targetIds);
5856
+ }
5857
+ if (name !== void 0 && name.length > 0) {
5858
+ conditions.push(`"name" ILIKE $${paramIndex++}`);
5859
+ queryParams.push(`%${name}%`);
5860
+ }
5594
5861
  }
5595
5862
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
5596
5863
  const countResult = await this.#db.client.one(
@@ -6248,12 +6515,24 @@ var DatasetsPG = class _DatasetsPG extends storage.DatasetsStorage {
6248
6515
  await this.#db.clearTable({ tableName: storage.TABLE_DATASETS });
6249
6516
  }
6250
6517
  };
6518
+ var DEFAULT_PRUNE_BATCH_SIZE = 1e3;
6251
6519
  var ExperimentsPG = class _ExperimentsPG extends storage.ExperimentsStorage {
6252
6520
  #db;
6253
6521
  #schema;
6254
6522
  #skipDefaultIndexes;
6255
6523
  #indexes;
6256
6524
  static MANAGED_TABLES = [storage.TABLE_EXPERIMENTS, storage.TABLE_EXPERIMENT_RESULTS];
6525
+ /**
6526
+ * Experiments prune as whole units: an aged experiment and its result rows go
6527
+ * together, mirroring `deleteExperiment`. Anchored on `completedAt` (not the
6528
+ * `completedAtZ` mirror, which carries a `DEFAULT NOW()` this domain never
6529
+ * overrides — it holds insert time even for running rows). `completedAt` is
6530
+ * written as a UTC ISO string and stays NULL while running, so
6531
+ * `completedAt < cutoff` is false for in-flight experiments.
6532
+ */
6533
+ static retentionTables = {
6534
+ experiments: { table: storage.TABLE_EXPERIMENTS, column: "completedAt", indexed: true }
6535
+ };
6257
6536
  constructor(config) {
6258
6537
  super();
6259
6538
  const { client, schemaName, skipDefaultIndexes, indexes } = resolvePgConfig(config);
@@ -6292,6 +6571,75 @@ var ExperimentsPG = class _ExperimentsPG extends storage.ExperimentsStorage {
6292
6571
  await this.createDefaultIndexes();
6293
6572
  await this.createCustomIndexes();
6294
6573
  }
6574
+ /**
6575
+ * Lazily ensures a btree index exists on each configured policy's retention
6576
+ * anchor column so age-based `prune()` deletes stay fast on large tables.
6577
+ * Called from the prune path (not init) so only deployments that configure
6578
+ * retention pay the index's write/disk overhead. Best-effort: failures are
6579
+ * logged and pruning proceeds (correct, just slower).
6580
+ * Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
6581
+ * so its supporting index is not part of the default index set.
6582
+ */
6583
+ async ensureRetentionIndexes(policies) {
6584
+ const prefix = this.#schema !== "public" ? `${this.#schema}_` : "";
6585
+ for (const [key, entry] of Object.entries(_ExperimentsPG.retentionTables)) {
6586
+ if (!entry.indexed || !policies[key]) continue;
6587
+ try {
6588
+ await this.#db.ensureIndex({
6589
+ indexName: `${prefix}mastra_${key}_retention_idx`,
6590
+ tableName: entry.table,
6591
+ column: entry.column
6592
+ });
6593
+ } catch (error) {
6594
+ this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
6595
+ }
6596
+ }
6597
+ }
6598
+ /**
6599
+ * Delete experiments whose `completedAt` is older than the policy's `maxAge`.
6600
+ *
6601
+ * Each batch selects up to `batchSize` aged experiments and deletes their
6602
+ * `experiment_results` rows and the experiment rows in one transaction —
6603
+ * mirroring `deleteExperiment` — so hitting `maxBatches`/`maxRows` or the
6604
+ * abort signal between batches never leaves a run hollow (parent kept,
6605
+ * results gone). NULL `completedAt` (still running) is excluded by the
6606
+ * `< cutoff` predicate. Bounds count whole experiments, not rows.
6607
+ */
6608
+ async prune(policies, options) {
6609
+ const policy = policies["experiments"];
6610
+ if (!policy || options?.signal?.aborted) {
6611
+ return policy ? [
6612
+ { domain: "experiments", table: storage.TABLE_EXPERIMENT_RESULTS, deleted: 0, done: false },
6613
+ { domain: "experiments", table: storage.TABLE_EXPERIMENTS, deleted: 0, done: false }
6614
+ ] : [];
6615
+ }
6616
+ await this.ensureRetentionIndexes(policies);
6617
+ const rawCutoff = cutoffFor(policy, "timestamp");
6618
+ const cutoff = rawCutoff instanceof Date ? rawCutoff.toISOString() : rawCutoff;
6619
+ const batchSize = policy.batchSize ?? DEFAULT_PRUNE_BATCH_SIZE;
6620
+ let childDeleted = 0;
6621
+ const parent = await runBatchedDelete({
6622
+ deleteBatch: async (limit) => {
6623
+ const { parents, children } = await this.#db.pruneUnitsBatch({
6624
+ parentTable: storage.TABLE_EXPERIMENTS,
6625
+ parentKey: "id",
6626
+ parentColumn: "completedAt",
6627
+ childTable: storage.TABLE_EXPERIMENT_RESULTS,
6628
+ childForeignKey: "experimentId",
6629
+ cutoff,
6630
+ limit
6631
+ });
6632
+ childDeleted += children;
6633
+ return parents;
6634
+ },
6635
+ batchSize,
6636
+ options
6637
+ });
6638
+ return [
6639
+ { domain: "experiments", table: storage.TABLE_EXPERIMENT_RESULTS, deleted: childDeleted, done: parent.done },
6640
+ { domain: "experiments", table: storage.TABLE_EXPERIMENTS, deleted: parent.deleted, done: parent.done }
6641
+ ];
6642
+ }
6295
6643
  getDefaultIndexDefinitions() {
6296
6644
  return [
6297
6645
  { name: "idx_experiments_datasetid", table: storage.TABLE_EXPERIMENTS, columns: ["datasetId"] },
@@ -6521,10 +6869,15 @@ var ExperimentsPG = class _ExperimentsPG extends storage.ExperimentsStorage {
6521
6869
  );
6522
6870
  }
6523
6871
  }
6524
- async getExperimentById({ id }) {
6872
+ async getExperimentById({
6873
+ id,
6874
+ filters
6875
+ }) {
6525
6876
  try {
6526
6877
  const tableName = getTableName2({ indexName: storage.TABLE_EXPERIMENTS, schemaName: getSchemaName2(this.#schema) });
6527
- const result = await this.#db.client.oneOrNone(`SELECT * FROM ${tableName} WHERE "id" = $1`, [id]);
6878
+ const { conditions, params } = tenancyWhere(filters, 2);
6879
+ const whereSql = ['"id" = $1', ...conditions].join(" AND ");
6880
+ const result = await this.#db.client.oneOrNone(`SELECT * FROM ${tableName} WHERE ${whereSql}`, [id, ...params]);
6528
6881
  return result ? this.transformExperimentRow(result) : null;
6529
6882
  } catch (error$1) {
6530
6883
  throw new error.MastraError(
@@ -6611,15 +6964,21 @@ var ExperimentsPG = class _ExperimentsPG extends storage.ExperimentsStorage {
6611
6964
  );
6612
6965
  }
6613
6966
  }
6614
- async deleteExperiment({ id }) {
6967
+ async deleteExperiment({ id, filters }) {
6615
6968
  try {
6616
6969
  const resultsTable = getTableName2({
6617
6970
  indexName: storage.TABLE_EXPERIMENT_RESULTS,
6618
6971
  schemaName: getSchemaName2(this.#schema)
6619
6972
  });
6620
6973
  const experimentsTable = getTableName2({ indexName: storage.TABLE_EXPERIMENTS, schemaName: getSchemaName2(this.#schema) });
6621
- await this.#db.client.none(`DELETE FROM ${resultsTable} WHERE "experimentId" = $1`, [id]);
6622
- await this.#db.client.none(`DELETE FROM ${experimentsTable} WHERE "id" = $1`, [id]);
6974
+ const { conditions, params } = tenancyWhere(filters, 2);
6975
+ const gateSql = `SELECT "id" FROM ${experimentsTable} WHERE ${['"id" = $1', ...conditions].join(" AND ")} FOR UPDATE`;
6976
+ await this.#db.client.tx(async (t) => {
6977
+ const parent = await t.oneOrNone(gateSql, [id, ...params]);
6978
+ if (!parent) return;
6979
+ await t.none(`DELETE FROM ${resultsTable} WHERE "experimentId" = $1`, [id]);
6980
+ await t.none(`DELETE FROM ${experimentsTable} WHERE "id" = $1`, [id]);
6981
+ });
6623
6982
  } catch (error$1) {
6624
6983
  throw new error.MastraError(
6625
6984
  {
@@ -6749,10 +7108,15 @@ var ExperimentsPG = class _ExperimentsPG extends storage.ExperimentsStorage {
6749
7108
  );
6750
7109
  }
6751
7110
  }
6752
- async getExperimentResultById({ id }) {
7111
+ async getExperimentResultById({
7112
+ id,
7113
+ filters
7114
+ }) {
6753
7115
  try {
6754
7116
  const tableName = getTableName2({ indexName: storage.TABLE_EXPERIMENT_RESULTS, schemaName: getSchemaName2(this.#schema) });
6755
- const result = await this.#db.client.oneOrNone(`SELECT * FROM ${tableName} WHERE "id" = $1`, [id]);
7117
+ const { conditions, params } = tenancyWhere(filters, 2);
7118
+ const whereSql = ['"id" = $1', ...conditions].join(" AND ");
7119
+ const result = await this.#db.client.oneOrNone(`SELECT * FROM ${tableName} WHERE ${whereSql}`, [id, ...params]);
6756
7120
  return result ? this.transformExperimentResultRow(result) : null;
6757
7121
  } catch (error$1) {
6758
7122
  throw new error.MastraError(
@@ -6827,9 +7191,23 @@ var ExperimentsPG = class _ExperimentsPG extends storage.ExperimentsStorage {
6827
7191
  );
6828
7192
  }
6829
7193
  }
6830
- async deleteExperimentResults({ experimentId }) {
7194
+ async deleteExperimentResults({
7195
+ experimentId,
7196
+ filters
7197
+ }) {
6831
7198
  try {
6832
7199
  const tableName = getTableName2({ indexName: storage.TABLE_EXPERIMENT_RESULTS, schemaName: getSchemaName2(this.#schema) });
7200
+ const experimentsTable = getTableName2({ indexName: storage.TABLE_EXPERIMENTS, schemaName: getSchemaName2(this.#schema) });
7201
+ if (filters?.organizationId !== void 0 || filters?.projectId !== void 0) {
7202
+ const { conditions, params } = tenancyWhere(filters, 2);
7203
+ const gateSql = `SELECT "id" FROM ${experimentsTable} WHERE ${['"id" = $1', ...conditions].join(" AND ")} FOR UPDATE`;
7204
+ await this.#db.client.tx(async (t) => {
7205
+ const parent = await t.oneOrNone(gateSql, [experimentId, ...params]);
7206
+ if (!parent) return;
7207
+ await t.none(`DELETE FROM ${tableName} WHERE "experimentId" = $1`, [experimentId]);
7208
+ });
7209
+ return;
7210
+ }
6833
7211
  await this.#db.client.none(`DELETE FROM ${tableName} WHERE "experimentId" = $1`, [experimentId]);
6834
7212
  } catch (error$1) {
6835
7213
  throw new error.MastraError(
@@ -8460,6 +8838,18 @@ function dedupeMessagesForSave(messages) {
8460
8838
  }
8461
8839
  var MemoryPG = class _MemoryPG extends storage.MemoryStorage {
8462
8840
  supportsObservationalMemory = true;
8841
+ /**
8842
+ * Retention-eligible tables. `threads`, `messages`, and `resources` all anchor
8843
+ * on the timezone-aware `createdAtZ` mirror column (kept in sync by triggers),
8844
+ * and are indexed for fast batched deletes. Cascade order is enforced in
8845
+ * `prune()` (children before threads), not here. Observational memory has no
8846
+ * timestamp anchor and is deliberately excluded.
8847
+ */
8848
+ static retentionTables = {
8849
+ messages: { table: storage.TABLE_MESSAGES, column: "createdAtZ", indexed: true },
8850
+ resources: { table: storage.TABLE_RESOURCES, column: "createdAtZ", indexed: true },
8851
+ threads: { table: storage.TABLE_THREADS, column: "createdAtZ", indexed: true }
8852
+ };
8463
8853
  #db;
8464
8854
  #schema;
8465
8855
  #skipDefaultIndexes;
@@ -8505,6 +8895,30 @@ var MemoryPG = class _MemoryPG extends storage.MemoryStorage {
8505
8895
  await this.createDefaultIndexes();
8506
8896
  await this.createCustomIndexes();
8507
8897
  }
8898
+ /**
8899
+ * Lazily ensures a btree index exists on each configured policy's retention
8900
+ * anchor column so age-based `prune()` deletes stay fast on large tables.
8901
+ * Called from the prune path (not init) so only deployments that configure
8902
+ * retention pay the index's write/disk overhead. Best-effort: failures are
8903
+ * logged and pruning proceeds (correct, just slower).
8904
+ * Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
8905
+ * so its supporting index is not part of the default index set.
8906
+ */
8907
+ async ensureRetentionIndexes(policies) {
8908
+ const prefix = this.#schema && this.#schema !== "public" ? `${this.#schema}_` : "";
8909
+ for (const [key, entry] of Object.entries(_MemoryPG.retentionTables)) {
8910
+ if (!entry.indexed || !policies[key]) continue;
8911
+ try {
8912
+ await this.#db.ensureIndex({
8913
+ indexName: `${prefix}mastra_${key}_retention_idx`,
8914
+ tableName: entry.table,
8915
+ column: entry.column
8916
+ });
8917
+ } catch (error) {
8918
+ this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
8919
+ }
8920
+ }
8921
+ }
8508
8922
  /**
8509
8923
  * Returns default index definitions for the memory domain tables.
8510
8924
  * @param schemaPrefix - Prefix for index names (e.g. "my_schema_" or "")
@@ -8605,6 +9019,76 @@ var MemoryPG = class _MemoryPG extends storage.MemoryStorage {
8605
9019
  await this.#db.clearTable({ tableName: storage.TABLE_THREADS });
8606
9020
  await this.#db.clearTable({ tableName: storage.TABLE_RESOURCES });
8607
9021
  }
9022
+ /**
9023
+ * Deletes rows older than the configured `maxAge` per table, in bounded,
9024
+ * batched, cancellable chunks. Tables are pruned children-first (messages and
9025
+ * resources before threads) since PostgreSQL has no FK cascade in this schema.
9026
+ * Unset tables are kept forever.
9027
+ *
9028
+ * When a `messages` policy is set, semantic-recall embeddings for pruned
9029
+ * messages are also swept from same-schema `memory_messages*` vector tables
9030
+ * (best-effort, mirroring `deleteThread`). Embeddings held in an external
9031
+ * vector store are out of reach and must be pruned by the operator.
9032
+ */
9033
+ async prune(policies, options) {
9034
+ await this.ensureRetentionIndexes(policies);
9035
+ const targets = resolveTargets({
9036
+ policies,
9037
+ descriptor: _MemoryPG.retentionTables,
9038
+ order: ["messages", "resources", "threads"]
9039
+ });
9040
+ const results = await runPrune({ db: this.#db, domain: "memory", targets, options });
9041
+ if (policies["messages"]) {
9042
+ await this.pruneOrphanedVectorRows(policies["messages"], options);
9043
+ }
9044
+ return results;
9045
+ }
9046
+ /**
9047
+ * Best-effort sweep of semantic-recall vector rows whose source message no
9048
+ * longer exists (e.g. it was just pruned), so recall doesn't keep returning
9049
+ * embeddings that resolve to nothing. Only same-schema default vector tables
9050
+ * (`memory_messages*`) are covered — the same set `deleteThread` cleans up.
9051
+ * Failures are logged, never thrown: vector cleanup must not fail the prune.
9052
+ */
9053
+ async pruneOrphanedVectorRows(policy, options) {
9054
+ try {
9055
+ const schemaName = this.#schema || "public";
9056
+ const vectorTables = await this.#db.client.manyOrNone(
9057
+ `
9058
+ SELECT tablename
9059
+ FROM pg_tables
9060
+ WHERE schemaname = $1
9061
+ AND (tablename = 'memory_messages' OR tablename LIKE 'memory_messages_%')
9062
+ `,
9063
+ [schemaName]
9064
+ );
9065
+ const messagesTable = getTableName4({ indexName: storage.TABLE_MESSAGES, schemaName: getSchemaName4(this.#schema) });
9066
+ for (const { tablename } of vectorTables) {
9067
+ const vectorTableName = getTableName4({ indexName: tablename, schemaName: getSchemaName4(this.#schema) });
9068
+ await runBatchedDelete({
9069
+ deleteBatch: async (limit) => {
9070
+ const result = await this.#db.client.query(
9071
+ `
9072
+ DELETE FROM ${vectorTableName}
9073
+ WHERE ctid IN (
9074
+ SELECT v.ctid FROM ${vectorTableName} v
9075
+ WHERE v.metadata->>'message_id' IS NOT NULL
9076
+ AND NOT EXISTS (SELECT 1 FROM ${messagesTable} m WHERE m.id = v.metadata->>'message_id')
9077
+ LIMIT $1
9078
+ )
9079
+ `,
9080
+ [limit]
9081
+ );
9082
+ return result.rowCount ?? 0;
9083
+ },
9084
+ batchSize: policy.batchSize ?? 1e3,
9085
+ options
9086
+ });
9087
+ }
9088
+ } catch (error) {
9089
+ this.logger?.warn?.("Failed to sweep orphaned semantic-recall vector rows after prune:", error);
9090
+ }
9091
+ }
8608
9092
  /**
8609
9093
  * Normalizes message row from database by applying createdAtZ fallback
8610
9094
  */
@@ -10916,6 +11400,13 @@ var NotificationsPG = class _NotificationsPG extends storage.NotificationsStorag
10916
11400
  #skipDefaultIndexes;
10917
11401
  #indexes;
10918
11402
  static MANAGED_TABLES = [storage.TABLE_NOTIFICATIONS];
11403
+ /**
11404
+ * Notifications are an append-only event feed that grows unbounded. Single
11405
+ * table, anchored on the timezone-aware `createdAtZ` mirror column.
11406
+ */
11407
+ static retentionTables = {
11408
+ notifications: { table: storage.TABLE_NOTIFICATIONS, column: "createdAtZ", indexed: true }
11409
+ };
10919
11410
  constructor(config) {
10920
11411
  super();
10921
11412
  const { client, schemaName, skipDefaultIndexes, indexes } = resolvePgConfig(config);
@@ -10932,6 +11423,40 @@ var NotificationsPG = class _NotificationsPG extends storage.NotificationsStorag
10932
11423
  await this.createDefaultIndexes();
10933
11424
  await this.createCustomIndexes();
10934
11425
  }
11426
+ /**
11427
+ * Lazily ensures a btree index exists on each configured policy's retention
11428
+ * anchor column so age-based `prune()` deletes stay fast on large tables.
11429
+ * Called from the prune path (not init) so only deployments that configure
11430
+ * retention pay the index's write/disk overhead. Best-effort: failures are
11431
+ * logged and pruning proceeds (correct, just slower).
11432
+ * Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
11433
+ * so its supporting index is not part of the default index set.
11434
+ */
11435
+ async ensureRetentionIndexes(policies) {
11436
+ const prefix = this.#schema && this.#schema !== "public" ? `${this.#schema}_` : "";
11437
+ for (const [key, entry] of Object.entries(_NotificationsPG.retentionTables)) {
11438
+ if (!entry.indexed || !policies[key]) continue;
11439
+ try {
11440
+ await this.#db.ensureIndex({
11441
+ indexName: `${prefix}mastra_${key}_retention_idx`,
11442
+ tableName: entry.table,
11443
+ column: entry.column
11444
+ });
11445
+ } catch (error) {
11446
+ this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
11447
+ }
11448
+ }
11449
+ }
11450
+ /** Delete notifications older than the `notifications` policy's `maxAge`, batched. */
11451
+ async prune(policies, options) {
11452
+ await this.ensureRetentionIndexes(policies);
11453
+ const targets = resolveTargets({
11454
+ policies,
11455
+ descriptor: _NotificationsPG.retentionTables,
11456
+ order: ["notifications"]
11457
+ });
11458
+ return runPrune({ db: this.#db, domain: "notifications", targets, options });
11459
+ }
10935
11460
  static getDefaultIndexDefs(schemaPrefix) {
10936
11461
  return [
10937
11462
  {
@@ -11173,6 +11698,14 @@ var NotificationsPG = class _NotificationsPG extends storage.NotificationsStorag
11173
11698
  }
11174
11699
  };
11175
11700
  var ObservabilityPG = class _ObservabilityPG extends storage.ObservabilityStorage {
11701
+ /**
11702
+ * Retention-eligible tables. The observability domain has a single physical
11703
+ * table (`mastra_ai_spans`); spans anchor on the timezone-aware `startedAtZ`
11704
+ * mirror column and are indexed for fast batched deletes.
11705
+ */
11706
+ static retentionTables = {
11707
+ spans: { table: storage.TABLE_SPANS, column: "startedAtZ", indexed: true }
11708
+ };
11176
11709
  #db;
11177
11710
  #schema;
11178
11711
  #skipDefaultIndexes;
@@ -11197,6 +11730,30 @@ var ObservabilityPG = class _ObservabilityPG extends storage.ObservabilityStorag
11197
11730
  await this.createDefaultIndexes();
11198
11731
  await this.createCustomIndexes();
11199
11732
  }
11733
+ /**
11734
+ * Lazily ensures a btree index exists on each configured policy's retention
11735
+ * anchor column so age-based `prune()` deletes stay fast on large span tables.
11736
+ * Called from the prune path (not init) so only deployments that configure
11737
+ * retention pay the index's write/disk overhead. Best-effort: failures are
11738
+ * logged and pruning proceeds (correct, just slower).
11739
+ * Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
11740
+ * so its supporting index is not part of the default index set.
11741
+ */
11742
+ async ensureRetentionIndexes(policies) {
11743
+ const prefix = this.#schema !== "public" ? `${this.#schema}_` : "";
11744
+ for (const [key, entry] of Object.entries(_ObservabilityPG.retentionTables)) {
11745
+ if (!entry.indexed || !policies[key]) continue;
11746
+ try {
11747
+ await this.#db.ensureIndex({
11748
+ indexName: `${prefix}mastra_${key}_retention_idx`,
11749
+ tableName: entry.table,
11750
+ column: entry.column
11751
+ });
11752
+ } catch (error) {
11753
+ this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
11754
+ }
11755
+ }
11756
+ }
11200
11757
  /**
11201
11758
  * Returns default index definitions for the observability domain tables.
11202
11759
  * @param schemaPrefix - Prefix for index names (e.g. "my_schema_" or "")
@@ -11341,6 +11898,19 @@ var ObservabilityPG = class _ObservabilityPG extends storage.ObservabilityStorag
11341
11898
  async dangerouslyClearAll() {
11342
11899
  await this.#db.clearTable({ tableName: storage.TABLE_SPANS });
11343
11900
  }
11901
+ /**
11902
+ * Deletes spans older than the configured `maxAge`, in bounded, batched,
11903
+ * cancellable chunks. Unset = keep forever.
11904
+ */
11905
+ async prune(policies, options) {
11906
+ await this.ensureRetentionIndexes(policies);
11907
+ const targets = resolveTargets({
11908
+ policies,
11909
+ descriptor: _ObservabilityPG.retentionTables,
11910
+ order: ["spans"]
11911
+ });
11912
+ return runPrune({ db: this.#db, domain: "observability", targets, options });
11913
+ }
11344
11914
  get tracingStrategy() {
11345
11915
  return {
11346
11916
  preferred: "batch-with-updates",
@@ -14037,6 +14607,78 @@ async function setupPartitioning(client, schema, options = {}) {
14037
14607
  return "native";
14038
14608
  }
14039
14609
  }
14610
+ function retentionCutoff(policy, now = Date.now()) {
14611
+ return new Date(now - storage.parseDuration(policy.maxAge));
14612
+ }
14613
+ async function listChildPartitions(client, schema, table) {
14614
+ const rows = await client.manyOrNone(
14615
+ `SELECT c.relname AS "name", pg_get_expr(c.relpartbound, c.oid) AS "bound"
14616
+ FROM pg_inherits i
14617
+ JOIN pg_class c ON c.oid = i.inhrelid
14618
+ JOIN pg_class p ON p.oid = i.inhparent
14619
+ JOIN pg_namespace n ON n.oid = p.relnamespace
14620
+ WHERE n.nspname = $1 AND p.relname = $2`,
14621
+ [schema, table]
14622
+ );
14623
+ const partitions = [];
14624
+ for (const row of rows ?? []) {
14625
+ const match = /TO \('([^']+)'\)/.exec(row.bound ?? "");
14626
+ if (!match?.[1]) continue;
14627
+ const upperBound = new Date(match[1]);
14628
+ if (Number.isNaN(upperBound.getTime())) continue;
14629
+ partitions.push({ name: row.name, upperBound });
14630
+ }
14631
+ return partitions.sort((a, b) => a.upperBound.getTime() - b.upperBound.getTime());
14632
+ }
14633
+ async function prunePartitionedTable({
14634
+ client,
14635
+ schema,
14636
+ table,
14637
+ cutoff,
14638
+ options
14639
+ }) {
14640
+ const partitions = await listChildPartitions(client, schema, table);
14641
+ const droppable = partitions.filter((p) => p.upperBound.getTime() <= cutoff.getTime());
14642
+ let deleted = 0;
14643
+ let batches = 0;
14644
+ for (const partition of droppable) {
14645
+ if (options?.signal?.aborted) return { deleted, done: false };
14646
+ if (options?.maxBatches !== void 0 && batches >= options.maxBatches) return { deleted, done: false };
14647
+ if (options?.maxRows !== void 0 && deleted >= options.maxRows) return { deleted, done: false };
14648
+ const child = qualifiedName(schema, partition.name);
14649
+ const parent = qualifiedTable(schema, table);
14650
+ const row = await client.one(`SELECT count(*)::int AS "n" FROM ${child}`);
14651
+ await client.none(`ALTER TABLE ${parent} DETACH PARTITION ${child}`);
14652
+ await client.none(`DROP TABLE IF EXISTS ${child}`);
14653
+ deleted += row.n;
14654
+ batches += 1;
14655
+ }
14656
+ return { deleted, done: true };
14657
+ }
14658
+ async function pruneTimescaleTable({
14659
+ client,
14660
+ schema,
14661
+ table,
14662
+ cutoff,
14663
+ options
14664
+ }) {
14665
+ if (options?.signal?.aborted || options?.maxBatches === 0) return { deleted: 0, done: false };
14666
+ const tableExpr = qualifiedTable(schema, table);
14667
+ const chunks = await client.manyOrNone(
14668
+ `SELECT show_chunks($1::regclass, older_than => $2::timestamptz)::text AS "chunk"`,
14669
+ [tableExpr, cutoff.toISOString()]
14670
+ );
14671
+ let deleted = 0;
14672
+ for (const { chunk } of chunks ?? []) {
14673
+ const row = await client.one(`SELECT count(*)::int AS "n" FROM ${chunk}`);
14674
+ deleted += row.n;
14675
+ }
14676
+ await client.none(`SELECT drop_chunks($1::regclass, older_than => $2::timestamptz)`, [
14677
+ tableExpr,
14678
+ cutoff.toISOString()
14679
+ ]);
14680
+ return { deleted, done: true };
14681
+ }
14040
14682
  function applyScoreFilters(acc, filters) {
14041
14683
  applyCommonFilters(acc, filters);
14042
14684
  applySingleOrArrayFilter(acc, "scorerId", filters?.scorerId);
@@ -14783,7 +15425,7 @@ function wrapError(op, error$1, details) {
14783
15425
  error$1
14784
15426
  );
14785
15427
  }
14786
- var ObservabilityStoragePostgresVNext = class extends storage.ObservabilityStorage {
15428
+ var ObservabilityStoragePostgresVNext = class _ObservabilityStoragePostgresVNext extends storage.ObservabilityStorage {
14787
15429
  #client;
14788
15430
  #schema;
14789
15431
  #partitioning;
@@ -14867,6 +15509,50 @@ var ObservabilityStoragePostgresVNext = class extends storage.ObservabilityStora
14867
15509
  get partitionMode() {
14868
15510
  return this.#partitionMode;
14869
15511
  }
15512
+ // -------------------------------------------------------------------------
15513
+ // Retention
15514
+ // -------------------------------------------------------------------------
15515
+ /**
15516
+ * All five signal tables are insert-only growth tables. The anchor column is
15517
+ * each table's partition / chunk key, so age-based expiry drops whole day
15518
+ * partitions instead of deleting rows (see `./retention.ts`). `indexed: true`
15519
+ * reflects that expiry never scans — it acts on partition bounds.
15520
+ */
15521
+ static retentionTables = {
15522
+ spans: { table: TABLE_SPAN_EVENTS, column: "endedAt", indexed: true },
15523
+ metrics: { table: TABLE_METRIC_EVENTS, column: "timestamp", indexed: true },
15524
+ logs: { table: TABLE_LOG_EVENTS, column: "timestamp", indexed: true },
15525
+ scores: { table: TABLE_SCORE_EVENTS, column: "timestamp", indexed: true },
15526
+ feedback: { table: TABLE_FEEDBACK_EVENTS, column: "timestamp", indexed: true }
15527
+ };
15528
+ /**
15529
+ * Expire signal events older than each table's `maxAge` by dropping whole
15530
+ * day partitions (native / pg_partman) or chunks (Timescale). Only
15531
+ * partitions wholly older than the cutoff are dropped, so the effective
15532
+ * granularity is one day. Each partition drop counts as one batch for
15533
+ * `maxBatches` / `maxRows` / abort-signal purposes; `deleted` reports the
15534
+ * row count of the dropped partitions.
15535
+ */
15536
+ async prune(policies, options) {
15537
+ return this.#run("VNEXT_PRUNE", async () => {
15538
+ const mode = this.#partitionMode ??= await resolveMode(this.#client, this.#partitioning);
15539
+ const results = [];
15540
+ const now = Date.now();
15541
+ for (const [key, entry] of Object.entries(_ObservabilityStoragePostgresVNext.retentionTables)) {
15542
+ const policy = policies[key];
15543
+ if (!policy) continue;
15544
+ if (options?.signal?.aborted) {
15545
+ results.push({ domain: "observability", table: entry.table, deleted: 0, done: false });
15546
+ continue;
15547
+ }
15548
+ const cutoff = retentionCutoff(policy, now);
15549
+ const args = { client: this.#client, schema: this.#schema, table: entry.table, cutoff, options };
15550
+ const outcome = mode === "timescale" ? await pruneTimescaleTable(args) : await prunePartitionedTable(args);
15551
+ results.push({ domain: "observability", table: entry.table, deleted: outcome.deleted, done: outcome.done });
15552
+ }
15553
+ return results;
15554
+ });
15555
+ }
14870
15556
  get observabilityStrategy() {
14871
15557
  return { preferred: "insert-only", supported: ["insert-only"] };
14872
15558
  }
@@ -15811,6 +16497,15 @@ var SchedulesPG = class _SchedulesPG extends storage.SchedulesStorage {
15811
16497
  #indexes;
15812
16498
  /** Tables managed by this domain */
15813
16499
  static MANAGED_TABLES = [storage.TABLE_SCHEDULES, storage.TABLE_SCHEDULE_TRIGGERS];
16500
+ /**
16501
+ * The fire/run history (`schedule_triggers`, one row per fire) is the growth
16502
+ * table; schedule definitions are config and excluded. Anchored on
16503
+ * `actual_fire_at`, a bigint epoch-ms column (numeric comparison, not
16504
+ * timestamptz).
16505
+ */
16506
+ static retentionTables = {
16507
+ triggers: { table: storage.TABLE_SCHEDULE_TRIGGERS, column: "actual_fire_at", indexed: true, anchorType: "epoch-ms" }
16508
+ };
15814
16509
  constructor(config) {
15815
16510
  super();
15816
16511
  const { client, schemaName, skipDefaultIndexes, indexes } = resolvePgConfig(config);
@@ -15832,6 +16527,43 @@ var SchedulesPG = class _SchedulesPG extends storage.SchedulesStorage {
15832
16527
  await this.createDefaultIndexes();
15833
16528
  await this.createCustomIndexes();
15834
16529
  }
16530
+ /**
16531
+ * Lazily ensures a btree index exists on each configured policy's retention
16532
+ * anchor column so age-based `prune()` deletes stay fast. The default
16533
+ * composite index leads with `schedule_id`, so a bare `actual_fire_at` range
16534
+ * scan can't use it. Called from the prune path (not init) so only
16535
+ * deployments that configure retention pay the index's write/disk overhead.
16536
+ * Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
16537
+ * so its supporting index is not part of the default index set.
16538
+ */
16539
+ async ensureRetentionIndexes(policies) {
16540
+ const prefix = this.#schema !== "public" ? `${this.#schema}_` : "";
16541
+ for (const [key, entry] of Object.entries(_SchedulesPG.retentionTables)) {
16542
+ if (!entry.indexed || !policies[key]) continue;
16543
+ try {
16544
+ await this.#db.ensureIndex({
16545
+ indexName: `${prefix}mastra_${key}_retention_idx`,
16546
+ tableName: entry.table,
16547
+ column: entry.column
16548
+ });
16549
+ } catch (error) {
16550
+ this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
16551
+ }
16552
+ }
16553
+ }
16554
+ /**
16555
+ * Delete trigger (fire history) rows whose `actual_fire_at` is older than the
16556
+ * `triggers` policy's `maxAge`, batched. Schedule definitions are never pruned.
16557
+ */
16558
+ async prune(policies, options) {
16559
+ await this.ensureRetentionIndexes(policies);
16560
+ const targets = resolveTargets({
16561
+ policies,
16562
+ descriptor: _SchedulesPG.retentionTables,
16563
+ order: ["triggers"]
16564
+ });
16565
+ return runPrune({ db: this.#db, domain: "schedules", targets, options });
16566
+ }
15835
16567
  /**
15836
16568
  * Returns default index definitions for the schedules domain.
15837
16569
  * @param schemaPrefix - Prefix for index names (e.g. "my_schema_" or "")
@@ -16164,6 +16896,11 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends storage.ScorerDefin
16164
16896
  tableName: storage.TABLE_SCORER_DEFINITIONS,
16165
16897
  schema: storage.TABLE_SCHEMAS[storage.TABLE_SCORER_DEFINITIONS]
16166
16898
  });
16899
+ await this.#db.alterTable({
16900
+ tableName: storage.TABLE_SCORER_DEFINITIONS,
16901
+ schema: storage.TABLE_SCHEMAS[storage.TABLE_SCORER_DEFINITIONS],
16902
+ ifNotExists: ["organizationId", "projectId"]
16903
+ });
16167
16904
  await this.#db.createTable({
16168
16905
  tableName: storage.TABLE_SCORER_DEFINITION_VERSIONS,
16169
16906
  schema: storage.TABLE_SCHEMAS[storage.TABLE_SCORER_DEFINITION_VERSIONS]
@@ -16219,14 +16956,16 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends storage.ScorerDefin
16219
16956
  const nowIso = now.toISOString();
16220
16957
  await this.#db.client.none(
16221
16958
  `INSERT INTO ${tableName} (
16222
- id, status, "activeVersionId", "authorId", metadata,
16959
+ id, status, "activeVersionId", "authorId", "organizationId", "projectId", metadata,
16223
16960
  "createdAt", "createdAtZ", "updatedAt", "updatedAtZ"
16224
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
16961
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
16225
16962
  [
16226
16963
  scorerDefinition.id,
16227
16964
  "draft",
16228
16965
  null,
16229
16966
  scorerDefinition.authorId ?? null,
16967
+ scorerDefinition.organizationId ?? null,
16968
+ scorerDefinition.projectId ?? null,
16230
16969
  scorerDefinition.metadata ? JSON.stringify(scorerDefinition.metadata) : null,
16231
16970
  nowIso,
16232
16971
  nowIso,
@@ -16234,7 +16973,14 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends storage.ScorerDefin
16234
16973
  nowIso
16235
16974
  ]
16236
16975
  );
16237
- const { id: _id, authorId: _authorId, metadata: _metadata, ...snapshotConfig } = scorerDefinition;
16976
+ const {
16977
+ id: _id,
16978
+ authorId: _authorId,
16979
+ organizationId: _organizationId,
16980
+ projectId: _projectId,
16981
+ metadata: _metadata,
16982
+ ...snapshotConfig
16983
+ } = scorerDefinition;
16238
16984
  const versionId = crypto.randomUUID();
16239
16985
  await this.createVersion({
16240
16986
  id: versionId,
@@ -16249,6 +16995,8 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends storage.ScorerDefin
16249
16995
  status: "draft",
16250
16996
  activeVersionId: void 0,
16251
16997
  authorId: scorerDefinition.authorId,
16998
+ organizationId: scorerDefinition.organizationId,
16999
+ projectId: scorerDefinition.projectId,
16252
17000
  metadata: scorerDefinition.metadata,
16253
17001
  createdAt: now,
16254
17002
  updatedAt: now
@@ -16362,7 +17110,16 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends storage.ScorerDefin
16362
17110
  }
16363
17111
  }
16364
17112
  async list(args) {
16365
- const { page = 0, perPage: perPageInput, orderBy, authorId, metadata, status } = args || {};
17113
+ const {
17114
+ page = 0,
17115
+ perPage: perPageInput,
17116
+ orderBy,
17117
+ authorId,
17118
+ organizationId,
17119
+ projectId,
17120
+ metadata,
17121
+ status
17122
+ } = args || {};
16366
17123
  const { field, direction } = this.parseOrderBy(orderBy);
16367
17124
  if (page < 0) {
16368
17125
  throw new error.MastraError(
@@ -16390,6 +17147,14 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends storage.ScorerDefin
16390
17147
  conditions.push(`"authorId" = $${paramIdx++}`);
16391
17148
  queryParams.push(authorId);
16392
17149
  }
17150
+ if (organizationId !== void 0) {
17151
+ conditions.push(`"organizationId" = $${paramIdx++}`);
17152
+ queryParams.push(organizationId);
17153
+ }
17154
+ if (projectId !== void 0) {
17155
+ conditions.push(`"projectId" = $${paramIdx++}`);
17156
+ queryParams.push(projectId);
17157
+ }
16393
17158
  if (metadata && Object.keys(metadata).length > 0) {
16394
17159
  conditions.push(`metadata @> $${paramIdx++}::jsonb`);
16395
17160
  queryParams.push(JSON.stringify(metadata));
@@ -16716,6 +17481,8 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends storage.ScorerDefin
16716
17481
  status: row.status,
16717
17482
  activeVersionId: row.activeVersionId,
16718
17483
  authorId: row.authorId,
17484
+ organizationId: row.organizationId,
17485
+ projectId: row.projectId,
16719
17486
  metadata: parseJsonResilient(row.metadata),
16720
17487
  createdAt: new Date(row.createdAtZ || row.createdAt),
16721
17488
  updatedAt: new Date(row.updatedAtZ || row.updatedAt)
@@ -16740,6 +17507,18 @@ var ScorerDefinitionsPG = class _ScorerDefinitionsPG extends storage.ScorerDefin
16740
17507
  };
16741
17508
  }
16742
17509
  };
17510
+ function applyTenancyFilters(conditions, queryParams, startIndex, filters) {
17511
+ let paramIndex = startIndex;
17512
+ if (filters?.organizationId !== void 0) {
17513
+ conditions.push(`"organizationId" = $${paramIndex++}`);
17514
+ queryParams.push(filters.organizationId);
17515
+ }
17516
+ if (filters?.projectId !== void 0) {
17517
+ conditions.push(`"projectId" = $${paramIndex++}`);
17518
+ queryParams.push(filters.projectId);
17519
+ }
17520
+ return paramIndex;
17521
+ }
16743
17522
  function getSchemaName6(schema) {
16744
17523
  return schema ? `"${schema}"` : '"public"';
16745
17524
  }
@@ -16762,6 +17541,13 @@ var ScoresPG = class _ScoresPG extends storage.ScoresStorage {
16762
17541
  #indexes;
16763
17542
  /** Tables managed by this domain */
16764
17543
  static MANAGED_TABLES = [storage.TABLE_SCORERS];
17544
+ /**
17545
+ * Scorer results accumulate as evals run. Single table, anchored on the
17546
+ * timezone-aware `createdAtZ` mirror column (kept in sync by triggers).
17547
+ */
17548
+ static retentionTables = {
17549
+ scorers: { table: storage.TABLE_SCORERS, column: "createdAtZ", indexed: true }
17550
+ };
16765
17551
  constructor(config) {
16766
17552
  super();
16767
17553
  const { client, schemaName, skipDefaultIndexes, indexes } = resolvePgConfig(config);
@@ -16775,11 +17561,35 @@ var ScoresPG = class _ScoresPG extends storage.ScoresStorage {
16775
17561
  await this.#db.alterTable({
16776
17562
  tableName: storage.TABLE_SCORERS,
16777
17563
  schema: storage.TABLE_SCHEMAS[storage.TABLE_SCORERS],
16778
- ifNotExists: ["spanId", "requestContext"]
17564
+ ifNotExists: ["spanId", "requestContext", "organizationId", "projectId", "batchId", "datasetId", "datasetItemId"]
16779
17565
  });
16780
17566
  await this.createDefaultIndexes();
16781
17567
  await this.createCustomIndexes();
16782
17568
  }
17569
+ /**
17570
+ * Lazily ensures a btree index exists on each configured policy's retention
17571
+ * anchor column so age-based `prune()` deletes stay fast on large tables.
17572
+ * Called from the prune path (not init) so only deployments that configure
17573
+ * retention pay the index's write/disk overhead. Best-effort: failures are
17574
+ * logged and pruning proceeds (correct, just slower).
17575
+ * Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
17576
+ * so its supporting index is not part of the default index set.
17577
+ */
17578
+ async ensureRetentionIndexes(policies) {
17579
+ const prefix = this.#schema && this.#schema !== "public" ? `${this.#schema}_` : "";
17580
+ for (const [key, entry] of Object.entries(_ScoresPG.retentionTables)) {
17581
+ if (!entry.indexed || !policies[key]) continue;
17582
+ try {
17583
+ await this.#db.ensureIndex({
17584
+ indexName: `${prefix}mastra_${key}_retention_idx`,
17585
+ tableName: entry.table,
17586
+ column: entry.column
17587
+ });
17588
+ } catch (error) {
17589
+ this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
17590
+ }
17591
+ }
17592
+ }
16783
17593
  /**
16784
17594
  * Returns default index definitions for the scores domain tables.
16785
17595
  * @param schemaPrefix - Prefix for index names (e.g. "my_schema_" or "")
@@ -16854,6 +17664,16 @@ var ScoresPG = class _ScoresPG extends storage.ScoresStorage {
16854
17664
  async dangerouslyClearAll() {
16855
17665
  await this.#db.clearTable({ tableName: storage.TABLE_SCORERS });
16856
17666
  }
17667
+ /** Delete scorer results older than the `scorers` policy's `maxAge`, batched. */
17668
+ async prune(policies, options) {
17669
+ await this.ensureRetentionIndexes(policies);
17670
+ const targets = resolveTargets({
17671
+ policies,
17672
+ descriptor: _ScoresPG.retentionTables,
17673
+ order: ["scorers"]
17674
+ });
17675
+ return runPrune({ db: this.#db, domain: "scores", targets, options });
17676
+ }
16857
17677
  async getScoreById({ id }) {
16858
17678
  try {
16859
17679
  const result = await this.#db.client.oneOrNone(
@@ -16877,7 +17697,8 @@ var ScoresPG = class _ScoresPG extends storage.ScoresStorage {
16877
17697
  pagination,
16878
17698
  entityId,
16879
17699
  entityType,
16880
- source
17700
+ source,
17701
+ filters
16881
17702
  }) {
16882
17703
  try {
16883
17704
  const conditions = [`"scorerId" = $1`];
@@ -16895,6 +17716,7 @@ var ScoresPG = class _ScoresPG extends storage.ScoresStorage {
16895
17716
  conditions.push(`"source" = $${paramIndex++}`);
16896
17717
  queryParams.push(source);
16897
17718
  }
17719
+ paramIndex = applyTenancyFilters(conditions, queryParams, paramIndex, filters);
16898
17720
  const whereClause = conditions.join(" AND ");
16899
17721
  const total = await this.#db.client.oneOrNone(
16900
17722
  `SELECT COUNT(*) FROM ${getTableName6({ indexName: storage.TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE ${whereClause}`,
@@ -17008,12 +17830,17 @@ var ScoresPG = class _ScoresPG extends storage.ScoresStorage {
17008
17830
  }
17009
17831
  async listScoresByRunId({
17010
17832
  runId,
17011
- pagination
17833
+ pagination,
17834
+ filters
17012
17835
  }) {
17013
17836
  try {
17837
+ const conditions = [`"runId" = $1`];
17838
+ const queryParams = [runId];
17839
+ let paramIndex = applyTenancyFilters(conditions, queryParams, 2, filters);
17840
+ const whereClause = conditions.join(" AND ");
17014
17841
  const total = await this.#db.client.oneOrNone(
17015
- `SELECT COUNT(*) FROM ${getTableName6({ indexName: storage.TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE "runId" = $1`,
17016
- [runId]
17842
+ `SELECT COUNT(*) FROM ${getTableName6({ indexName: storage.TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE ${whereClause}`,
17843
+ queryParams
17017
17844
  );
17018
17845
  const { page, perPage: perPageInput } = pagination;
17019
17846
  const perPage = storage.normalizePerPage(perPageInput, 100);
@@ -17032,8 +17859,8 @@ var ScoresPG = class _ScoresPG extends storage.ScoresStorage {
17032
17859
  const limitValue = perPageInput === false ? Number(total?.count) : perPage;
17033
17860
  const end = perPageInput === false ? Number(total?.count) : start + perPage;
17034
17861
  const result = await this.#db.client.manyOrNone(
17035
- `SELECT * FROM ${getTableName6({ indexName: storage.TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE "runId" = $1 LIMIT $2 OFFSET $3`,
17036
- [runId, limitValue, start]
17862
+ `SELECT * FROM ${getTableName6({ indexName: storage.TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE ${whereClause} ORDER BY "createdAt" DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
17863
+ [...queryParams, limitValue, start]
17037
17864
  );
17038
17865
  return {
17039
17866
  pagination: {
@@ -17058,12 +17885,17 @@ var ScoresPG = class _ScoresPG extends storage.ScoresStorage {
17058
17885
  async listScoresByEntityId({
17059
17886
  entityId,
17060
17887
  entityType,
17061
- pagination
17888
+ pagination,
17889
+ filters
17062
17890
  }) {
17063
17891
  try {
17892
+ const conditions = [`"entityId" = $1`, `"entityType" = $2`];
17893
+ const queryParams = [entityId, entityType];
17894
+ let paramIndex = applyTenancyFilters(conditions, queryParams, 3, filters);
17895
+ const whereClause = conditions.join(" AND ");
17064
17896
  const total = await this.#db.client.oneOrNone(
17065
- `SELECT COUNT(*) FROM ${getTableName6({ indexName: storage.TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE "entityId" = $1 AND "entityType" = $2`,
17066
- [entityId, entityType]
17897
+ `SELECT COUNT(*) FROM ${getTableName6({ indexName: storage.TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE ${whereClause}`,
17898
+ queryParams
17067
17899
  );
17068
17900
  const { page, perPage: perPageInput } = pagination;
17069
17901
  const perPage = storage.normalizePerPage(perPageInput, 100);
@@ -17082,8 +17914,8 @@ var ScoresPG = class _ScoresPG extends storage.ScoresStorage {
17082
17914
  const limitValue = perPageInput === false ? Number(total?.count) : perPage;
17083
17915
  const end = perPageInput === false ? Number(total?.count) : start + perPage;
17084
17916
  const result = await this.#db.client.manyOrNone(
17085
- `SELECT * FROM ${getTableName6({ indexName: storage.TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE "entityId" = $1 AND "entityType" = $2 LIMIT $3 OFFSET $4`,
17086
- [entityId, entityType, limitValue, start]
17917
+ `SELECT * FROM ${getTableName6({ indexName: storage.TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) })} WHERE ${whereClause} ORDER BY "createdAt" DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
17918
+ [...queryParams, limitValue, start]
17087
17919
  );
17088
17920
  return {
17089
17921
  pagination: {
@@ -17108,13 +17940,18 @@ var ScoresPG = class _ScoresPG extends storage.ScoresStorage {
17108
17940
  async listScoresBySpan({
17109
17941
  traceId,
17110
17942
  spanId,
17111
- pagination
17943
+ pagination,
17944
+ filters
17112
17945
  }) {
17113
17946
  try {
17114
17947
  const tableName = getTableName6({ indexName: storage.TABLE_SCORERS, schemaName: getSchemaName6(this.#schema) });
17948
+ const conditions = [`"traceId" = $1`, `"spanId" = $2`];
17949
+ const queryParams = [traceId, spanId];
17950
+ let paramIndex = applyTenancyFilters(conditions, queryParams, 3, filters);
17951
+ const whereClause = conditions.join(" AND ");
17115
17952
  const countSQLResult = await this.#db.client.oneOrNone(
17116
- `SELECT COUNT(*) as count FROM ${tableName} WHERE "traceId" = $1 AND "spanId" = $2`,
17117
- [traceId, spanId]
17953
+ `SELECT COUNT(*) as count FROM ${tableName} WHERE ${whereClause}`,
17954
+ queryParams
17118
17955
  );
17119
17956
  const total = Number(countSQLResult?.count ?? 0);
17120
17957
  const { page, perPage: perPageInput } = pagination;
@@ -17123,8 +17960,8 @@ var ScoresPG = class _ScoresPG extends storage.ScoresStorage {
17123
17960
  const limitValue = perPageInput === false ? total : perPage;
17124
17961
  const end = perPageInput === false ? total : start + perPage;
17125
17962
  const result = await this.#db.client.manyOrNone(
17126
- `SELECT * FROM ${tableName} WHERE "traceId" = $1 AND "spanId" = $2 ORDER BY "createdAt" DESC LIMIT $3 OFFSET $4`,
17127
- [traceId, spanId, limitValue, start]
17963
+ `SELECT * FROM ${tableName} WHERE ${whereClause} ORDER BY "createdAt" DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
17964
+ [...queryParams, limitValue, start]
17128
17965
  );
17129
17966
  const hasMore = end < total;
17130
17967
  const scores = result.map((row) => transformScoreRow(row)) ?? [];
@@ -18175,6 +19012,14 @@ var WorkflowsPG = class _WorkflowsPG extends storage.WorkflowsStorage {
18175
19012
  #indexes;
18176
19013
  /** Tables managed by this domain */
18177
19014
  static MANAGED_TABLES = [storage.TABLE_WORKFLOW_SNAPSHOT];
19015
+ /**
19016
+ * Workflow run snapshots accumulate as runs execute. Anchored on the
19017
+ * timezone-aware `updatedAtZ` mirror column (last activity) so suspended or
19018
+ * long-running runs are not pruned by start age.
19019
+ */
19020
+ static retentionTables = {
19021
+ workflowSnapshot: { table: storage.TABLE_WORKFLOW_SNAPSHOT, column: "updatedAtZ", indexed: true }
19022
+ };
18178
19023
  constructor(config) {
18179
19024
  super();
18180
19025
  const { client, schemaName, skipDefaultIndexes, indexes } = resolvePgConfig(config);
@@ -18246,6 +19091,40 @@ var WorkflowsPG = class _WorkflowsPG extends storage.WorkflowsStorage {
18246
19091
  await this.createDefaultIndexes();
18247
19092
  await this.createCustomIndexes();
18248
19093
  }
19094
+ /**
19095
+ * Lazily ensures a btree index exists on each configured policy's retention
19096
+ * anchor column so age-based `prune()` deletes stay fast on large tables.
19097
+ * Called from the prune path (not init) so only deployments that configure
19098
+ * retention pay the index's write/disk overhead. Best-effort: failures are
19099
+ * logged and pruning proceeds (correct, just slower).
19100
+ * Created even with `skipDefaultIndexes` — retention is an explicit opt-in,
19101
+ * so its supporting index is not part of the default index set.
19102
+ */
19103
+ async ensureRetentionIndexes(policies) {
19104
+ const prefix = this.#schema && this.#schema !== "public" ? `${this.#schema}_` : "";
19105
+ for (const [key, entry] of Object.entries(_WorkflowsPG.retentionTables)) {
19106
+ if (!entry.indexed || !policies[key]) continue;
19107
+ try {
19108
+ await this.#db.ensureIndex({
19109
+ indexName: `${prefix}mastra_${key}_retention_idx`,
19110
+ tableName: entry.table,
19111
+ column: entry.column
19112
+ });
19113
+ } catch (error) {
19114
+ this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
19115
+ }
19116
+ }
19117
+ }
19118
+ /** Delete workflow run snapshots older than the `workflowSnapshot` policy's `maxAge`, batched. */
19119
+ async prune(policies, options) {
19120
+ await this.ensureRetentionIndexes(policies);
19121
+ const targets = resolveTargets({
19122
+ policies,
19123
+ descriptor: _WorkflowsPG.retentionTables,
19124
+ order: ["workflowSnapshot"]
19125
+ });
19126
+ return runPrune({ db: this.#db, domain: "workflows", targets, options });
19127
+ }
18249
19128
  /**
18250
19129
  * Creates custom user-defined indexes for this domain's tables.
18251
19130
  */
@@ -19359,7 +20238,7 @@ var PostgresStore = class extends storage.MastraCompositeStore {
19359
20238
  constructor(config) {
19360
20239
  try {
19361
20240
  validateConfig("PostgresStore", config);
19362
- super({ id: config.id, name: "PostgresStore", disableInit: config.disableInit });
20241
+ super({ id: config.id, name: "PostgresStore", disableInit: config.disableInit, retention: config.retention });
19363
20242
  this.schema = utils.parseSqlIdentifier(config.schemaName || "public", "schema name");
19364
20243
  if (isPoolConfig(config)) {
19365
20244
  this.#pool = config.pool;