@mastra/pg 1.26.0-alpha.2 → 1.26.0-alpha.4

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.
@@ -3,7 +3,7 @@ name: mastra-pg
3
3
  description: Documentation for @mastra/pg. Use when working with @mastra/pg APIs, configuration, or implementation.
4
4
  metadata:
5
5
  package: "@mastra/pg"
6
- version: "1.26.0-alpha.2"
6
+ version: "1.26.0-alpha.4"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.26.0-alpha.2",
2
+ "version": "1.26.0-alpha.4",
3
3
  "package": "@mastra/pg",
4
4
  "exports": {},
5
5
  "modules": {}
@@ -51,7 +51,7 @@ export const agent = new Agent({
51
51
 
52
52
  **options.observationalMemory** (`boolean | ObservationalMemoryOptions`): Enable Observational Memory for long-context agentic memory. Set to true for defaults, or pass a config object to customize token budgets, models, and scope. See Observational Memory reference for configuration details.
53
53
 
54
- **options.generateTitle** (`boolean | { model: DynamicArgument<MastraLanguageModel>; instructions?: DynamicArgument<string> }`): Controls automatic thread title generation from the conversation transcript. Can be a boolean or an object with custom model and instructions.
54
+ **options.generateTitle** (`boolean | { model?: DynamicArgument<MastraModelConfig>; instructions?: DynamicArgument<string>; minMessages?: number; emitEvent?: boolean }`): Controls automatic thread title generation from the conversation transcript. Accepts a boolean or an object with a custom model (any MastraModelConfig: a model instance, a "provider/model" ID, or an OpenAI-compatible config; defaults to the agent's own model), custom instructions, a minimum message count, and emitEvent. With emitEvent: true the run's stream waits for the title and emits it as a transient data-thread-title chunk before finish (durable and evented agents persist the title but don't emit the chunk yet).
55
55
 
56
56
  ## Returns
57
57
 
@@ -345,6 +345,29 @@ await store.upsert({
345
345
 
346
346
  For detailed setup instructions and best practices, see the [official Elasticsearch documentation](https://www.elastic.co/docs/solutions/search/get-started).
347
347
 
348
+ **Azure AI Search**:
349
+
350
+ ```ts
351
+ import { AzureAISearchVector } from '@mastra/azure-ai-search'
352
+
353
+ const store = new AzureAISearchVector({
354
+ id: 'azure-search-vectors',
355
+ endpoint: process.env.AZURE_AI_SEARCH_ENDPOINT!,
356
+ credential: process.env.AZURE_AI_SEARCH_CREDENTIAL!,
357
+ })
358
+
359
+ await store.createIndex({
360
+ indexName: 'my-collection',
361
+ dimension: 1536,
362
+ })
363
+
364
+ await store.upsert({
365
+ indexName: 'my-collection',
366
+ vectors: embeddings,
367
+ metadata: chunks.map(chunk => ({ text: chunk.text })),
368
+ })
369
+ ```
370
+
348
371
  **Couchbase**:
349
372
 
350
373
  ```ts
@@ -626,6 +649,17 @@ Index names must:
626
649
  - Example: `myindex-` isn't valid (ends with hyphen)
627
650
  - Example: `MyIndex` isn't valid (contains uppercase letters)
628
651
 
652
+ **Azure AI Search**:
653
+
654
+ Index names must:
655
+
656
+ - Use only lowercase letters, numbers, dashes (`-`), and underscore (`_`) characters
657
+ - Not start or end with a dash
658
+ - Be between 2 and 128 characters long
659
+ - Example: `my-index-123` and `my_index` are valid
660
+ - Example: `MyIndex` isn't valid (contains uppercase letters)
661
+ - Example: `my-index-` isn't valid (ends with a dash)
662
+
629
663
  ### Upserting Embeddings
630
664
 
631
665
  After creating an index, you can store embeddings along with their basic metadata:
package/dist/index.cjs CHANGED
@@ -2360,6 +2360,8 @@ var RoutingDbClient = class {
2360
2360
  return this.active.tx(callback);
2361
2361
  }
2362
2362
  };
2363
+ /** Bytes reserved for the `_xxxxxxxx` collision suffix when hashWhenTruncated applies. */
2364
+ const TRUNCATION_HASH_SUFFIX_LENGTH = 9;
2363
2365
  function truncateIdentifier(value, maxLength = 63) {
2364
2366
  if (maxLength <= 0) return "";
2365
2367
  if (Buffer.byteLength(value, "utf-8") <= maxLength) return value;
@@ -2380,9 +2382,25 @@ function truncateIdentifier(value, maxLength = 63) {
2380
2382
  * in system catalogs (pg_constraint.conname, pg_indexes.indexname, etc.).
2381
2383
  * Without this normalisation, runtime lookups that compare a mixed-case name
2382
2384
  * against the catalog would silently fail.
2385
+ *
2386
+ * With `hashWhenTruncated`, a name that exceeds the limit is truncated further
2387
+ * to make room for `_` + 8 hex chars of the full name's sha256. Plain
2388
+ * truncation cuts the tail, so two names sharing a long `<schema>_<prefix>`
2389
+ * collapse to the same identifier and `CREATE INDEX IF NOT EXISTS` (which
2390
+ * matches by name only) silently skips the second one. The suffix is
2391
+ * deterministic, so creation, warm-init snapshot checks, and DDL export all
2392
+ * agree on the same name. Opt-in because renaming already-released constraint
2393
+ * names would orphan the existing objects in deployed catalogs.
2383
2394
  */
2384
- function buildConstraintName({ baseName, schemaName, maxLength = 63 }) {
2385
- return truncateIdentifier(`${schemaName ? `${schemaName}_` : ""}${baseName}`.toLowerCase(), maxLength);
2395
+ function buildConstraintName({ baseName, schemaName, maxLength = 63, hashWhenTruncated = false }) {
2396
+ const fullName = `${schemaName ? `${schemaName}_` : ""}${baseName}`.toLowerCase();
2397
+ if (hashWhenTruncated && Buffer.byteLength(fullName, "utf-8") > maxLength) {
2398
+ const suffixLength = Math.min(TRUNCATION_HASH_SUFFIX_LENGTH, maxLength);
2399
+ if (suffixLength < 2) return truncateIdentifier(fullName, maxLength);
2400
+ const hash = (0, crypto$1.createHash)("sha256").update(fullName).digest("hex").slice(0, suffixLength - 1);
2401
+ return `${truncateIdentifier(fullName, maxLength - suffixLength)}_${hash}`;
2402
+ }
2403
+ return truncateIdentifier(fullName, maxLength);
2386
2404
  }
2387
2405
  //#endregion
2388
2406
  //#region src/storage/db/pg-errors.ts
@@ -10782,7 +10800,7 @@ var MemoryPG = class MemoryPG extends _mastra_core_storage.MemoryStorage {
10782
10800
  hasMore: false
10783
10801
  };
10784
10802
  const limitValue = perPageInput === false ? total : perPage;
10785
- const dataQuery = `SELECT id, "resourceId", title, metadata, "createdAt", "createdAtZ", "updatedAt", "updatedAtZ" ${baseQuery} ORDER BY COALESCE("${field}Z", "${field}") ${direction} LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
10803
+ const dataQuery = `SELECT id, "resourceId", title, metadata, "createdAt", "createdAtZ", "updatedAt", "updatedAtZ" ${baseQuery} ORDER BY COALESCE("${field}Z", "${field}") ${direction}, "id" ${direction} LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
10786
10804
  return {
10787
10805
  threads: (await this.#db.readClient.manyOrNone(dataQuery, [
10788
10806
  ...queryParams,
@@ -10975,10 +10993,11 @@ var MemoryPG = class MemoryPG extends _mastra_core_storage.MemoryStorage {
10975
10993
  return messages.sort((a, b) => {
10976
10994
  const aValue = field === "createdAt" ? new Date(a.createdAt).getTime() : a[field];
10977
10995
  const bValue = field === "createdAt" ? new Date(b.createdAt).getTime() : b[field];
10978
- if (aValue == null && bValue == null) return a.id.localeCompare(b.id);
10996
+ const idOrder = direction === "ASC" ? a.id.localeCompare(b.id) : b.id.localeCompare(a.id);
10997
+ if (aValue == null && bValue == null) return idOrder;
10979
10998
  if (aValue == null) return 1;
10980
10999
  if (bValue == null) return -1;
10981
- if (aValue === bValue) return a.id.localeCompare(b.id);
11000
+ if (aValue === bValue) return idOrder;
10982
11001
  if (typeof aValue === "number" && typeof bValue === "number") return direction === "ASC" ? aValue - bValue : bValue - aValue;
10983
11002
  return direction === "ASC" ? String(aValue).localeCompare(String(bValue)) : String(bValue).localeCompare(String(aValue));
10984
11003
  });
@@ -11181,7 +11200,7 @@ var MemoryPG = class MemoryPG extends _mastra_core_storage.MemoryStorage {
11181
11200
  const metadataFilter = (0, _mastra_core_storage.validateStorageMetadataFilter)(filter?.metadata);
11182
11201
  try {
11183
11202
  const { field, direction } = this.parseOrderBy(orderBy, "ASC");
11184
- const orderByStatement = `ORDER BY "${field}" ${direction}`;
11203
+ const orderByStatement = `ORDER BY "${field}" ${direction}, "id" ${direction}`;
11185
11204
  const selectStatement = `SELECT id, content, role, type, "createdAt", "createdAtZ", thread_id AS "threadId", "resourceId"`;
11186
11205
  const tableName = getTableName$3({
11187
11206
  indexName: _mastra_core_storage.TABLE_MESSAGES,
@@ -11336,7 +11355,7 @@ var MemoryPG = class MemoryPG extends _mastra_core_storage.MemoryStorage {
11336
11355
  const metadataFilter = (0, _mastra_core_storage.validateStorageMetadataFilter)(filter?.metadata);
11337
11356
  try {
11338
11357
  const { field, direction } = this.parseOrderBy(orderBy, "ASC");
11339
- const orderByStatement = `ORDER BY "${field}" ${direction}`;
11358
+ const orderByStatement = `ORDER BY "${field}" ${direction}, "id" ${direction}`;
11340
11359
  const selectStatement = `SELECT id, content, role, type, "createdAt", "createdAtZ", thread_id AS "threadId", "resourceId"`;
11341
11360
  const tableName = getTableName$3({
11342
11361
  indexName: _mastra_core_storage.TABLE_MESSAGES,
@@ -14831,6 +14850,11 @@ function tableIndexes() {
14831
14850
  columns: "(\"tags\")",
14832
14851
  using: "gin"
14833
14852
  },
14853
+ {
14854
+ name: "mastra_score_events_scoreid_cursor_idx",
14855
+ table: TABLE_SCORE_EVENTS,
14856
+ columns: "(\"scoreId\", \"cursorId\" DESC)"
14857
+ },
14834
14858
  {
14835
14859
  name: "mastra_score_events_cursor_idx",
14836
14860
  table: TABLE_SCORE_EVENTS,
@@ -16963,13 +16987,32 @@ function pushScoreIdentity(acc, scorerId, scoreSource) {
16963
16987
  acc.params.push(scoreSource);
16964
16988
  }
16965
16989
  }
16990
+ function scoreRewriteConflict(row) {
16991
+ return `ON CONFLICT ("scoreId", "timestamp") DO UPDATE SET ${[
16992
+ ...Object.keys(row).filter((column) => column !== "scoreId" && column !== "timestamp").map((column) => `"${column}" = EXCLUDED."${column}"`),
16993
+ "\"cursorId\" = EXCLUDED.\"cursorId\"",
16994
+ "\"xactId\" = EXCLUDED.\"xactId\""
16995
+ ].join(", ")}`;
16996
+ }
16997
+ function collapseExactScoreConflicts(rows) {
16998
+ const records = /* @__PURE__ */ new Map();
16999
+ for (const row of rows) {
17000
+ const timestamp = new Date(row.timestamp).toISOString();
17001
+ const key = `${String(row.scoreId)}\u0000${timestamp}`;
17002
+ records.delete(key);
17003
+ records.set(key, row);
17004
+ }
17005
+ return [...records.values()];
17006
+ }
16966
17007
  async function createScore(client, schema, args) {
16967
- const insert = buildInsert(schema, TABLE_SCORE_EVENTS, [scoreRecordToRow(args.score)]);
17008
+ const row = scoreRecordToRow(args.score);
17009
+ const insert = buildInsert(schema, TABLE_SCORE_EVENTS, [row], scoreRewriteConflict(row));
16968
17010
  if (insert) await client.query(insert.text, insert.values);
16969
17011
  }
16970
17012
  async function batchCreateScores(client, schema, args) {
16971
17013
  if (args.scores.length === 0) return;
16972
- const insert = buildInsert(schema, TABLE_SCORE_EVENTS, args.scores.map(scoreRecordToRow));
17014
+ const rows = collapseExactScoreConflicts(args.scores.map(scoreRecordToRow));
17015
+ const insert = buildInsert(schema, TABLE_SCORE_EVENTS, rows, scoreRewriteConflict(rows[0]));
16973
17016
  if (insert) await client.query(insert.text, insert.values);
16974
17017
  }
16975
17018
  /**
@@ -16992,6 +17035,16 @@ async function deleteScores(client, schema, args) {
16992
17035
  }
16993
17036
  await client.query(`DELETE FROM ${table} WHERE ${conditions.join(" AND ")}`, values);
16994
17037
  }
17038
+ function latestScorePredicate(table, alias = "s") {
17039
+ return `NOT EXISTS (
17040
+ SELECT 1 FROM ${table} newer
17041
+ WHERE newer."scoreId" = ${alias}."scoreId"
17042
+ AND newer."cursorId" > ${alias}."cursorId"
17043
+ )`;
17044
+ }
17045
+ function applyLatestScorePredicate(acc, table) {
17046
+ acc.conditions.push(latestScorePredicate(table));
17047
+ }
16995
17048
  async function listScores(client, schema, args) {
16996
17049
  const { mode, filters, pagination, orderBy, after, limit } = _mastra_core_storage.listScoresArgsSchema.parse(args);
16997
17050
  const table = qualifiedTable(schema, TABLE_SCORE_EVENTS);
@@ -17002,28 +17055,50 @@ async function listScores(client, schema, args) {
17002
17055
  return listScoresPage(client, table, filters, pagination.page, pagination.perPage, orderBy.field, orderBy.direction);
17003
17056
  }
17004
17057
  async function getScoreById(client, schema, scoreId) {
17058
+ const table = qualifiedTable(schema, TABLE_SCORE_EVENTS);
17005
17059
  const row = await client.oneOrNone(`SELECT ${SCORE_SELECT_COLUMNS}
17006
- FROM ${qualifiedTable(schema, TABLE_SCORE_EVENTS)}
17060
+ FROM ${table}
17007
17061
  WHERE "scoreId" = $1
17008
- ORDER BY "timestamp" DESC
17062
+ ORDER BY "cursorId" DESC
17009
17063
  LIMIT 1`, [scoreId]);
17010
17064
  return row ? rowToScoreRecord(row) : null;
17011
17065
  }
17012
17066
  async function listScoresPage(client, table, filters, page, perPage, orderField, orderDir) {
17013
- return listSignalPage({
17067
+ const acc = newFilterAccumulator();
17068
+ applyScoreFilters(acc, filters);
17069
+ applyLatestScorePredicate(acc, table);
17070
+ const whereClause = whereOrEmpty(acc);
17071
+ const countRow = await client.oneOrNone(`SELECT COUNT(*)::text AS count FROM ${table} s ${whereClause}`, acc.params);
17072
+ const total = Number(countRow?.count ?? 0);
17073
+ let scores = [];
17074
+ if (total > 0) {
17075
+ const safeOrderField = (0, _mastra_core_utils.parseSqlIdentifier)(orderField, "order field");
17076
+ scores = (await client.manyOrNone(`SELECT ${SCORE_SELECT_COLUMNS}
17077
+ FROM ${table} s
17078
+ ${whereClause}
17079
+ ORDER BY "${safeOrderField}" ${orderDir}, "cursorId" ${orderDir}
17080
+ LIMIT $${acc.next++} OFFSET $${acc.next++}`, [
17081
+ ...acc.params,
17082
+ perPage,
17083
+ page * perPage
17084
+ ])).map(rowToScoreRecord);
17085
+ }
17086
+ const deltaCursor = deltaPollingFeatureEnabled() ? await readSignalStreamHeadCursor({
17014
17087
  client,
17015
17088
  table,
17016
17089
  filters,
17017
- page,
17018
- perPage,
17019
- orderField,
17020
- orderDir,
17021
- includeDeltaCursor: deltaPollingFeatureEnabled(),
17022
- selectColumns: SCORE_SELECT_COLUMNS,
17023
- responseKey: "scores",
17024
- applyFilters: applyScoreFilters,
17025
- mapRow: rowToScoreRecord
17026
- });
17090
+ applyFilters: applyScoreFilters
17091
+ }) : void 0;
17092
+ return {
17093
+ scores,
17094
+ pagination: {
17095
+ total,
17096
+ page,
17097
+ perPage,
17098
+ hasMore: (page + 1) * perPage < total
17099
+ },
17100
+ ...deltaCursor !== void 0 ? { deltaCursor } : {}
17101
+ };
17027
17102
  }
17028
17103
  async function listScoresDelta(client, table, filters, after, limit) {
17029
17104
  return listSignalDelta({
@@ -17039,12 +17114,14 @@ async function listScoresDelta(client, table, filters, after, limit) {
17039
17114
  });
17040
17115
  }
17041
17116
  async function runScoreAggregateQuery(client, schema, args, filters) {
17117
+ const table = qualifiedTable(schema, TABLE_SCORE_EVENTS);
17042
17118
  const acc = newFilterAccumulator();
17043
17119
  pushScoreIdentity(acc, args.scorerId, args.scoreSource);
17044
17120
  applyScoreFilters(acc, filters);
17121
+ applyLatestScorePredicate(acc, table);
17045
17122
  const sql = `
17046
17123
  SELECT ${aggregationSql(args.aggregation, "\"score\"")} AS "value"
17047
- FROM ${qualifiedTable(schema, TABLE_SCORE_EVENTS)}
17124
+ FROM ${table} s
17048
17125
  ${whereOrEmpty(acc)}
17049
17126
  `;
17050
17127
  const row = await client.oneOrNone(sql, acc.params);
@@ -17076,10 +17153,12 @@ async function getScoreBreakdown(client, schema, args) {
17076
17153
  });
17077
17154
  pushScoreIdentity(acc, args.scorerId, args.scoreSource);
17078
17155
  applyScoreFilters(acc, args.filters);
17156
+ const table = qualifiedTable(schema, TABLE_SCORE_EVENTS);
17157
+ applyLatestScorePredicate(acc, table);
17079
17158
  const sql = `
17080
17159
  SELECT ${resolved.map((e) => e.selectSql).join(", ")},
17081
17160
  ${aggregationSql(args.aggregation, "\"score\"")} AS "value"
17082
- FROM ${qualifiedTable(schema, TABLE_SCORE_EVENTS)}
17161
+ FROM ${table} s
17083
17162
  ${whereOrEmpty(acc)}
17084
17163
  GROUP BY ${resolved.map((e) => e.alias).join(", ")}
17085
17164
  ORDER BY "value" DESC NULLS LAST
@@ -17099,11 +17178,13 @@ async function getScoreTimeSeries(client, schema, args) {
17099
17178
  });
17100
17179
  pushScoreIdentity(acc, args.scorerId, args.scoreSource);
17101
17180
  applyScoreFilters(acc, args.filters);
17181
+ const table = qualifiedTable(schema, TABLE_SCORE_EVENTS);
17182
+ applyLatestScorePredicate(acc, table);
17102
17183
  const sql = `
17103
17184
  SELECT ${bucket} AS bucket,
17104
17185
  ${resolved.map((e) => e.selectSql).join(", ")},
17105
17186
  ${aggregationSql(args.aggregation, "\"score\"")} AS "value"
17106
- FROM ${qualifiedTable(schema, TABLE_SCORE_EVENTS)}
17187
+ FROM ${table} s
17107
17188
  ${whereOrEmpty(acc)}
17108
17189
  GROUP BY bucket, ${resolved.map((e) => e.alias).join(", ")}
17109
17190
  ORDER BY bucket
@@ -17121,10 +17202,12 @@ async function getScoreTimeSeries(client, schema, args) {
17121
17202
  const acc = newFilterAccumulator();
17122
17203
  pushScoreIdentity(acc, args.scorerId, args.scoreSource);
17123
17204
  applyScoreFilters(acc, args.filters);
17205
+ const table = qualifiedTable(schema, TABLE_SCORE_EVENTS);
17206
+ applyLatestScorePredicate(acc, table);
17124
17207
  const sql = `
17125
17208
  SELECT ${bucket} AS bucket,
17126
17209
  ${aggregationSql(args.aggregation, "\"score\"")} AS "value"
17127
- FROM ${qualifiedTable(schema, TABLE_SCORE_EVENTS)}
17210
+ FROM ${table} s
17128
17211
  ${whereOrEmpty(acc)}
17129
17212
  GROUP BY bucket
17130
17213
  ORDER BY bucket
@@ -17144,9 +17227,11 @@ async function getScorePercentiles(client, schema, args) {
17144
17227
  const acc = newFilterAccumulator();
17145
17228
  pushScoreIdentity(acc, args.scorerId, args.scoreSource);
17146
17229
  applyScoreFilters(acc, args.filters);
17230
+ const table = qualifiedTable(schema, TABLE_SCORE_EVENTS);
17231
+ applyLatestScorePredicate(acc, table);
17147
17232
  const sql = `
17148
17233
  SELECT ${bucket} AS bucket, ${percentileSelectSql(args.percentiles, "\"score\"")}
17149
- FROM ${qualifiedTable(schema, TABLE_SCORE_EVENTS)}
17234
+ FROM ${table} s
17150
17235
  ${whereOrEmpty(acc)}
17151
17236
  GROUP BY bucket
17152
17237
  ORDER BY bucket
@@ -17345,13 +17430,6 @@ function latestSpanPredicate$1(spanTable) {
17345
17430
  AND (newer."isPending" < s."isPending" OR (newer."isPending" = s."isPending" AND newer."cursorId" > s."cursorId"))
17346
17431
  )`;
17347
17432
  }
17348
- function latestScorePredicate(scoreTable) {
17349
- return `NOT EXISTS (
17350
- SELECT 1 FROM ${scoreTable} newer
17351
- WHERE newer."scoreId" = s."scoreId"
17352
- AND newer."cursorId" > s."cursorId"
17353
- )`;
17354
- }
17355
17433
  function latestFeedbackPredicate(feedbackTable) {
17356
17434
  return `NOT EXISTS (
17357
17435
  SELECT 1 FROM ${feedbackTable} newer
@@ -17521,7 +17599,7 @@ function compilePostgresTraceScope(schema, selection, relationCollections) {
17521
17599
  values
17522
17600
  };
17523
17601
  }
17524
- function compilePostgresTraceQuery(schema, plan) {
17602
+ function compilePostgresTraceQuery(schema, plan, mode = "data") {
17525
17603
  const { ctes, values } = compilePostgresTraceScope(schema, plan, collectRelationCollections(plan.where));
17526
17604
  let predicateSql = "TRUE";
17527
17605
  if (plan.where) {
@@ -17535,6 +17613,12 @@ function compilePostgresTraceQuery(schema, plan) {
17535
17613
  WHERE ${predicateSql}
17536
17614
  )`);
17537
17615
  const candidates = `WITH ${ctes.join(",\n")}`;
17616
+ if (plan.paginationMode === "page" && mode === "count") return {
17617
+ text: `${candidates}
17618
+ SELECT COUNT(*)::text AS count
17619
+ FROM candidates`,
17620
+ values
17621
+ };
17538
17622
  if (plan.result === "groups") {
17539
17623
  const pageCondition = plan.cursor ? `AND "threadId" > $${values.length + 1}` : "";
17540
17624
  if (plan.cursor) values.push(plan.cursor.threadId);
@@ -17552,6 +17636,17 @@ LIMIT $${values.length}`,
17552
17636
  }
17553
17637
  const orderField = plan.orderBy.field === "startedAt" ? "\"startedAt\"" : "\"endedAt\"";
17554
17638
  const direction = plan.orderBy.direction === "asc" ? "ASC" : "DESC";
17639
+ if (plan.paginationMode === "page") {
17640
+ values.push(plan.perPage, plan.page * plan.perPage);
17641
+ return {
17642
+ text: `${candidates}
17643
+ SELECT *
17644
+ FROM candidates
17645
+ ORDER BY ${orderField} ${direction}, "traceId" ASC
17646
+ LIMIT $${values.length - 1} OFFSET $${values.length}`,
17647
+ values
17648
+ };
17649
+ }
17555
17650
  let pageCondition = "";
17556
17651
  if (plan.cursor) {
17557
17652
  const comparison = plan.orderBy.direction === "asc" ? ">" : "<";
@@ -17698,10 +17793,11 @@ function isPostgresResourceLimit(error) {
17698
17793
  const candidate = error;
17699
17794
  return candidate.code === "53200" || candidate.code === "53400";
17700
17795
  }
17701
- async function runWithPostgresTraceQueryTimeout(client, timeoutMs, execute) {
17796
+ async function runWithPostgresTraceQueryTimeout(client, timeoutMs, execute, options = {}) {
17702
17797
  const resolvedTimeoutMs = _mastra_core_storage.resolveTraceQueryTimeoutMs(timeoutMs);
17703
17798
  try {
17704
17799
  return await client.tx(async (transaction) => {
17800
+ if (options.repeatableRead) await transaction.query("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ");
17705
17801
  await transaction.query(`SELECT set_config('statement_timeout', $1, true)`, [`${resolvedTimeoutMs}ms`]);
17706
17802
  return execute(transaction);
17707
17803
  });
@@ -17735,6 +17831,50 @@ async function getTraceQueryValues(client, schema, plan, timeoutMs) {
17735
17831
  });
17736
17832
  }
17737
17833
  async function queryTraces(client, schema, plan, timeoutMs) {
17834
+ if (plan.paginationMode === "page") {
17835
+ const resolvedTimeoutMs = _mastra_core_storage.resolveTraceQueryTimeoutMs(timeoutMs);
17836
+ const deadline = performance.now() + resolvedTimeoutMs;
17837
+ const countQuery = compilePostgresTraceQuery(schema, plan, "count");
17838
+ const dataQuery = compilePostgresTraceQuery(schema, plan);
17839
+ const { total, rows } = await runWithPostgresTraceQueryTimeout(client, resolvedTimeoutMs, async (transaction) => {
17840
+ const countRows = await transaction.any(countQuery.text, countQuery.values);
17841
+ const remainingTimeoutMs = Math.floor(deadline - performance.now());
17842
+ if (remainingTimeoutMs <= 0) throw new _mastra_core_storage.TraceQueryExecutionError();
17843
+ await transaction.query(`SELECT set_config('statement_timeout', $1, true)`, [`${remainingTimeoutMs}ms`]);
17844
+ const rows = await transaction.any(dataQuery.text, dataQuery.values);
17845
+ return {
17846
+ total: Number(countRows[0]?.count ?? 0),
17847
+ rows
17848
+ };
17849
+ }, { repeatableRead: true });
17850
+ const traces = rows.map((row) => ({
17851
+ traceId: String(row.traceId),
17852
+ rootSpanId: String(row.rootSpanId),
17853
+ name: row.name,
17854
+ entityId: row.entityId ?? null,
17855
+ parentSpanId: row.parentSpanId ?? null,
17856
+ createdAt: asIsoTimestamp$1(row.startedAt),
17857
+ metadata: row.metadata ?? null,
17858
+ inputPreview: _mastra_core_storage.buildInputPreview(row.input) ?? null,
17859
+ threadId: row.threadId == null ? null : String(row.threadId),
17860
+ resourceId: row.resourceId == null ? null : String(row.resourceId),
17861
+ startedAt: asIsoTimestamp$1(row.startedAt),
17862
+ endedAt: asIsoTimestamp$1(row.endedAt),
17863
+ entityName: row.entityName == null ? null : String(row.entityName),
17864
+ entityType: row.entityType == null ? null : String(row.entityType),
17865
+ environment: row.environment == null ? null : String(row.environment),
17866
+ status: row.status
17867
+ }));
17868
+ return _mastra_core_storage.traceQueryResponseSchema.parse({
17869
+ traces,
17870
+ pagination: {
17871
+ total,
17872
+ page: plan.page,
17873
+ perPage: plan.perPage,
17874
+ hasMore: (plan.page + 1) * plan.perPage < total
17875
+ }
17876
+ });
17877
+ }
17738
17878
  const query = compilePostgresTraceQuery(schema, plan);
17739
17879
  const rows = await runWithPostgresTraceQueryTimeout(client, timeoutMs, (transaction) => transaction.any(query.text, query.values));
17740
17880
  const visibleRows = rows.slice(0, plan.limit);
@@ -22195,6 +22335,7 @@ const WORKFLOW_SNAPSHOT_STATUS_INDEX = "mastra_workflow_snapshot_name_status_cre
22195
22335
  * Schema-prefixed name of the status index, lowercased and truncated the same way Postgres
22196
22336
  * stores it, so the init snapshot's index set answers "does it exist?" without a probe or a
22197
22337
  * no-op `CREATE INDEX` (schema-prefixed names routinely exceed the 63-byte limit).
22338
+ * Exported for tests.
22198
22339
  */
22199
22340
  function workflowSnapshotStatusIndexName(schemaName) {
22200
22341
  return buildConstraintName({
@@ -22212,6 +22353,48 @@ function workflowSnapshotStatusIndexSQL(indexName, schemaName) {
22212
22353
  schemaName: getSchemaName(schemaName)
22213
22354
  })} (workflow_name, (snapshot ->> 'status'), "createdAt" DESC)`;
22214
22355
  }
22356
+ /** Base name (before any schema prefix) of the expression index backing the threadId filter. */
22357
+ const WORKFLOW_SNAPSHOT_THREAD_ID_INDEX = "mastra_workflow_snapshot_threadid_idx";
22358
+ /**
22359
+ * Schema-prefixed name of the threadId index (see workflowSnapshotStatusIndexName).
22360
+ *
22361
+ * Unlike the status index, truncation appends a collision hash: both index names share the
22362
+ * long `<schema>_mastra_workflow_snapshot_` prefix, so with a schema name of 37+ bytes plain
22363
+ * truncation collapses them to the same 63-byte identifier and `CREATE INDEX IF NOT EXISTS`
22364
+ * silently skips this index. The status index keeps plain truncation because its truncated
22365
+ * name already exists in deployed catalogs; this index is new and free to adopt the rule.
22366
+ * Exported for tests.
22367
+ */
22368
+ function workflowSnapshotThreadIdIndexName(schemaName) {
22369
+ return buildConstraintName({
22370
+ baseName: WORKFLOW_SNAPSHOT_THREAD_ID_INDEX,
22371
+ schemaName: schemaName && schemaName !== "public" ? schemaName : void 0,
22372
+ hashWhenTruncated: true
22373
+ });
22374
+ }
22375
+ /**
22376
+ * Expression extracting the thread id embedded in a snapshot (jsonb columns only). Mirrors
22377
+ * the canonical extraction in `@mastra/core` (`getSnapshotMemoryInfo`), which reads one of
22378
+ * two layouts:
22379
+ * 1. agentic-loop: `context.<suspended step>.suspendPayload.__streamState.messageList.memoryInfo.threadId`
22380
+ * 2. durable loop: `context.input.messageListState.memoryInfo.threadId`
22381
+ *
22382
+ * `jsonb_path_query_first(jsonb, jsonpath)` is IMMUTABLE, so the expression is valid in an
22383
+ * expression index. The WHERE clause in listWorkflowRuns() must use this exact expression
22384
+ * text so the planner can match it against the index. If the snapshot layout changes in
22385
+ * core, this expression must be updated in lockstep or it will wrongly exclude rows.
22386
+ */
22387
+ const WORKFLOW_SNAPSHOT_THREAD_ID_EXPR = `COALESCE(jsonb_path_query_first(snapshot, '$.context.* ? (@.status == "suspended").suspendPayload.__streamState.messageList.memoryInfo.threadId') #>> '{}', snapshot #>> '{context,input,messageListState,memoryInfo,threadId}')`;
22388
+ /**
22389
+ * Expression index on the snapshot-embedded thread id so listWorkflowRuns() threadId filters
22390
+ * (Agent.listSuspendedRuns) can use an index instead of detoasting every snapshot.
22391
+ */
22392
+ function workflowSnapshotThreadIdIndexSQL(indexName, schemaName) {
22393
+ return `CREATE INDEX IF NOT EXISTS "${indexName}" ON ${getTableName({
22394
+ indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
22395
+ schemaName: getSchemaName(schemaName)
22396
+ })} ((${WORKFLOW_SNAPSHOT_THREAD_ID_EXPR}))`;
22397
+ }
22215
22398
  var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorage {
22216
22399
  #db;
22217
22400
  #schema;
@@ -22284,6 +22467,7 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
22284
22467
  }));
22285
22468
  for (const idx of WorkflowsPG.getDefaultIndexDefs(schemaPrefix)) statements.push(generateIndexSQL(idx, schemaName));
22286
22469
  statements.push(`${workflowSnapshotStatusIndexSQL(workflowSnapshotStatusIndexName(parsedSchema), schemaName)};`);
22470
+ statements.push(`${workflowSnapshotThreadIdIndexSQL(workflowSnapshotThreadIdIndexName(parsedSchema), schemaName)};`);
22287
22471
  return statements;
22288
22472
  }
22289
22473
  /**
@@ -22310,6 +22494,12 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
22310
22494
  } catch (error) {
22311
22495
  this.logger?.warn?.(`Failed to create index ${indexName}:`, error);
22312
22496
  }
22497
+ const threadIdIndexName = workflowSnapshotThreadIdIndexName(this.#schema);
22498
+ try {
22499
+ await this.#db.createIndexFromStatement(threadIdIndexName, workflowSnapshotThreadIdIndexSQL(threadIdIndexName, this.#schema));
22500
+ } catch (error) {
22501
+ this.logger?.warn?.(`Failed to create index ${threadIdIndexName}:`, error);
22502
+ }
22313
22503
  }
22314
22504
  async init() {
22315
22505
  await this.#db.createTable({
@@ -22590,7 +22780,7 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
22590
22780
  }, error);
22591
22781
  }
22592
22782
  }
22593
- async listWorkflowRuns({ workflowName, fromDate, toDate, perPage, page, resourceId, status } = {}) {
22783
+ async listWorkflowRuns({ workflowName, fromDate, toDate, perPage, page, resourceId, threadId, status } = {}) {
22594
22784
  try {
22595
22785
  const conditions = [];
22596
22786
  const values = [];
@@ -22611,6 +22801,11 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
22611
22801
  values.push(resourceId);
22612
22802
  paramIndex++;
22613
22803
  } else this.logger?.warn?.(`[${_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
22804
+ if (threadId) if (await this.#db.getColumnType(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT, "snapshot") === "jsonb") {
22805
+ conditions.push(`${WORKFLOW_SNAPSHOT_THREAD_ID_EXPR} = $${paramIndex}`);
22806
+ values.push(threadId);
22807
+ paramIndex++;
22808
+ } else this.logger?.warn?.(`[${_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT}] snapshot column is not jsonb. Skipping threadId filter.`);
22614
22809
  if (fromDate) {
22615
22810
  conditions.push(`"createdAt" >= $${paramIndex}`);
22616
22811
  values.push(fromDate);