@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.
package/dist/index.js CHANGED
@@ -2336,6 +2336,8 @@ var RoutingDbClient = class {
2336
2336
  return this.active.tx(callback);
2337
2337
  }
2338
2338
  };
2339
+ /** Bytes reserved for the `_xxxxxxxx` collision suffix when hashWhenTruncated applies. */
2340
+ const TRUNCATION_HASH_SUFFIX_LENGTH = 9;
2339
2341
  function truncateIdentifier(value, maxLength = 63) {
2340
2342
  if (maxLength <= 0) return "";
2341
2343
  if (Buffer.byteLength(value, "utf-8") <= maxLength) return value;
@@ -2356,9 +2358,25 @@ function truncateIdentifier(value, maxLength = 63) {
2356
2358
  * in system catalogs (pg_constraint.conname, pg_indexes.indexname, etc.).
2357
2359
  * Without this normalisation, runtime lookups that compare a mixed-case name
2358
2360
  * against the catalog would silently fail.
2361
+ *
2362
+ * With `hashWhenTruncated`, a name that exceeds the limit is truncated further
2363
+ * to make room for `_` + 8 hex chars of the full name's sha256. Plain
2364
+ * truncation cuts the tail, so two names sharing a long `<schema>_<prefix>`
2365
+ * collapse to the same identifier and `CREATE INDEX IF NOT EXISTS` (which
2366
+ * matches by name only) silently skips the second one. The suffix is
2367
+ * deterministic, so creation, warm-init snapshot checks, and DDL export all
2368
+ * agree on the same name. Opt-in because renaming already-released constraint
2369
+ * names would orphan the existing objects in deployed catalogs.
2359
2370
  */
2360
- function buildConstraintName({ baseName, schemaName, maxLength = 63 }) {
2361
- return truncateIdentifier(`${schemaName ? `${schemaName}_` : ""}${baseName}`.toLowerCase(), maxLength);
2371
+ function buildConstraintName({ baseName, schemaName, maxLength = 63, hashWhenTruncated = false }) {
2372
+ const fullName = `${schemaName ? `${schemaName}_` : ""}${baseName}`.toLowerCase();
2373
+ if (hashWhenTruncated && Buffer.byteLength(fullName, "utf-8") > maxLength) {
2374
+ const suffixLength = Math.min(TRUNCATION_HASH_SUFFIX_LENGTH, maxLength);
2375
+ if (suffixLength < 2) return truncateIdentifier(fullName, maxLength);
2376
+ const hash = createHash("sha256").update(fullName).digest("hex").slice(0, suffixLength - 1);
2377
+ return `${truncateIdentifier(fullName, maxLength - suffixLength)}_${hash}`;
2378
+ }
2379
+ return truncateIdentifier(fullName, maxLength);
2362
2380
  }
2363
2381
  //#endregion
2364
2382
  //#region src/storage/db/pg-errors.ts
@@ -10758,7 +10776,7 @@ var MemoryPG = class MemoryPG extends MemoryStorage {
10758
10776
  hasMore: false
10759
10777
  };
10760
10778
  const limitValue = perPageInput === false ? total : perPage;
10761
- const dataQuery = `SELECT id, "resourceId", title, metadata, "createdAt", "createdAtZ", "updatedAt", "updatedAtZ" ${baseQuery} ORDER BY COALESCE("${field}Z", "${field}") ${direction} LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
10779
+ 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}`;
10762
10780
  return {
10763
10781
  threads: (await this.#db.readClient.manyOrNone(dataQuery, [
10764
10782
  ...queryParams,
@@ -10951,10 +10969,11 @@ var MemoryPG = class MemoryPG extends MemoryStorage {
10951
10969
  return messages.sort((a, b) => {
10952
10970
  const aValue = field === "createdAt" ? new Date(a.createdAt).getTime() : a[field];
10953
10971
  const bValue = field === "createdAt" ? new Date(b.createdAt).getTime() : b[field];
10954
- if (aValue == null && bValue == null) return a.id.localeCompare(b.id);
10972
+ const idOrder = direction === "ASC" ? a.id.localeCompare(b.id) : b.id.localeCompare(a.id);
10973
+ if (aValue == null && bValue == null) return idOrder;
10955
10974
  if (aValue == null) return 1;
10956
10975
  if (bValue == null) return -1;
10957
- if (aValue === bValue) return a.id.localeCompare(b.id);
10976
+ if (aValue === bValue) return idOrder;
10958
10977
  if (typeof aValue === "number" && typeof bValue === "number") return direction === "ASC" ? aValue - bValue : bValue - aValue;
10959
10978
  return direction === "ASC" ? String(aValue).localeCompare(String(bValue)) : String(bValue).localeCompare(String(aValue));
10960
10979
  });
@@ -11157,7 +11176,7 @@ var MemoryPG = class MemoryPG extends MemoryStorage {
11157
11176
  const metadataFilter = validateStorageMetadataFilter(filter?.metadata);
11158
11177
  try {
11159
11178
  const { field, direction } = this.parseOrderBy(orderBy, "ASC");
11160
- const orderByStatement = `ORDER BY "${field}" ${direction}`;
11179
+ const orderByStatement = `ORDER BY "${field}" ${direction}, "id" ${direction}`;
11161
11180
  const selectStatement = `SELECT id, content, role, type, "createdAt", "createdAtZ", thread_id AS "threadId", "resourceId"`;
11162
11181
  const tableName = getTableName$3({
11163
11182
  indexName: TABLE_MESSAGES,
@@ -11312,7 +11331,7 @@ var MemoryPG = class MemoryPG extends MemoryStorage {
11312
11331
  const metadataFilter = validateStorageMetadataFilter(filter?.metadata);
11313
11332
  try {
11314
11333
  const { field, direction } = this.parseOrderBy(orderBy, "ASC");
11315
- const orderByStatement = `ORDER BY "${field}" ${direction}`;
11334
+ const orderByStatement = `ORDER BY "${field}" ${direction}, "id" ${direction}`;
11316
11335
  const selectStatement = `SELECT id, content, role, type, "createdAt", "createdAtZ", thread_id AS "threadId", "resourceId"`;
11317
11336
  const tableName = getTableName$3({
11318
11337
  indexName: TABLE_MESSAGES,
@@ -14807,6 +14826,11 @@ function tableIndexes() {
14807
14826
  columns: "(\"tags\")",
14808
14827
  using: "gin"
14809
14828
  },
14829
+ {
14830
+ name: "mastra_score_events_scoreid_cursor_idx",
14831
+ table: TABLE_SCORE_EVENTS,
14832
+ columns: "(\"scoreId\", \"cursorId\" DESC)"
14833
+ },
14810
14834
  {
14811
14835
  name: "mastra_score_events_cursor_idx",
14812
14836
  table: TABLE_SCORE_EVENTS,
@@ -16939,13 +16963,32 @@ function pushScoreIdentity(acc, scorerId, scoreSource) {
16939
16963
  acc.params.push(scoreSource);
16940
16964
  }
16941
16965
  }
16966
+ function scoreRewriteConflict(row) {
16967
+ return `ON CONFLICT ("scoreId", "timestamp") DO UPDATE SET ${[
16968
+ ...Object.keys(row).filter((column) => column !== "scoreId" && column !== "timestamp").map((column) => `"${column}" = EXCLUDED."${column}"`),
16969
+ "\"cursorId\" = EXCLUDED.\"cursorId\"",
16970
+ "\"xactId\" = EXCLUDED.\"xactId\""
16971
+ ].join(", ")}`;
16972
+ }
16973
+ function collapseExactScoreConflicts(rows) {
16974
+ const records = /* @__PURE__ */ new Map();
16975
+ for (const row of rows) {
16976
+ const timestamp = new Date(row.timestamp).toISOString();
16977
+ const key = `${String(row.scoreId)}\u0000${timestamp}`;
16978
+ records.delete(key);
16979
+ records.set(key, row);
16980
+ }
16981
+ return [...records.values()];
16982
+ }
16942
16983
  async function createScore(client, schema, args) {
16943
- const insert = buildInsert(schema, TABLE_SCORE_EVENTS, [scoreRecordToRow(args.score)]);
16984
+ const row = scoreRecordToRow(args.score);
16985
+ const insert = buildInsert(schema, TABLE_SCORE_EVENTS, [row], scoreRewriteConflict(row));
16944
16986
  if (insert) await client.query(insert.text, insert.values);
16945
16987
  }
16946
16988
  async function batchCreateScores(client, schema, args) {
16947
16989
  if (args.scores.length === 0) return;
16948
- const insert = buildInsert(schema, TABLE_SCORE_EVENTS, args.scores.map(scoreRecordToRow));
16990
+ const rows = collapseExactScoreConflicts(args.scores.map(scoreRecordToRow));
16991
+ const insert = buildInsert(schema, TABLE_SCORE_EVENTS, rows, scoreRewriteConflict(rows[0]));
16949
16992
  if (insert) await client.query(insert.text, insert.values);
16950
16993
  }
16951
16994
  /**
@@ -16968,6 +17011,16 @@ async function deleteScores(client, schema, args) {
16968
17011
  }
16969
17012
  await client.query(`DELETE FROM ${table} WHERE ${conditions.join(" AND ")}`, values);
16970
17013
  }
17014
+ function latestScorePredicate(table, alias = "s") {
17015
+ return `NOT EXISTS (
17016
+ SELECT 1 FROM ${table} newer
17017
+ WHERE newer."scoreId" = ${alias}."scoreId"
17018
+ AND newer."cursorId" > ${alias}."cursorId"
17019
+ )`;
17020
+ }
17021
+ function applyLatestScorePredicate(acc, table) {
17022
+ acc.conditions.push(latestScorePredicate(table));
17023
+ }
16971
17024
  async function listScores(client, schema, args) {
16972
17025
  const { mode, filters, pagination, orderBy, after, limit } = listScoresArgsSchema.parse(args);
16973
17026
  const table = qualifiedTable(schema, TABLE_SCORE_EVENTS);
@@ -16978,28 +17031,50 @@ async function listScores(client, schema, args) {
16978
17031
  return listScoresPage(client, table, filters, pagination.page, pagination.perPage, orderBy.field, orderBy.direction);
16979
17032
  }
16980
17033
  async function getScoreById(client, schema, scoreId) {
17034
+ const table = qualifiedTable(schema, TABLE_SCORE_EVENTS);
16981
17035
  const row = await client.oneOrNone(`SELECT ${SCORE_SELECT_COLUMNS}
16982
- FROM ${qualifiedTable(schema, TABLE_SCORE_EVENTS)}
17036
+ FROM ${table}
16983
17037
  WHERE "scoreId" = $1
16984
- ORDER BY "timestamp" DESC
17038
+ ORDER BY "cursorId" DESC
16985
17039
  LIMIT 1`, [scoreId]);
16986
17040
  return row ? rowToScoreRecord(row) : null;
16987
17041
  }
16988
17042
  async function listScoresPage(client, table, filters, page, perPage, orderField, orderDir) {
16989
- return listSignalPage({
17043
+ const acc = newFilterAccumulator();
17044
+ applyScoreFilters(acc, filters);
17045
+ applyLatestScorePredicate(acc, table);
17046
+ const whereClause = whereOrEmpty(acc);
17047
+ const countRow = await client.oneOrNone(`SELECT COUNT(*)::text AS count FROM ${table} s ${whereClause}`, acc.params);
17048
+ const total = Number(countRow?.count ?? 0);
17049
+ let scores = [];
17050
+ if (total > 0) {
17051
+ const safeOrderField = parseSqlIdentifier(orderField, "order field");
17052
+ scores = (await client.manyOrNone(`SELECT ${SCORE_SELECT_COLUMNS}
17053
+ FROM ${table} s
17054
+ ${whereClause}
17055
+ ORDER BY "${safeOrderField}" ${orderDir}, "cursorId" ${orderDir}
17056
+ LIMIT $${acc.next++} OFFSET $${acc.next++}`, [
17057
+ ...acc.params,
17058
+ perPage,
17059
+ page * perPage
17060
+ ])).map(rowToScoreRecord);
17061
+ }
17062
+ const deltaCursor = deltaPollingFeatureEnabled() ? await readSignalStreamHeadCursor({
16990
17063
  client,
16991
17064
  table,
16992
17065
  filters,
16993
- page,
16994
- perPage,
16995
- orderField,
16996
- orderDir,
16997
- includeDeltaCursor: deltaPollingFeatureEnabled(),
16998
- selectColumns: SCORE_SELECT_COLUMNS,
16999
- responseKey: "scores",
17000
- applyFilters: applyScoreFilters,
17001
- mapRow: rowToScoreRecord
17002
- });
17066
+ applyFilters: applyScoreFilters
17067
+ }) : void 0;
17068
+ return {
17069
+ scores,
17070
+ pagination: {
17071
+ total,
17072
+ page,
17073
+ perPage,
17074
+ hasMore: (page + 1) * perPage < total
17075
+ },
17076
+ ...deltaCursor !== void 0 ? { deltaCursor } : {}
17077
+ };
17003
17078
  }
17004
17079
  async function listScoresDelta(client, table, filters, after, limit) {
17005
17080
  return listSignalDelta({
@@ -17015,12 +17090,14 @@ async function listScoresDelta(client, table, filters, after, limit) {
17015
17090
  });
17016
17091
  }
17017
17092
  async function runScoreAggregateQuery(client, schema, args, filters) {
17093
+ const table = qualifiedTable(schema, TABLE_SCORE_EVENTS);
17018
17094
  const acc = newFilterAccumulator();
17019
17095
  pushScoreIdentity(acc, args.scorerId, args.scoreSource);
17020
17096
  applyScoreFilters(acc, filters);
17097
+ applyLatestScorePredicate(acc, table);
17021
17098
  const sql = `
17022
17099
  SELECT ${aggregationSql(args.aggregation, "\"score\"")} AS "value"
17023
- FROM ${qualifiedTable(schema, TABLE_SCORE_EVENTS)}
17100
+ FROM ${table} s
17024
17101
  ${whereOrEmpty(acc)}
17025
17102
  `;
17026
17103
  const row = await client.oneOrNone(sql, acc.params);
@@ -17052,10 +17129,12 @@ async function getScoreBreakdown(client, schema, args) {
17052
17129
  });
17053
17130
  pushScoreIdentity(acc, args.scorerId, args.scoreSource);
17054
17131
  applyScoreFilters(acc, args.filters);
17132
+ const table = qualifiedTable(schema, TABLE_SCORE_EVENTS);
17133
+ applyLatestScorePredicate(acc, table);
17055
17134
  const sql = `
17056
17135
  SELECT ${resolved.map((e) => e.selectSql).join(", ")},
17057
17136
  ${aggregationSql(args.aggregation, "\"score\"")} AS "value"
17058
- FROM ${qualifiedTable(schema, TABLE_SCORE_EVENTS)}
17137
+ FROM ${table} s
17059
17138
  ${whereOrEmpty(acc)}
17060
17139
  GROUP BY ${resolved.map((e) => e.alias).join(", ")}
17061
17140
  ORDER BY "value" DESC NULLS LAST
@@ -17075,11 +17154,13 @@ async function getScoreTimeSeries(client, schema, args) {
17075
17154
  });
17076
17155
  pushScoreIdentity(acc, args.scorerId, args.scoreSource);
17077
17156
  applyScoreFilters(acc, args.filters);
17157
+ const table = qualifiedTable(schema, TABLE_SCORE_EVENTS);
17158
+ applyLatestScorePredicate(acc, table);
17078
17159
  const sql = `
17079
17160
  SELECT ${bucket} AS bucket,
17080
17161
  ${resolved.map((e) => e.selectSql).join(", ")},
17081
17162
  ${aggregationSql(args.aggregation, "\"score\"")} AS "value"
17082
- FROM ${qualifiedTable(schema, TABLE_SCORE_EVENTS)}
17163
+ FROM ${table} s
17083
17164
  ${whereOrEmpty(acc)}
17084
17165
  GROUP BY bucket, ${resolved.map((e) => e.alias).join(", ")}
17085
17166
  ORDER BY bucket
@@ -17097,10 +17178,12 @@ async function getScoreTimeSeries(client, schema, args) {
17097
17178
  const acc = newFilterAccumulator();
17098
17179
  pushScoreIdentity(acc, args.scorerId, args.scoreSource);
17099
17180
  applyScoreFilters(acc, args.filters);
17181
+ const table = qualifiedTable(schema, TABLE_SCORE_EVENTS);
17182
+ applyLatestScorePredicate(acc, table);
17100
17183
  const sql = `
17101
17184
  SELECT ${bucket} AS bucket,
17102
17185
  ${aggregationSql(args.aggregation, "\"score\"")} AS "value"
17103
- FROM ${qualifiedTable(schema, TABLE_SCORE_EVENTS)}
17186
+ FROM ${table} s
17104
17187
  ${whereOrEmpty(acc)}
17105
17188
  GROUP BY bucket
17106
17189
  ORDER BY bucket
@@ -17120,9 +17203,11 @@ async function getScorePercentiles(client, schema, args) {
17120
17203
  const acc = newFilterAccumulator();
17121
17204
  pushScoreIdentity(acc, args.scorerId, args.scoreSource);
17122
17205
  applyScoreFilters(acc, args.filters);
17206
+ const table = qualifiedTable(schema, TABLE_SCORE_EVENTS);
17207
+ applyLatestScorePredicate(acc, table);
17123
17208
  const sql = `
17124
17209
  SELECT ${bucket} AS bucket, ${percentileSelectSql(args.percentiles, "\"score\"")}
17125
- FROM ${qualifiedTable(schema, TABLE_SCORE_EVENTS)}
17210
+ FROM ${table} s
17126
17211
  ${whereOrEmpty(acc)}
17127
17212
  GROUP BY bucket
17128
17213
  ORDER BY bucket
@@ -17321,13 +17406,6 @@ function latestSpanPredicate$1(spanTable) {
17321
17406
  AND (newer."isPending" < s."isPending" OR (newer."isPending" = s."isPending" AND newer."cursorId" > s."cursorId"))
17322
17407
  )`;
17323
17408
  }
17324
- function latestScorePredicate(scoreTable) {
17325
- return `NOT EXISTS (
17326
- SELECT 1 FROM ${scoreTable} newer
17327
- WHERE newer."scoreId" = s."scoreId"
17328
- AND newer."cursorId" > s."cursorId"
17329
- )`;
17330
- }
17331
17409
  function latestFeedbackPredicate(feedbackTable) {
17332
17410
  return `NOT EXISTS (
17333
17411
  SELECT 1 FROM ${feedbackTable} newer
@@ -17497,7 +17575,7 @@ function compilePostgresTraceScope(schema, selection, relationCollections) {
17497
17575
  values
17498
17576
  };
17499
17577
  }
17500
- function compilePostgresTraceQuery(schema, plan) {
17578
+ function compilePostgresTraceQuery(schema, plan, mode = "data") {
17501
17579
  const { ctes, values } = compilePostgresTraceScope(schema, plan, collectRelationCollections(plan.where));
17502
17580
  let predicateSql = "TRUE";
17503
17581
  if (plan.where) {
@@ -17511,6 +17589,12 @@ function compilePostgresTraceQuery(schema, plan) {
17511
17589
  WHERE ${predicateSql}
17512
17590
  )`);
17513
17591
  const candidates = `WITH ${ctes.join(",\n")}`;
17592
+ if (plan.paginationMode === "page" && mode === "count") return {
17593
+ text: `${candidates}
17594
+ SELECT COUNT(*)::text AS count
17595
+ FROM candidates`,
17596
+ values
17597
+ };
17514
17598
  if (plan.result === "groups") {
17515
17599
  const pageCondition = plan.cursor ? `AND "threadId" > $${values.length + 1}` : "";
17516
17600
  if (plan.cursor) values.push(plan.cursor.threadId);
@@ -17528,6 +17612,17 @@ LIMIT $${values.length}`,
17528
17612
  }
17529
17613
  const orderField = plan.orderBy.field === "startedAt" ? "\"startedAt\"" : "\"endedAt\"";
17530
17614
  const direction = plan.orderBy.direction === "asc" ? "ASC" : "DESC";
17615
+ if (plan.paginationMode === "page") {
17616
+ values.push(plan.perPage, plan.page * plan.perPage);
17617
+ return {
17618
+ text: `${candidates}
17619
+ SELECT *
17620
+ FROM candidates
17621
+ ORDER BY ${orderField} ${direction}, "traceId" ASC
17622
+ LIMIT $${values.length - 1} OFFSET $${values.length}`,
17623
+ values
17624
+ };
17625
+ }
17531
17626
  let pageCondition = "";
17532
17627
  if (plan.cursor) {
17533
17628
  const comparison = plan.orderBy.direction === "asc" ? ">" : "<";
@@ -17674,10 +17769,11 @@ function isPostgresResourceLimit(error) {
17674
17769
  const candidate = error;
17675
17770
  return candidate.code === "53200" || candidate.code === "53400";
17676
17771
  }
17677
- async function runWithPostgresTraceQueryTimeout(client, timeoutMs, execute) {
17772
+ async function runWithPostgresTraceQueryTimeout(client, timeoutMs, execute, options = {}) {
17678
17773
  const resolvedTimeoutMs = coreStorage.resolveTraceQueryTimeoutMs(timeoutMs);
17679
17774
  try {
17680
17775
  return await client.tx(async (transaction) => {
17776
+ if (options.repeatableRead) await transaction.query("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ");
17681
17777
  await transaction.query(`SELECT set_config('statement_timeout', $1, true)`, [`${resolvedTimeoutMs}ms`]);
17682
17778
  return execute(transaction);
17683
17779
  });
@@ -17711,6 +17807,50 @@ async function getTraceQueryValues(client, schema, plan, timeoutMs) {
17711
17807
  });
17712
17808
  }
17713
17809
  async function queryTraces(client, schema, plan, timeoutMs) {
17810
+ if (plan.paginationMode === "page") {
17811
+ const resolvedTimeoutMs = coreStorage.resolveTraceQueryTimeoutMs(timeoutMs);
17812
+ const deadline = performance.now() + resolvedTimeoutMs;
17813
+ const countQuery = compilePostgresTraceQuery(schema, plan, "count");
17814
+ const dataQuery = compilePostgresTraceQuery(schema, plan);
17815
+ const { total, rows } = await runWithPostgresTraceQueryTimeout(client, resolvedTimeoutMs, async (transaction) => {
17816
+ const countRows = await transaction.any(countQuery.text, countQuery.values);
17817
+ const remainingTimeoutMs = Math.floor(deadline - performance.now());
17818
+ if (remainingTimeoutMs <= 0) throw new coreStorage.TraceQueryExecutionError();
17819
+ await transaction.query(`SELECT set_config('statement_timeout', $1, true)`, [`${remainingTimeoutMs}ms`]);
17820
+ const rows = await transaction.any(dataQuery.text, dataQuery.values);
17821
+ return {
17822
+ total: Number(countRows[0]?.count ?? 0),
17823
+ rows
17824
+ };
17825
+ }, { repeatableRead: true });
17826
+ const traces = rows.map((row) => ({
17827
+ traceId: String(row.traceId),
17828
+ rootSpanId: String(row.rootSpanId),
17829
+ name: row.name,
17830
+ entityId: row.entityId ?? null,
17831
+ parentSpanId: row.parentSpanId ?? null,
17832
+ createdAt: asIsoTimestamp$1(row.startedAt),
17833
+ metadata: row.metadata ?? null,
17834
+ inputPreview: coreStorage.buildInputPreview(row.input) ?? null,
17835
+ threadId: row.threadId == null ? null : String(row.threadId),
17836
+ resourceId: row.resourceId == null ? null : String(row.resourceId),
17837
+ startedAt: asIsoTimestamp$1(row.startedAt),
17838
+ endedAt: asIsoTimestamp$1(row.endedAt),
17839
+ entityName: row.entityName == null ? null : String(row.entityName),
17840
+ entityType: row.entityType == null ? null : String(row.entityType),
17841
+ environment: row.environment == null ? null : String(row.environment),
17842
+ status: row.status
17843
+ }));
17844
+ return coreStorage.traceQueryResponseSchema.parse({
17845
+ traces,
17846
+ pagination: {
17847
+ total,
17848
+ page: plan.page,
17849
+ perPage: plan.perPage,
17850
+ hasMore: (plan.page + 1) * plan.perPage < total
17851
+ }
17852
+ });
17853
+ }
17714
17854
  const query = compilePostgresTraceQuery(schema, plan);
17715
17855
  const rows = await runWithPostgresTraceQueryTimeout(client, timeoutMs, (transaction) => transaction.any(query.text, query.values));
17716
17856
  const visibleRows = rows.slice(0, plan.limit);
@@ -22171,6 +22311,7 @@ const WORKFLOW_SNAPSHOT_STATUS_INDEX = "mastra_workflow_snapshot_name_status_cre
22171
22311
  * Schema-prefixed name of the status index, lowercased and truncated the same way Postgres
22172
22312
  * stores it, so the init snapshot's index set answers "does it exist?" without a probe or a
22173
22313
  * no-op `CREATE INDEX` (schema-prefixed names routinely exceed the 63-byte limit).
22314
+ * Exported for tests.
22174
22315
  */
22175
22316
  function workflowSnapshotStatusIndexName(schemaName) {
22176
22317
  return buildConstraintName({
@@ -22188,6 +22329,48 @@ function workflowSnapshotStatusIndexSQL(indexName, schemaName) {
22188
22329
  schemaName: getSchemaName(schemaName)
22189
22330
  })} (workflow_name, (snapshot ->> 'status'), "createdAt" DESC)`;
22190
22331
  }
22332
+ /** Base name (before any schema prefix) of the expression index backing the threadId filter. */
22333
+ const WORKFLOW_SNAPSHOT_THREAD_ID_INDEX = "mastra_workflow_snapshot_threadid_idx";
22334
+ /**
22335
+ * Schema-prefixed name of the threadId index (see workflowSnapshotStatusIndexName).
22336
+ *
22337
+ * Unlike the status index, truncation appends a collision hash: both index names share the
22338
+ * long `<schema>_mastra_workflow_snapshot_` prefix, so with a schema name of 37+ bytes plain
22339
+ * truncation collapses them to the same 63-byte identifier and `CREATE INDEX IF NOT EXISTS`
22340
+ * silently skips this index. The status index keeps plain truncation because its truncated
22341
+ * name already exists in deployed catalogs; this index is new and free to adopt the rule.
22342
+ * Exported for tests.
22343
+ */
22344
+ function workflowSnapshotThreadIdIndexName(schemaName) {
22345
+ return buildConstraintName({
22346
+ baseName: WORKFLOW_SNAPSHOT_THREAD_ID_INDEX,
22347
+ schemaName: schemaName && schemaName !== "public" ? schemaName : void 0,
22348
+ hashWhenTruncated: true
22349
+ });
22350
+ }
22351
+ /**
22352
+ * Expression extracting the thread id embedded in a snapshot (jsonb columns only). Mirrors
22353
+ * the canonical extraction in `@mastra/core` (`getSnapshotMemoryInfo`), which reads one of
22354
+ * two layouts:
22355
+ * 1. agentic-loop: `context.<suspended step>.suspendPayload.__streamState.messageList.memoryInfo.threadId`
22356
+ * 2. durable loop: `context.input.messageListState.memoryInfo.threadId`
22357
+ *
22358
+ * `jsonb_path_query_first(jsonb, jsonpath)` is IMMUTABLE, so the expression is valid in an
22359
+ * expression index. The WHERE clause in listWorkflowRuns() must use this exact expression
22360
+ * text so the planner can match it against the index. If the snapshot layout changes in
22361
+ * core, this expression must be updated in lockstep or it will wrongly exclude rows.
22362
+ */
22363
+ 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}')`;
22364
+ /**
22365
+ * Expression index on the snapshot-embedded thread id so listWorkflowRuns() threadId filters
22366
+ * (Agent.listSuspendedRuns) can use an index instead of detoasting every snapshot.
22367
+ */
22368
+ function workflowSnapshotThreadIdIndexSQL(indexName, schemaName) {
22369
+ return `CREATE INDEX IF NOT EXISTS "${indexName}" ON ${getTableName({
22370
+ indexName: TABLE_WORKFLOW_SNAPSHOT,
22371
+ schemaName: getSchemaName(schemaName)
22372
+ })} ((${WORKFLOW_SNAPSHOT_THREAD_ID_EXPR}))`;
22373
+ }
22191
22374
  var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
22192
22375
  #db;
22193
22376
  #schema;
@@ -22260,6 +22443,7 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
22260
22443
  }));
22261
22444
  for (const idx of WorkflowsPG.getDefaultIndexDefs(schemaPrefix)) statements.push(generateIndexSQL(idx, schemaName));
22262
22445
  statements.push(`${workflowSnapshotStatusIndexSQL(workflowSnapshotStatusIndexName(parsedSchema), schemaName)};`);
22446
+ statements.push(`${workflowSnapshotThreadIdIndexSQL(workflowSnapshotThreadIdIndexName(parsedSchema), schemaName)};`);
22263
22447
  return statements;
22264
22448
  }
22265
22449
  /**
@@ -22286,6 +22470,12 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
22286
22470
  } catch (error) {
22287
22471
  this.logger?.warn?.(`Failed to create index ${indexName}:`, error);
22288
22472
  }
22473
+ const threadIdIndexName = workflowSnapshotThreadIdIndexName(this.#schema);
22474
+ try {
22475
+ await this.#db.createIndexFromStatement(threadIdIndexName, workflowSnapshotThreadIdIndexSQL(threadIdIndexName, this.#schema));
22476
+ } catch (error) {
22477
+ this.logger?.warn?.(`Failed to create index ${threadIdIndexName}:`, error);
22478
+ }
22289
22479
  }
22290
22480
  async init() {
22291
22481
  await this.#db.createTable({
@@ -22566,7 +22756,7 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
22566
22756
  }, error);
22567
22757
  }
22568
22758
  }
22569
- async listWorkflowRuns({ workflowName, fromDate, toDate, perPage, page, resourceId, status } = {}) {
22759
+ async listWorkflowRuns({ workflowName, fromDate, toDate, perPage, page, resourceId, threadId, status } = {}) {
22570
22760
  try {
22571
22761
  const conditions = [];
22572
22762
  const values = [];
@@ -22587,6 +22777,11 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
22587
22777
  values.push(resourceId);
22588
22778
  paramIndex++;
22589
22779
  } else this.logger?.warn?.(`[${TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
22780
+ if (threadId) if (await this.#db.getColumnType(TABLE_WORKFLOW_SNAPSHOT, "snapshot") === "jsonb") {
22781
+ conditions.push(`${WORKFLOW_SNAPSHOT_THREAD_ID_EXPR} = $${paramIndex}`);
22782
+ values.push(threadId);
22783
+ paramIndex++;
22784
+ } else this.logger?.warn?.(`[${TABLE_WORKFLOW_SNAPSHOT}] snapshot column is not jsonb. Skipping threadId filter.`);
22590
22785
  if (fromDate) {
22591
22786
  conditions.push(`"createdAt" >= $${paramIndex}`);
22592
22787
  values.push(fromDate);