@mastra/pg 1.35.0 → 1.36.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 (30) hide show
  1. package/dist/docs/SKILL.md +2 -13
  2. package/dist/docs/assets/SOURCE_MAP.json +158 -2
  3. package/dist/docs/references/integrations-databases-neon.md +37 -1
  4. package/dist/docs/references/integrations-databases-postgresql.md +46 -0
  5. package/dist/docs/references/reference-processors-message-history-processor.md +1 -1
  6. package/dist/docs/references/reference-processors-semantic-recall-processor.md +1 -1
  7. package/dist/docs/references/reference-tools-vector-query-tool.md +3 -3
  8. package/dist/index.cjs +797 -64
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.js +798 -65
  11. package/dist/index.js.map +1 -1
  12. package/dist/storage/db/index.d.ts +3 -3
  13. package/dist/storage/db/index.d.ts.map +1 -1
  14. package/dist/storage/domains/observability/v-next/feedback.d.ts +1 -0
  15. package/dist/storage/domains/observability/v-next/feedback.d.ts.map +1 -1
  16. package/dist/storage/domains/observability/v-next/index.d.ts +1 -0
  17. package/dist/storage/domains/observability/v-next/index.d.ts.map +1 -1
  18. package/dist/storage/domains/workflows/index.d.ts +87 -7
  19. package/dist/storage/domains/workflows/index.d.ts.map +1 -1
  20. package/dist/storage/retention.d.ts +3 -1
  21. package/dist/storage/retention.d.ts.map +1 -1
  22. package/package.json +3 -3
  23. package/dist/docs/references/docs-deployment-workers.md +0 -404
  24. package/dist/docs/references/docs-memory-semantic-recall.md +0 -485
  25. package/dist/docs/references/docs-memory-working-memory.md +0 -450
  26. package/dist/docs/references/docs-storage.md +0 -224
  27. package/dist/docs/references/reference-rag-retrieval.md +0 -672
  28. package/dist/docs/references/reference-rag-vector-databases.md +0 -808
  29. package/dist/docs/references/reference-storage-composite.md +0 -433
  30. package/dist/docs/references/reference-storage-retention.md +0 -310
package/dist/index.cjs CHANGED
@@ -5242,13 +5242,13 @@ function cutoffFor(policy, anchorType, now = Date.now()) {
5242
5242
  return anchorType === "epoch-ms" ? cutoffMs : new Date(cutoffMs);
5243
5243
  }
5244
5244
  const runBatchedDelete = _mastra_core_storage.runRetentionBatches;
5245
- function runPrune({ db, domain, targets, options }) {
5245
+ function runPrune({ db, domain, targets, options, deleteBatch }) {
5246
5246
  return (0, _mastra_core_storage.executeRetentionPrune)({
5247
5247
  domain,
5248
5248
  targets,
5249
5249
  options,
5250
5250
  cutoffFor: (target, now) => cutoffFor(target.policy, target.anchorType ?? "timestamp", now),
5251
- deleteBatch: (target, cutoff, limit) => db.pruneBatch({
5251
+ deleteBatch: (target, cutoff, limit) => deleteBatch?.(target, cutoff, limit) ?? db.pruneBatch({
5252
5252
  tableName: target.table,
5253
5253
  column: target.column,
5254
5254
  cutoff,
@@ -18337,6 +18337,11 @@ async function batchCreateFeedback(client, schema, args, partitioning) {
18337
18337
  const rows = [...new Map(args.feedbacks.map((feedback) => [feedback.feedbackId, feedback])).values()].map(feedbackRecordToRow);
18338
18338
  await insertRows(client, schema, TABLE_FEEDBACK_EVENTS, rows, partitioning, FEEDBACK_UPSERT_CLAUSE);
18339
18339
  }
18340
+ async function getFeedbackById(client, schema, feedbackId) {
18341
+ const row = await client.oneOrNone(`SELECT ${FEEDBACK_SELECT_COLUMNS} FROM ${qualifiedTable(schema, TABLE_FEEDBACK_EVENTS)}
18342
+ WHERE "feedbackId" = $1 ORDER BY "cursorId" DESC LIMIT 1`, [feedbackId]);
18343
+ return row ? rowToFeedbackRecord(row) : null;
18344
+ }
18340
18345
  async function updateFeedbackReviewStatus(client, schema, args) {
18341
18346
  const { feedbackId, reviewStatus } = parseUpdateFeedbackReviewStatusArgs(args);
18342
18347
  const row = await client.oneOrNone(`UPDATE ${qualifiedTable(schema, TABLE_FEEDBACK_EVENTS)}
@@ -22172,6 +22177,9 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
22172
22177
  async getScoreById(scoreId) {
22173
22178
  return this.#run("GET_SCORE_BY_ID", () => getScoreById(this.#readClient, this.#schema, scoreId), { scoreId });
22174
22179
  }
22180
+ async getFeedbackById(feedbackId) {
22181
+ return this.#run("GET_FEEDBACK_BY_ID", () => getFeedbackById(this.#readClient, this.#schema, feedbackId), { feedbackId });
22182
+ }
22175
22183
  async listFeedback(args) {
22176
22184
  return this.#run("LIST_FEEDBACK", () => listFeedback(this.#readClient, this.#schema, args));
22177
22185
  }
@@ -25625,6 +25633,16 @@ function workflowSnapshotStatusIndexSQL(indexName, schemaName, concurrently = fa
25625
25633
  });
25626
25634
  return `CREATE INDEX ${concurrently ? "CONCURRENTLY " : ""}IF NOT EXISTS "${indexName}" ON ${tableName} (workflow_name, (snapshot ->> 'status'), "createdAt" DESC)`;
25627
25635
  }
25636
+ /**
25637
+ * Storage v2 index names share the long `<schema>_mastra_workflow_runs_` prefix, so a long
25638
+ * schema name would collapse them to one 63-byte identifier under plain truncation; hash instead.
25639
+ */
25640
+ function runIndexName(schemaPrefix, baseName) {
25641
+ return buildConstraintName({
25642
+ baseName: `${schemaPrefix}${baseName}`,
25643
+ hashWhenTruncated: true
25644
+ });
25645
+ }
25628
25646
  /** Base name (before any schema prefix) of the expression index backing the threadId filter. */
25629
25647
  const WORKFLOW_SNAPSHOT_THREAD_ID_INDEX = "mastra_workflow_snapshot_threadid_idx";
25630
25648
  /**
@@ -25669,13 +25687,126 @@ function workflowSnapshotThreadIdIndexSQL(indexName, schemaName, concurrently =
25669
25687
  });
25670
25688
  return `CREATE INDEX ${concurrently ? "CONCURRENTLY " : ""}IF NOT EXISTS "${indexName}" ON ${tableName} ((${WORKFLOW_SNAPSHOT_THREAD_ID_EXPR}))`;
25671
25689
  }
25690
+ /** Base name (before any schema prefix) of the index finding the `state-base` entries built on a run. */
25691
+ const WORKFLOW_RUN_ENTRIES_STATE_BASE_INDEX = "mastra_workflow_run_entries_state_base_idx";
25692
+ /** The run a `state-base` entry builds on; `#detachStateChildren` must use these exact expressions. */
25693
+ const STATE_BASE_RUN_EXPR = `(payload ->> 'workflowName'), (payload ->> 'runId')`;
25694
+ /**
25695
+ * Partial expression index finding the `state-base` entries built on a run, so deleting a run whose
25696
+ * state others build on doesn't scan every entry. See {@link workflowSnapshotStatusIndexSQL} for
25697
+ * `concurrently`.
25698
+ */
25699
+ function stateBaseIndexSQL(indexName, schemaName, concurrently = false) {
25700
+ const tableName = getTableName({
25701
+ indexName: _mastra_core_storage.TABLE_WORKFLOW_RUN_ENTRIES,
25702
+ schemaName: getSchemaName(schemaName)
25703
+ });
25704
+ return `CREATE INDEX ${concurrently ? "CONCURRENTLY " : ""}IF NOT EXISTS "${indexName}" ON ${tableName} (${STATE_BASE_RUN_EXPR}) WHERE kind = 'state-base'`;
25705
+ }
25706
+ /**
25707
+ * The entries a storage v2 write adds: one per step, in order, then one per step patch, then the
25708
+ * state or state base (a checkpoint with neither starts the state over at `{}`), then the state
25709
+ * patch unless a full state was given.
25710
+ */
25711
+ function journalEntries(steps, { state, stateBase, statePatch }, checkpoint, stepPatches = {}) {
25712
+ const entries = Object.entries(steps).map(([stepId, result]) => ({
25713
+ kind: "step",
25714
+ step_id: stepId,
25715
+ payload: result
25716
+ }));
25717
+ for (const [stepId, ops] of Object.entries(stepPatches)) entries.push({
25718
+ kind: "step-patch",
25719
+ step_id: stepId,
25720
+ payload: ops
25721
+ });
25722
+ if (state !== void 0) entries.push({
25723
+ kind: "state",
25724
+ step_id: null,
25725
+ payload: state
25726
+ });
25727
+ else if (stateBase !== void 0) entries.push({
25728
+ kind: "state-base",
25729
+ step_id: null,
25730
+ payload: stateBase
25731
+ });
25732
+ else if (checkpoint) entries.push({
25733
+ kind: "state",
25734
+ step_id: null,
25735
+ payload: {}
25736
+ });
25737
+ if (state === void 0 && statePatch !== void 0) entries.push({
25738
+ kind: "state-patch",
25739
+ step_id: null,
25740
+ payload: statePatch
25741
+ });
25742
+ return entries;
25743
+ }
25744
+ /**
25745
+ * Rows `e(elem, ord)` of the jsonb array parameter `param` of {@link EntryInput}, and the
25746
+ * entry columns they insert. `elem -> 'payload'` keeps a JSON null apart from an undefined
25747
+ * payload (dropped by `JSON.stringify`), which becomes SQL NULL.
25748
+ */
25749
+ const entryRows = (param) => `jsonb_array_elements(${param}::jsonb) WITH ORDINALITY AS e(elem, ord)`;
25750
+ const ENTRY_VALUES = `e.elem ->> 'kind', e.elem ->> 'step_id', e.elem -> 'payload'`;
25751
+ /** The entries of {@link EntryRow} rows; an undefined payload reads back as undefined, as in v1. */
25752
+ function toEntries(rows) {
25753
+ return rows.map((row) => {
25754
+ const payload = row.absent ? void 0 : row.payload;
25755
+ switch (row.kind) {
25756
+ case "step": return {
25757
+ seq: row.seq,
25758
+ kind: "step",
25759
+ stepId: row.step_id,
25760
+ result: payload
25761
+ };
25762
+ case "step-patch": return {
25763
+ seq: row.seq,
25764
+ kind: "step-patch",
25765
+ stepId: row.step_id,
25766
+ ops: payload
25767
+ };
25768
+ case "state": return {
25769
+ seq: row.seq,
25770
+ kind: "state",
25771
+ state: payload
25772
+ };
25773
+ case "state-patch": return {
25774
+ seq: row.seq,
25775
+ kind: "state-patch",
25776
+ ops: payload
25777
+ };
25778
+ case "state-base": return {
25779
+ seq: row.seq,
25780
+ kind: "state-base",
25781
+ base: payload
25782
+ };
25783
+ case "state-draft": {
25784
+ const draft = payload;
25785
+ return "state" in draft ? {
25786
+ seq: row.seq,
25787
+ kind: "state-draft",
25788
+ state: draft.state
25789
+ } : {
25790
+ seq: row.seq,
25791
+ kind: "state-draft",
25792
+ on: draft.on,
25793
+ ops: draft.ops
25794
+ };
25795
+ }
25796
+ }
25797
+ });
25798
+ }
25672
25799
  var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorage {
25673
25800
  #db;
25674
25801
  #schema;
25675
25802
  #skipDefaultIndexes;
25676
25803
  #indexes;
25677
25804
  /** Tables managed by this domain */
25678
- static MANAGED_TABLES = [_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT];
25805
+ static MANAGED_TABLES = [
25806
+ _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
25807
+ _mastra_core_storage.TABLE_WORKFLOW_RUNS,
25808
+ _mastra_core_storage.TABLE_WORKFLOW_RUN_ENTRIES
25809
+ ];
25679
25810
  /**
25680
25811
  * Workflow run snapshots accumulate as runs execute. Anchored on the
25681
25812
  * timezone-aware `updatedAtZ` mirror column (last activity) so suspended or
@@ -25738,11 +25869,43 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
25738
25869
  };
25739
25870
  }
25740
25871
  static getDefaultIndexDefs(schemaPrefix) {
25741
- return [{
25742
- name: `${schemaPrefix}mastra_workflow_snapshot_name_createdat_idx`,
25743
- table: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
25744
- columns: ["workflow_name", "createdAt DESC"]
25745
- }];
25872
+ return [
25873
+ {
25874
+ name: `${schemaPrefix}mastra_workflow_snapshot_name_createdat_idx`,
25875
+ table: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
25876
+ columns: ["workflow_name", "createdAt DESC"]
25877
+ },
25878
+ {
25879
+ name: runIndexName(schemaPrefix, "mastra_workflow_runs_name_createdat_idx"),
25880
+ table: _mastra_core_storage.TABLE_WORKFLOW_RUNS,
25881
+ columns: ["workflow_name", "createdAt DESC"]
25882
+ },
25883
+ {
25884
+ name: runIndexName(schemaPrefix, "mastra_workflow_runs_name_status_createdat_idx"),
25885
+ table: _mastra_core_storage.TABLE_WORKFLOW_RUNS,
25886
+ columns: [
25887
+ "workflow_name",
25888
+ "status",
25889
+ "createdAt DESC"
25890
+ ]
25891
+ },
25892
+ {
25893
+ name: runIndexName(schemaPrefix, "mastra_workflow_runs_threadid_idx"),
25894
+ table: _mastra_core_storage.TABLE_WORKFLOW_RUNS,
25895
+ columns: ["threadId"]
25896
+ },
25897
+ {
25898
+ name: runIndexName(schemaPrefix, "mastra_workflow_run_entries_step_idx"),
25899
+ table: _mastra_core_storage.TABLE_WORKFLOW_RUN_ENTRIES,
25900
+ columns: [
25901
+ "workflow_name",
25902
+ "run_id",
25903
+ "kind",
25904
+ "step_id",
25905
+ "seq"
25906
+ ]
25907
+ }
25908
+ ];
25746
25909
  }
25747
25910
  /**
25748
25911
  * Returns all DDL statements for this domain: table with unique constraint.
@@ -25758,9 +25921,17 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
25758
25921
  schemaName,
25759
25922
  includeAllConstraints: true
25760
25923
  }));
25924
+ for (const [tableName, config] of Object.entries(_mastra_core_storage.WORKFLOW_RUN_JOURNAL_TABLE_CONFIGS)) statements.push(generateTableSQL({
25925
+ tableName,
25926
+ schema: config.columns,
25927
+ schemaName,
25928
+ compositePrimaryKey: config.compositePrimaryKey,
25929
+ includeAllConstraints: true
25930
+ }));
25761
25931
  for (const idx of WorkflowsPG.getDefaultIndexDefs(schemaPrefix)) statements.push(generateIndexSQL(idx, schemaName));
25762
25932
  statements.push(`${workflowSnapshotStatusIndexSQL(workflowSnapshotStatusIndexName(parsedSchema), schemaName)};`);
25763
25933
  statements.push(`${workflowSnapshotThreadIdIndexSQL(workflowSnapshotThreadIdIndexName(parsedSchema), schemaName)};`);
25934
+ statements.push(`${stateBaseIndexSQL(runIndexName(schemaPrefix, WORKFLOW_RUN_ENTRIES_STATE_BASE_INDEX), schemaName)};`);
25764
25935
  statements.push(generateTableSQL({
25765
25936
  tableName: _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS,
25766
25937
  schema: _mastra_core_storage.RUN_FENCING_TABLE_SCHEMAS[_mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS],
@@ -25786,6 +25957,12 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
25786
25957
  } catch (error) {
25787
25958
  this.logger?.warn?.(`Failed to create index ${indexDef.name}:`, error);
25788
25959
  }
25960
+ const stateBaseIndexName = runIndexName(this.#schema !== "public" ? `${schemaNamePrefix(this.#schema)}_` : "", WORKFLOW_RUN_ENTRIES_STATE_BASE_INDEX);
25961
+ try {
25962
+ await this.#db.createIndexFromStatement(stateBaseIndexName, stateBaseIndexSQL(stateBaseIndexName, this.#schema, true));
25963
+ } catch (error) {
25964
+ this.logger?.warn?.(`Failed to create index ${stateBaseIndexName}:`, error);
25965
+ }
25789
25966
  if (await this.#db.getColumnType(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT, "snapshot") !== "jsonb") return;
25790
25967
  const indexName = workflowSnapshotStatusIndexName(this.#schema);
25791
25968
  try {
@@ -25814,6 +25991,11 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
25814
25991
  tableName: _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS,
25815
25992
  schema: _mastra_core_storage.RUN_FENCING_TABLE_SCHEMAS[_mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS]
25816
25993
  });
25994
+ for (const [tableName, config] of Object.entries(_mastra_core_storage.WORKFLOW_RUN_JOURNAL_TABLE_CONFIGS)) await this.#db.createTable({
25995
+ tableName,
25996
+ schema: config.columns,
25997
+ compositePrimaryKey: config.compositePrimaryKey
25998
+ });
25817
25999
  await this.createDefaultIndexes();
25818
26000
  await this.createCustomIndexes();
25819
26001
  }
@@ -25840,8 +26022,22 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
25840
26022
  this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
25841
26023
  }
25842
26024
  }
26025
+ if (!policies.workflowSnapshot) return;
26026
+ try {
26027
+ await this.#db.ensureIndex({
26028
+ indexName: `${prefix}mastra_workflow_runs_retention_idx`,
26029
+ tableName: _mastra_core_storage.TABLE_WORKFLOW_RUNS,
26030
+ column: "updatedAtZ"
26031
+ });
26032
+ } catch (error) {
26033
+ this.logger?.warn?.(`Failed to create retention index for ${_mastra_core_storage.TABLE_WORKFLOW_RUNS}:`, error);
26034
+ }
25843
26035
  }
25844
- /** Delete workflow run snapshots and run ownership rows older than their policy's `maxAge`, batched. */
26036
+ /**
26037
+ * Delete workflow runs and run ownership rows older than their policy's `maxAge`, batched.
26038
+ * The `workflowSnapshot` policy covers both storage versions: storage v2 runs are pruned by
26039
+ * their header's last write, together with their entries.
26040
+ */
25845
26041
  async prune(policies, options) {
25846
26042
  await this.ensureRetentionIndexes(policies);
25847
26043
  const targets = resolveTargets({
@@ -25849,11 +26045,40 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
25849
26045
  descriptor: WorkflowsPG.retentionTables,
25850
26046
  order: ["workflowSnapshot", "runOwnership"]
25851
26047
  });
26048
+ const snapshotTarget = targets.findIndex((target) => target.table === _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT);
26049
+ if (snapshotTarget !== -1) targets.splice(snapshotTarget + 1, 0, {
26050
+ ...targets[snapshotTarget],
26051
+ table: _mastra_core_storage.TABLE_WORKFLOW_RUNS
26052
+ });
25852
26053
  return runPrune({
25853
26054
  db: this.#db,
25854
26055
  domain: "workflows",
25855
26056
  targets,
25856
- options
26057
+ options,
26058
+ deleteBatch: (target, cutoff, limit) => target.table === _mastra_core_storage.TABLE_WORKFLOW_RUNS ? this.#pruneRunsBatch(cutoff, limit) : void 0
26059
+ });
26060
+ }
26061
+ /**
26062
+ * Deletes up to `limit` aged storage v2 runs, header and entries together, first detaching the
26063
+ * runs whose state others build on (see `#detachStateChildren`).
26064
+ */
26065
+ async #pruneRunsBatch(cutoff, limit) {
26066
+ return this.#db.client.tx(async (t) => {
26067
+ const doomed = await t.manyOrNone(`SELECT workflow_name, run_id, "stateShared" FROM ${this.#runsTable()}
26068
+ WHERE "updatedAtZ" < $1
26069
+ ORDER BY "updatedAtZ"
26070
+ LIMIT $2
26071
+ FOR UPDATE SKIP LOCKED`, [cutoff, limit]);
26072
+ if (!doomed.length) return 0;
26073
+ for (const run of doomed) if (run.stateShared) await this.#detachStateChildren(t, run.workflow_name, run.run_id);
26074
+ return (await t.query(`WITH doomed AS (
26075
+ SELECT * FROM unnest($1::text[], $2::text[]) AS d(workflow_name, run_id)
26076
+ ), entries AS (
26077
+ DELETE FROM ${this.#entriesTable()} e USING doomed d
26078
+ WHERE e.workflow_name = d.workflow_name AND e.run_id = d.run_id
26079
+ )
26080
+ DELETE FROM ${this.#runsTable()} r USING doomed d
26081
+ WHERE r.workflow_name = d.workflow_name AND r.run_id = d.run_id`, [doomed.map((run) => run.workflow_name), doomed.map((run) => run.run_id)])).rowCount ?? 0;
25857
26082
  });
25858
26083
  }
25859
26084
  /**
@@ -25870,6 +26095,459 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
25870
26095
  async dangerouslyClearAll() {
25871
26096
  await this.#db.clearTable({ tableName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT });
25872
26097
  await this.#db.clearTable({ tableName: _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS });
26098
+ await this.#db.clearTable({ tableName: _mastra_core_storage.TABLE_WORKFLOW_RUNS });
26099
+ await this.#db.clearTable({ tableName: _mastra_core_storage.TABLE_WORKFLOW_RUN_ENTRIES });
26100
+ }
26101
+ supportsRunJournal() {
26102
+ return true;
26103
+ }
26104
+ #runsTable() {
26105
+ return getTableName({
26106
+ indexName: _mastra_core_storage.TABLE_WORKFLOW_RUNS,
26107
+ schemaName: getSchemaName(this.#schema)
26108
+ });
26109
+ }
26110
+ #entriesTable() {
26111
+ return getTableName({
26112
+ indexName: _mastra_core_storage.TABLE_WORKFLOW_RUN_ENTRIES,
26113
+ schemaName: getSchemaName(this.#schema)
26114
+ });
26115
+ }
26116
+ /**
26117
+ * A jsonb array of {@link EntryRow} for the storage v2 run `alias` refers to: each step's
26118
+ * latest entry, ordered by when each step was first written so `context` keys keep their write
26119
+ * order, the step patches after it (in `seq` order, for `latestStepResults`), and the entries
26120
+ * `materializeState` uses for the latest state: the latest full state or state base and every
26121
+ * state patch after it, in `seq` order. Drafts are left out.
26122
+ *
26123
+ * With `summaryFieldsParam` (the parameter holding `STEP_RESULT_SUMMARY_FIELDS`), successful
26124
+ * steps other than `input` come back as their summary, built here so their data is never sent,
26125
+ * unless step patches follow it (the caller summarizes the patched result instead).
26126
+ * Like `summarizeStepResult`, it keeps a field that is JSON null and leaves out one that is absent.
26127
+ */
26128
+ #entriesSql(alias, { summaryFieldsParam } = {}) {
26129
+ let payload = "x.payload";
26130
+ if (summaryFieldsParam) {
26131
+ const summarized = `(x.kind = 'step' AND x.step_id <> 'input' AND x.payload ->> 'status' = 'success'
26132
+ AND (x.patch_seq IS NULL OR x.patch_seq < x.seq))`;
26133
+ payload = `CASE WHEN ${summarized} THEN (
26134
+ SELECT COALESCE(jsonb_object_agg(f.key, f.value), '{}'::jsonb)
26135
+ FROM jsonb_each(x.payload) f WHERE f.key = ANY(${summaryFieldsParam}::text[])
26136
+ ) ELSE x.payload END, 'summarized', ${summarized}`;
26137
+ }
26138
+ return `(SELECT COALESCE(jsonb_agg(jsonb_build_object(
26139
+ 'seq', x.seq, 'kind', x.kind, 'step_id', x.step_id, 'absent', x.payload IS NULL, 'payload', ${payload}
26140
+ ) ORDER BY CASE WHEN x.kind = 'step' THEN x.first_seq ELSE x.seq END), '[]'::jsonb)
26141
+ FROM (
26142
+ SELECT e.seq, e.kind, e.step_id, e.payload,
26143
+ ROW_NUMBER() OVER (PARTITION BY e.kind, e.step_id ORDER BY e.seq DESC) AS rn,
26144
+ MIN(e.seq) OVER (PARTITION BY e.kind, e.step_id) AS first_seq,
26145
+ MAX(CASE WHEN e.kind = 'step' THEN e.seq END) OVER (PARTITION BY e.step_id) AS step_seq,
26146
+ MAX(CASE WHEN e.kind = 'step-patch' THEN e.seq END) OVER (PARTITION BY e.step_id) AS patch_seq,
26147
+ MAX(CASE WHEN e.kind IN ('state', 'state-base') THEN e.seq END) OVER () AS base_seq
26148
+ FROM ${this.#entriesTable()} e
26149
+ WHERE e.workflow_name = ${alias}.workflow_name AND e.run_id = ${alias}.run_id AND e.kind <> 'state-draft'
26150
+ ) x
26151
+ WHERE CASE x.kind
26152
+ WHEN 'step' THEN x.rn = 1
26153
+ WHEN 'step-patch' THEN x.seq > x.step_seq
26154
+ ELSE x.base_seq IS NULL OR x.seq >= x.base_seq
26155
+ END)`;
26156
+ }
26157
+ /**
26158
+ * A jsonb array of {@link EntryRow} with the entries `materializeState` uses for the state of the
26159
+ * storage v2 run `alias` refers to as of a version: `seqParam` its `seq`, `draftParam` whether
26160
+ * it is a draft. For a draft, every state entry up to it, as the drafts it is on can build on
26161
+ * any earlier version; otherwise the latest full state or state base up to it and the state
26162
+ * patches after it.
26163
+ */
26164
+ #stateEntriesSql(alias, seqParam, draftParam) {
26165
+ return `(SELECT COALESCE(jsonb_agg(jsonb_build_object(
26166
+ 'seq', x.seq, 'kind', x.kind, 'step_id', x.step_id, 'absent', x.payload IS NULL, 'payload', x.payload
26167
+ ) ORDER BY x.seq), '[]'::jsonb)
26168
+ FROM (
26169
+ SELECT e.seq, e.kind, e.step_id, e.payload,
26170
+ MAX(CASE WHEN e.kind IN ('state', 'state-base') THEN e.seq END) OVER () AS base_seq
26171
+ FROM ${this.#entriesTable()} e
26172
+ WHERE e.workflow_name = ${alias}.workflow_name AND e.run_id = ${alias}.run_id
26173
+ AND e.kind NOT IN ('step', 'step-patch') AND e.seq <= ${seqParam}::int
26174
+ ) x
26175
+ WHERE ${draftParam}::boolean OR (x.kind <> 'state-draft' AND (x.base_seq IS NULL OR x.seq >= x.base_seq)))`;
26176
+ }
26177
+ /**
26178
+ * Selects v1 snapshot rows and storage v2 runs as one result, newest first. Each version
26179
+ * has its own WHERE clause; `null` leaves it out. Storage v2 rows carry their header and
26180
+ * entries, or in summary mode only the summary snapshot. `page` is appended to the
26181
+ * combined, ordered rows (e.g. `LIMIT 1`).
26182
+ */
26183
+ async #runsQuery({ v1Where, v2Where, v1Snapshot = "snapshot", summary, page = "" }) {
26184
+ const branches = [];
26185
+ if (v1Where !== null) {
26186
+ const optional = {
26187
+ resourceId: "text",
26188
+ createdAtZ: "timestamptz",
26189
+ updatedAtZ: "timestamptz"
26190
+ };
26191
+ const columns = {};
26192
+ for (const [column, type] of Object.entries(optional)) columns[column] = await this.#db.getColumnType(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT, column) !== null ? `"${column}"` : `NULL::${type} AS "${column}"`;
26193
+ branches.push(`SELECT workflow_name, run_id, ${columns.resourceId}, ${v1Snapshot} AS snapshot, NULL::jsonb AS header,
26194
+ "createdAt", ${columns.createdAtZ}, "updatedAt", ${columns.updatedAtZ}, 1 AS version
26195
+ FROM ${getTableName({
26196
+ indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
26197
+ schemaName: getSchemaName(this.#schema)
26198
+ })} ${v1Where}`);
26199
+ }
26200
+ if (v2Where !== null) {
26201
+ const columns = summary ? `jsonb_build_object('status', header -> 'status', 'timestamp', header -> 'timestamp') AS snapshot, NULL::jsonb AS header` : `NULL AS snapshot, header`;
26202
+ branches.push(`SELECT workflow_name, run_id, "resourceId", ${columns},
26203
+ "createdAt", "createdAtZ", "updatedAt", "updatedAtZ", 2 AS version
26204
+ FROM ${this.#runsTable()} ${v2Where}`);
26205
+ }
26206
+ return `SELECT p.*${summary ? "" : `, CASE WHEN p.version = 2 THEN ${this.#entriesSql("p")} END AS entries`}
26207
+ FROM (${branches.join(" UNION ALL ")} ORDER BY "createdAt" DESC ${page}) p
26208
+ ORDER BY p."createdAt" DESC`;
26209
+ }
26210
+ /**
26211
+ * The `base` that `materializeState` takes for a run's entries (as of `at`, or latest): the state
26212
+ * version of another run it builds on (`stateBaseOf`), resolved recursively, else undefined. `{}`
26213
+ * when that run isn't stored. One query per run in the chain.
26214
+ */
26215
+ async #stateBase(q, entries, at) {
26216
+ const ref = (0, _mastra_core_storage.stateBaseOf)(entries, at);
26217
+ if (!ref) return void 0;
26218
+ return (await this.#stateAt(q, ref.workflowName, ref.runId, ref))?.state ?? {};
26219
+ }
26220
+ /** A storage v2 run's state as of the version `at`, or null when the run isn't stored. */
26221
+ async #stateAt(q, workflowName, runId, at) {
26222
+ const row = await q.oneOrNone(`SELECT ${this.#stateEntriesSql("r", "$3", "$4")} AS entries
26223
+ FROM ${this.#runsTable()} r WHERE r.workflow_name = $1 AND r.run_id = $2`, [
26224
+ workflowName,
26225
+ runId,
26226
+ at.seq,
26227
+ at.draft === true
26228
+ ]);
26229
+ if (!row) return null;
26230
+ const entries = toEntries(row.entries);
26231
+ return (0, _mastra_core_storage.materializeState)(entries, {
26232
+ at,
26233
+ base: await this.#stateBase(q, entries, at)
26234
+ });
26235
+ }
26236
+ /**
26237
+ * Before a run is deleted, stores the full state in place of every `state-base` entry built on
26238
+ * it, so the runs built on it keep reading the same state. Runs inside the delete's transaction.
26239
+ */
26240
+ async #detachStateChildren(q, workflowName, runId) {
26241
+ const children = await q.manyOrNone(`SELECT workflow_name, run_id, seq, payload FROM ${this.#entriesTable()}
26242
+ WHERE kind = 'state-base' AND (${STATE_BASE_RUN_EXPR}) = ($1, $2)`, [workflowName, runId]);
26243
+ const states = /* @__PURE__ */ new Map();
26244
+ for (const child of children) {
26245
+ const version = `${child.payload.seq}:${child.payload.draft === true}`;
26246
+ let state = states.get(version);
26247
+ if (!state) {
26248
+ state = (await this.#stateAt(q, workflowName, runId, child.payload))?.state ?? {};
26249
+ states.set(version, state);
26250
+ }
26251
+ await q.none(`UPDATE ${this.#entriesTable()} SET kind = 'state', payload = $4::jsonb
26252
+ WHERE workflow_name = $1 AND run_id = $2 AND seq = $3`, [
26253
+ child.workflow_name,
26254
+ child.run_id,
26255
+ child.seq,
26256
+ toPgJson(state)
26257
+ ]);
26258
+ }
26259
+ }
26260
+ /** Rebuilds a storage v2 run's `WorkflowRunState` from its header and `#entriesSql` rows. */
26261
+ async #materializeRun(client, header, rows) {
26262
+ const entries = toEntries(rows);
26263
+ return (0, _mastra_core_storage.materializeWorkflowRun)(header, entries, await this.#stateBase(client, entries));
26264
+ }
26265
+ /** Parses a row of `#runsQuery`. */
26266
+ async #parseRun(client, row) {
26267
+ if (row.version !== 2) return this.parseWorkflowRun(row);
26268
+ const snapshot = row.entries ? await this.#materializeRun(client, row.header, row.entries) : row.snapshot;
26269
+ return {
26270
+ ...this.parseWorkflowRun({
26271
+ ...row,
26272
+ snapshot
26273
+ }),
26274
+ storageVersion: 2
26275
+ };
26276
+ }
26277
+ #runError(operation, workflowName, runId, error) {
26278
+ return new _mastra_core_error.MastraError({
26279
+ id: (0, _mastra_core_storage.createStorageErrorId)("PG", operation, "FAILED"),
26280
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
26281
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
26282
+ details: {
26283
+ workflowName,
26284
+ runId
26285
+ }
26286
+ }, error);
26287
+ }
26288
+ async loadWorkflowRunHeader({ workflowName, runId }) {
26289
+ try {
26290
+ return await this.#db.client.oneOrNone(`SELECT header, "lastSeq", "startStateSeq", "runToken" FROM ${this.#runsTable()} WHERE workflow_name = $1 AND run_id = $2`, [workflowName, runId]);
26291
+ } catch (error) {
26292
+ throw this.#runError("LOAD_WORKFLOW_RUN_HEADER", workflowName, runId, error);
26293
+ }
26294
+ }
26295
+ async loadWorkflowRun({ workflowName, runId, successfulSteps }) {
26296
+ try {
26297
+ const summary = successfulSteps === "summary";
26298
+ const run = await this.#db.client.oneOrNone(`SELECT r.header, r."lastSeq", r."startStateSeq", r."runToken", ${this.#entriesSql("r", { summaryFieldsParam: summary ? "$3" : void 0 })} AS entries
26299
+ FROM ${this.#runsTable()} r WHERE r.workflow_name = $1 AND r.run_id = $2`, summary ? [
26300
+ workflowName,
26301
+ runId,
26302
+ _mastra_core_storage.STEP_RESULT_SUMMARY_FIELDS
26303
+ ] : [workflowName, runId]);
26304
+ if (!run) return null;
26305
+ const entries = toEntries(run.entries);
26306
+ const summarizedInQuery = new Set(run.entries.filter((row) => row.summarized).map((row) => row.step_id));
26307
+ const steps = {};
26308
+ const summarizedStepIds = [];
26309
+ for (const [stepId, result] of (0, _mastra_core_storage.latestStepResults)(entries)) {
26310
+ steps[stepId] = result;
26311
+ if (summary && stepId !== "input" && result?.status === "success") {
26312
+ if (!summarizedInQuery.has(stepId)) steps[stepId] = (0, _mastra_core_storage.summarizeStepResult)(result);
26313
+ summarizedStepIds.push(stepId);
26314
+ }
26315
+ }
26316
+ const base = await this.#stateBase(this.#db.client, entries);
26317
+ const { state, statePatchBytes } = (0, _mastra_core_storage.materializeState)(entries, { base });
26318
+ return {
26319
+ header: run.header,
26320
+ lastSeq: run.lastSeq,
26321
+ startStateSeq: run.startStateSeq,
26322
+ runToken: run.runToken,
26323
+ state,
26324
+ statePatchBytes,
26325
+ steps,
26326
+ summarizedStepIds
26327
+ };
26328
+ } catch (error) {
26329
+ throw this.#runError("LOAD_WORKFLOW_RUN", workflowName, runId, error);
26330
+ }
26331
+ }
26332
+ async loadWorkflowRunSteps({ workflowName, runId, stepIds, expectedLastSeq, expectedRunToken }) {
26333
+ try {
26334
+ const run = await this.#db.client.oneOrNone(`SELECT r."lastSeq", r."runToken", (
26335
+ SELECT COALESCE(jsonb_agg(jsonb_build_object(
26336
+ 'seq', x.seq, 'kind', x.kind, 'step_id', x.step_id, 'absent', x.payload IS NULL, 'payload', x.payload
26337
+ ) ORDER BY x.seq), '[]'::jsonb)
26338
+ FROM (
26339
+ SELECT e.seq, e.kind, e.step_id, e.payload,
26340
+ MAX(CASE WHEN e.kind = 'step' THEN e.seq END) OVER (PARTITION BY e.step_id) AS step_seq
26341
+ FROM ${this.#entriesTable()} e
26342
+ WHERE e.workflow_name = r.workflow_name AND e.run_id = r.run_id
26343
+ AND e.kind IN ('step', 'step-patch') AND e.step_id = ANY($3::text[])
26344
+ ) x
26345
+ WHERE x.seq >= x.step_seq
26346
+ ) AS entries
26347
+ FROM ${this.#runsTable()} r WHERE r.workflow_name = $1 AND r.run_id = $2`, [
26348
+ workflowName,
26349
+ runId,
26350
+ stepIds
26351
+ ]);
26352
+ if (!run) return null;
26353
+ if (expectedLastSeq !== void 0 && run.lastSeq !== expectedLastSeq) return null;
26354
+ if (expectedRunToken !== void 0 && run.runToken !== expectedRunToken) return null;
26355
+ return Object.fromEntries((0, _mastra_core_storage.latestStepResults)(toEntries(run.entries)));
26356
+ } catch (error) {
26357
+ throw this.#runError("LOAD_WORKFLOW_RUN_STEPS", workflowName, runId, error);
26358
+ }
26359
+ }
26360
+ async updateWorkflowRunHeader({ workflowName, runId, opts, fence: explicitFence }) {
26361
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence, runId);
26362
+ try {
26363
+ return await this.#db.client.tx(async (t) => {
26364
+ if (fence) await assertRunFence(t, this.#runOwnersTable(), fence, "updateWorkflowRunHeader");
26365
+ const run = await t.oneOrNone(`SELECT header FROM ${this.#runsTable()} WHERE workflow_name = $1 AND run_id = $2 FOR UPDATE`, [workflowName, runId]);
26366
+ if (!run) return void 0;
26367
+ const { expectedStatus, ...patch } = opts;
26368
+ if (!(0, _mastra_core_storage.matchesExpectedWorkflowStatus)(run.header.status, expectedStatus)) return void 0;
26369
+ const header = {
26370
+ ...run.header,
26371
+ ...patch
26372
+ };
26373
+ const now = /* @__PURE__ */ new Date();
26374
+ return await t.one(`UPDATE ${this.#runsTable()}
26375
+ SET header = $3::jsonb, status = $4, "updatedAt" = $5, "updatedAtZ" = $6
26376
+ WHERE workflow_name = $1 AND run_id = $2
26377
+ RETURNING header, "lastSeq", "startStateSeq", "runToken"`, [
26378
+ workflowName,
26379
+ runId,
26380
+ toPgJson(header),
26381
+ header.status,
26382
+ now,
26383
+ now
26384
+ ]);
26385
+ });
26386
+ } catch (error) {
26387
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
26388
+ throw this.#runError("UPDATE_WORKFLOW_RUN_HEADER", workflowName, runId, error);
26389
+ }
26390
+ }
26391
+ /**
26392
+ * Upserts a storage v2 run's header from `$1`–`$12` ({@link #headerParams}), adds `$7` (the
26393
+ * number of entries the write adds) to `lastSeq`, and returns the new `lastSeq`, `stateShared`
26394
+ * and `runToken`. `$12` is the run token of a run the write creates; a stored run keeps its own.
26395
+ * A checkpoint's state entries come last, so its `startStateSeq` is the new `lastSeq`; an append
26396
+ * keeps the stored one unless given `$13`. The header's row lock is held
26397
+ * until commit, so the writes to one run commit in `seq` order. With `expectedParam` (a text
26398
+ * array of statuses), a stored run whose status isn't in it is left as is and nothing is returned.
26399
+ */
26400
+ #upsertHeaderSql(checkpoint, expectedParam) {
26401
+ const startStateSeq = checkpoint ? {
26402
+ insert: "$7::int",
26403
+ update: "r.\"lastSeq\" + EXCLUDED.\"lastSeq\""
26404
+ } : {
26405
+ insert: "COALESCE($13::int, 0)",
26406
+ update: "COALESCE($13::int, r.\"startStateSeq\")"
26407
+ };
26408
+ return `INSERT INTO ${this.#runsTable()} AS r
26409
+ (workflow_name, run_id, "resourceId", "threadId", status, header, "lastSeq", "startStateSeq", "stateShared",
26410
+ "runToken", "createdAt", "updatedAt", "createdAtZ", "updatedAtZ")
26411
+ VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7::int, ${startStateSeq.insert}, false, $12, $8, $9, $10, $11)
26412
+ ON CONFLICT (workflow_name, run_id) DO UPDATE
26413
+ SET "resourceId" = COALESCE(EXCLUDED."resourceId", r."resourceId"), "threadId" = EXCLUDED."threadId",
26414
+ status = EXCLUDED.status, header = EXCLUDED.header, "lastSeq" = r."lastSeq" + EXCLUDED."lastSeq",
26415
+ "startStateSeq" = ${startStateSeq.update},
26416
+ "updatedAt" = EXCLUDED."updatedAt", "updatedAtZ" = EXCLUDED."updatedAtZ"
26417
+ ${expectedParam ? `WHERE r.status = ANY(${expectedParam}::text[])` : ""}
26418
+ RETURNING "lastSeq", "stateShared", "runToken"`;
26419
+ }
26420
+ #headerParams(run, entryCount) {
26421
+ const now = /* @__PURE__ */ new Date();
26422
+ return [
26423
+ run.workflowName,
26424
+ run.runId,
26425
+ run.resourceId,
26426
+ run.threadId ?? null,
26427
+ run.header.status,
26428
+ toPgJson(run.header),
26429
+ entryCount,
26430
+ now,
26431
+ now,
26432
+ now,
26433
+ now,
26434
+ crypto.randomUUID()
26435
+ ];
26436
+ }
26437
+ async writeWorkflowRunCheckpoint({ fence: explicitFence, expectedStatus, steps, state, stateBase, statePatch, ...run }) {
26438
+ const entries = journalEntries(steps, {
26439
+ state,
26440
+ stateBase,
26441
+ statePatch
26442
+ }, true);
26443
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence, run.runId);
26444
+ const expected = expectedStatus === void 0 ? [] : [[expectedStatus].flat()];
26445
+ try {
26446
+ return await this.#db.client.tx(async (t) => {
26447
+ if (fence) await assertRunFence(t, this.#runOwnersTable(), fence, "writeWorkflowRunCheckpoint");
26448
+ const base = state === void 0 ? stateBase : void 0;
26449
+ let baseLastSeq;
26450
+ if (base) baseLastSeq = (await t.oneOrNone(`SELECT "lastSeq" FROM ${this.#runsTable()} WHERE workflow_name = $1 AND run_id = $2 FOR KEY SHARE`, [base.workflowName, base.runId]))?.lastSeq;
26451
+ const header = await t.oneOrNone(this.#upsertHeaderSql(true, expected.length ? "$13" : void 0), [...this.#headerParams(run, entries.length), ...expected]);
26452
+ if (!header) return null;
26453
+ if (base) {
26454
+ await t.none(`UPDATE ${this.#runsTable()} SET "stateShared" = true
26455
+ WHERE workflow_name = $1 AND run_id = $2 AND NOT "stateShared"`, [base.workflowName, base.runId]);
26456
+ const { stored } = await t.one(`SELECT EXISTS (
26457
+ SELECT 1 FROM ${this.#entriesTable()} WHERE workflow_name = $1 AND run_id = $2
26458
+ AND CASE WHEN $4::boolean THEN kind = 'state-draft' AND seq = $3::int
26459
+ ELSE kind IN ('state', 'state-base') AND seq <= $3::int END
26460
+ ) AS stored`, [
26461
+ base.workflowName,
26462
+ base.runId,
26463
+ base.seq,
26464
+ base.draft === true
26465
+ ]);
26466
+ if (!stored || baseLastSeq === void 0 || base.seq > baseLastSeq) throw new Error(`Workflow run ${run.workflowName}/${run.runId} builds on state version ${base.seq} of ${base.workflowName}/${base.runId}, which is not stored`);
26467
+ }
26468
+ const { lastSeq, stateShared, runToken } = header;
26469
+ await t.none(`DELETE FROM ${this.#entriesTable()}
26470
+ WHERE workflow_name = $1 AND run_id = $2${stateShared ? ` AND kind IN ('step', 'step-patch')` : ""}`, [run.workflowName, run.runId]);
26471
+ await t.none(`INSERT INTO ${this.#entriesTable()} (workflow_name, run_id, seq, kind, step_id, payload)
26472
+ SELECT $1, $2, $3::int - $4::int + e.ord, ${ENTRY_VALUES}
26473
+ FROM ${entryRows("$5")}`, [
26474
+ run.workflowName,
26475
+ run.runId,
26476
+ lastSeq,
26477
+ entries.length,
26478
+ toPgJson(entries)
26479
+ ]);
26480
+ return {
26481
+ lastSeq,
26482
+ runToken
26483
+ };
26484
+ });
26485
+ } catch (error) {
26486
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
26487
+ throw this.#runError("WRITE_WORKFLOW_RUN_CHECKPOINT", run.workflowName, run.runId, error);
26488
+ }
26489
+ }
26490
+ /** One statement: the header upsert numbers the entries inserted after it. */
26491
+ async appendWorkflowRunEntries({ fence, steps, stepPatches, state, statePatch, startStateSeq, expectedStatus, ...run }) {
26492
+ const entries = journalEntries(steps, {
26493
+ state,
26494
+ statePatch
26495
+ }, false, stepPatches);
26496
+ const expected = expectedStatus === void 0 ? [] : [[expectedStatus].flat()];
26497
+ try {
26498
+ return await withRunFence(this.#db.client, this.#runOwnersTable(), (0, _mastra_core_storage.resolveRunFence)(this, fence, run.runId), "appendWorkflowRunEntries", (q) => q.oneOrNone(`WITH h AS (${this.#upsertHeaderSql(false, expected.length ? "$15" : void 0)}),
26499
+ e AS (
26500
+ INSERT INTO ${this.#entriesTable()} (workflow_name, run_id, seq, kind, step_id, payload)
26501
+ SELECT $1, $2, h."lastSeq" - $7::int + e.ord, ${ENTRY_VALUES}
26502
+ FROM h, ${entryRows("$14")}
26503
+ )
26504
+ SELECT "lastSeq", "runToken" FROM h`, [
26505
+ ...this.#headerParams(run, entries.length),
26506
+ startStateSeq ?? null,
26507
+ toPgJson(entries),
26508
+ ...expected
26509
+ ]));
26510
+ } catch (error) {
26511
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
26512
+ throw this.#runError("APPEND_WORKFLOW_RUN_ENTRIES", run.workflowName, run.runId, error);
26513
+ }
26514
+ }
26515
+ async loadWorkflowRunState({ workflowName, runId, seq }) {
26516
+ try {
26517
+ return (await this.#stateAt(this.#db.client, workflowName, runId, { seq }))?.state ?? null;
26518
+ } catch (error) {
26519
+ throw this.#runError("LOAD_WORKFLOW_RUN_STATE", workflowName, runId, error);
26520
+ }
26521
+ }
26522
+ async appendWorkflowRunStateDraft(args) {
26523
+ const { workflowName, runId } = args;
26524
+ const payload = "state" in args ? { state: args.state } : {
26525
+ on: args.on.draft ? {
26526
+ seq: args.on.seq,
26527
+ draft: true
26528
+ } : { seq: args.on.seq },
26529
+ ops: args.ops
26530
+ };
26531
+ let row;
26532
+ try {
26533
+ row = await withRunFence(this.#db.client, this.#runOwnersTable(), (0, _mastra_core_storage.resolveRunFence)(this, void 0, runId), "appendWorkflowRunStateDraft", (q) => q.oneOrNone(`WITH h AS (
26534
+ UPDATE ${this.#runsTable()} SET "lastSeq" = "lastSeq" + 1, "stateShared" = true
26535
+ WHERE workflow_name = $1 AND run_id = $2
26536
+ RETURNING "lastSeq"
26537
+ )
26538
+ INSERT INTO ${this.#entriesTable()} (workflow_name, run_id, seq, kind, step_id, payload)
26539
+ SELECT $1, $2, h."lastSeq", 'state-draft', NULL, $3::jsonb FROM h
26540
+ RETURNING seq`, [
26541
+ workflowName,
26542
+ runId,
26543
+ toPgJson(payload)
26544
+ ]));
26545
+ } catch (error) {
26546
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
26547
+ throw this.#runError("APPEND_WORKFLOW_RUN_STATE_DRAFT", workflowName, runId, error);
26548
+ }
26549
+ if (!row) throw new Error(`Storage v2 run ${workflowName}/${runId} is not stored`);
26550
+ return row;
25873
26551
  }
25874
26552
  supportsRunFencing() {
25875
26553
  return true;
@@ -25998,6 +26676,7 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
25998
26676
  });
25999
26677
  const existingSnapshotResult = await t.oneOrNone(`SELECT snapshot FROM ${tableName} WHERE workflow_name = $1 AND run_id = $2 FOR UPDATE`, [workflowName, runId]);
26000
26678
  if (!existingSnapshotResult) {
26679
+ if (await t.oneOrNone(`SELECT 1 FROM ${this.#runsTable()} WHERE workflow_name = $1 AND run_id = $2`, [workflowName, runId])) throw new Error(`updateWorkflowResults does not support storage v2 runs (run ${runId})`);
26001
26680
  (0, _mastra_core_storage.assertWorkflowStepGuard)({
26002
26681
  snapshot: null,
26003
26682
  stepId,
@@ -26070,11 +26749,36 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
26070
26749
  }, error);
26071
26750
  }
26072
26751
  }
26073
- async updateWorkflowState({ workflowName, runId, opts, fence: explicitFence }) {
26752
+ async updateWorkflowState({ workflowName, runId, opts, fence: explicitFence, storageVersion }) {
26074
26753
  const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence, runId);
26075
26754
  try {
26076
26755
  return await this.#db.client.tx(async (t) => {
26077
26756
  if (fence) await assertRunFence(t, this.#runOwnersTable(), fence, "updateWorkflowState");
26757
+ if (storageVersion !== 1) {
26758
+ const run = await t.oneOrNone(`SELECT header FROM ${this.#runsTable()} WHERE workflow_name = $1 AND run_id = $2 FOR UPDATE`, [workflowName, runId]);
26759
+ if (run) {
26760
+ const { expectedStatus, ...patch } = opts;
26761
+ if (!(0, _mastra_core_storage.matchesExpectedWorkflowStatus)(run.header.status, expectedStatus)) return;
26762
+ const header = {
26763
+ ...run.header,
26764
+ ...patch
26765
+ };
26766
+ const now = /* @__PURE__ */ new Date();
26767
+ const { entries } = await t.one(`UPDATE ${this.#runsTable()} r
26768
+ SET header = $3::jsonb, status = $4, "updatedAt" = $5, "updatedAtZ" = $6
26769
+ WHERE r.workflow_name = $1 AND r.run_id = $2
26770
+ RETURNING ${this.#entriesSql("r")} AS entries`, [
26771
+ workflowName,
26772
+ runId,
26773
+ toPgJson(header),
26774
+ header.status,
26775
+ now,
26776
+ now
26777
+ ]);
26778
+ return this.#materializeRun(t, header, entries);
26779
+ }
26780
+ if (storageVersion === 2) return void 0;
26781
+ }
26078
26782
  const tableName = getTableName({
26079
26783
  indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
26080
26784
  schemaName: getSchemaName(this.#schema)
@@ -26154,10 +26858,14 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
26154
26858
  async loadWorkflowRunStatus({ workflowName, runId }) {
26155
26859
  try {
26156
26860
  const statusExpr = await this.#statusExpression("snapshot");
26157
- return (await this.#db.client.oneOrNone(`SELECT ${statusExpr} AS status FROM ${getTableName({
26861
+ return (await this.#db.client.oneOrNone(`SELECT COALESCE(
26862
+ (SELECT status FROM ${this.#runsTable()} WHERE workflow_name = $1 AND run_id = $2),
26863
+ (SELECT ${statusExpr} FROM ${getTableName({
26158
26864
  indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
26159
26865
  schemaName: getSchemaName(this.#schema)
26160
- })} WHERE "workflow_name" = $1 AND "run_id" = $2 ORDER BY "createdAt" DESC LIMIT 1`, [workflowName, runId]))?.status ?? null;
26866
+ })}
26867
+ WHERE "workflow_name" = $1 AND "run_id" = $2 ORDER BY "createdAt" DESC LIMIT 1)
26868
+ ) AS status`, [workflowName, runId]))?.status ?? null;
26161
26869
  } catch (error) {
26162
26870
  throw new _mastra_core_error.MastraError({
26163
26871
  id: (0, _mastra_core_storage.createStorageErrorId)("PG", "LOAD_WORKFLOW_RUN_STATUS", "FAILED"),
@@ -26170,8 +26878,14 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
26170
26878
  }, error);
26171
26879
  }
26172
26880
  }
26173
- async loadWorkflowSnapshot({ workflowName, runId }) {
26881
+ async loadWorkflowSnapshot({ workflowName, runId, storageVersion }) {
26174
26882
  try {
26883
+ if (storageVersion !== 1) {
26884
+ const run = await this.#db.client.oneOrNone(`SELECT r.header, ${this.#entriesSql("r")} AS entries
26885
+ FROM ${this.#runsTable()} r WHERE r.workflow_name = $1 AND r.run_id = $2`, [workflowName, runId]);
26886
+ if (run) return await this.#materializeRun(this.#db.client, run.header, run.entries);
26887
+ if (storageVersion === 2) return null;
26888
+ }
26175
26889
  const result = await this.#db.load({
26176
26890
  tableName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
26177
26891
  keys: {
@@ -26188,7 +26902,7 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
26188
26902
  }, error);
26189
26903
  }
26190
26904
  }
26191
- async getWorkflowRunById({ runId, workflowName }) {
26905
+ async getWorkflowRunById({ runId, workflowName, storageVersion }) {
26192
26906
  try {
26193
26907
  const conditions = [];
26194
26908
  const values = [];
@@ -26204,18 +26918,14 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
26204
26918
  paramIndex++;
26205
26919
  }
26206
26920
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
26207
- const query = `
26208
- SELECT * FROM ${getTableName({
26209
- indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
26210
- schemaName: getSchemaName(this.#schema)
26211
- })}
26212
- ${whereClause}
26213
- ORDER BY "createdAt" DESC LIMIT 1
26214
- `;
26215
- const queryValues = values;
26216
- const result = await this.#db.readClient.oneOrNone(query, queryValues);
26921
+ const query = await this.#runsQuery({
26922
+ v1Where: storageVersion === 2 ? null : whereClause,
26923
+ v2Where: storageVersion === 1 ? null : whereClause,
26924
+ page: "LIMIT 1"
26925
+ });
26926
+ const result = await this.#db.readClient.oneOrNone(query, values);
26217
26927
  if (!result) return null;
26218
- return this.parseWorkflowRun(result);
26928
+ return await this.#parseRun(this.#db.readClient, result);
26219
26929
  } catch (error) {
26220
26930
  throw new _mastra_core_error.MastraError({
26221
26931
  id: (0, _mastra_core_storage.createStorageErrorId)("PG", "GET_WORKFLOW_RUN_BY_ID", "FAILED"),
@@ -26228,12 +26938,28 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
26228
26938
  }, error);
26229
26939
  }
26230
26940
  }
26231
- async deleteWorkflowRunById({ runId, workflowName, fence }) {
26941
+ async deleteWorkflowRunById({ runId, workflowName, storageVersion, fence }) {
26942
+ const resolvedFence = (0, _mastra_core_storage.resolveRunFence)(this, fence, runId);
26232
26943
  try {
26233
- await withRunFence(this.#db.client, this.#runOwnersTable(), (0, _mastra_core_storage.resolveRunFence)(this, fence, runId), "deleteWorkflowRunById", (q) => q.none(`DELETE FROM ${getTableName({
26234
- indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
26235
- schemaName: getSchemaName(this.#schema)
26236
- })} WHERE run_id = $1 AND workflow_name = $2`, [runId, workflowName]));
26944
+ if (storageVersion === 1) {
26945
+ await withRunFence(this.#db.client, this.#runOwnersTable(), resolvedFence, "deleteWorkflowRunById", (q) => q.none(`DELETE FROM ${getTableName({
26946
+ indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
26947
+ schemaName: getSchemaName(this.#schema)
26948
+ })} WHERE run_id = $1 AND workflow_name = $2`, [runId, workflowName]));
26949
+ return;
26950
+ }
26951
+ await this.#db.client.tx(async (t) => {
26952
+ if (resolvedFence) await assertRunFence(t, this.#runOwnersTable(), resolvedFence, "deleteWorkflowRunById");
26953
+ if ((await t.oneOrNone(`SELECT "stateShared" FROM ${this.#runsTable()} WHERE workflow_name = $1 AND run_id = $2 FOR UPDATE`, [workflowName, runId]))?.stateShared) await this.#detachStateChildren(t, workflowName, runId);
26954
+ for (const table of [
26955
+ this.#runsTable(),
26956
+ this.#entriesTable(),
26957
+ ...storageVersion === 2 ? [] : [getTableName({
26958
+ indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
26959
+ schemaName: getSchemaName(this.#schema)
26960
+ })]
26961
+ ]) await t.none(`DELETE FROM ${table} WHERE run_id = $1 AND workflow_name = $2`, [runId, workflowName]);
26962
+ });
26237
26963
  } catch (error) {
26238
26964
  if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
26239
26965
  throw new _mastra_core_error.MastraError({
@@ -26250,77 +26976,84 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
26250
26976
  async listWorkflowRuns({ workflowName, fromDate, toDate, perPage, page, resourceId, threadId, status, summary } = {}) {
26251
26977
  try {
26252
26978
  const conditions = [];
26979
+ const v2Conditions = [];
26253
26980
  const values = [];
26254
26981
  let paramIndex = 1;
26255
26982
  if (workflowName) {
26256
26983
  conditions.push(`workflow_name = $${paramIndex}`);
26984
+ v2Conditions.push(`workflow_name = $${paramIndex}`);
26257
26985
  values.push(workflowName);
26258
26986
  paramIndex++;
26259
26987
  }
26260
26988
  if (status) {
26261
26989
  const statusExpr = await this.#db.getColumnType(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT, "snapshot") === "jsonb" ? `snapshot ->> 'status'` : `regexp_replace(snapshot::text, '\\\\u(0000|[Dd][89A-Fa-f][0-9A-Fa-f]{2})', '', 'g')::jsonb ->> 'status'`;
26262
26990
  conditions.push(`${statusExpr} = $${paramIndex}`);
26991
+ v2Conditions.push(`status = $${paramIndex}`);
26263
26992
  values.push(status);
26264
26993
  paramIndex++;
26265
26994
  }
26266
26995
  if (resourceId) {
26267
- if (await this.#db.hasColumn(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT, "resourceId")) {
26268
- conditions.push(`"resourceId" = $${paramIndex}`);
26269
- values.push(resourceId);
26270
- paramIndex++;
26271
- } else this.logger?.warn?.(`[${_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
26996
+ if (await this.#db.hasColumn(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT, "resourceId")) conditions.push(`"resourceId" = $${paramIndex}`);
26997
+ else this.logger?.warn?.(`[${_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
26998
+ v2Conditions.push(`"resourceId" = $${paramIndex}`);
26999
+ values.push(resourceId);
27000
+ paramIndex++;
26272
27001
  }
26273
27002
  if (threadId) {
26274
- if (await this.#db.getColumnType(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT, "snapshot") === "jsonb") {
26275
- conditions.push(`${WORKFLOW_SNAPSHOT_THREAD_ID_EXPR} = $${paramIndex}`);
26276
- values.push(threadId);
26277
- paramIndex++;
26278
- } else this.logger?.warn?.(`[${_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT}] snapshot column is not jsonb. Skipping threadId filter.`);
27003
+ if (await this.#db.getColumnType(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT, "snapshot") === "jsonb") conditions.push(`${WORKFLOW_SNAPSHOT_THREAD_ID_EXPR} = $${paramIndex}`);
27004
+ else this.logger?.warn?.(`[${_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT}] snapshot column is not jsonb. Skipping threadId filter.`);
27005
+ v2Conditions.push(`"threadId" = $${paramIndex}`);
27006
+ values.push(threadId);
27007
+ paramIndex++;
26279
27008
  }
26280
27009
  if (fromDate) {
26281
27010
  conditions.push(`"createdAt" >= $${paramIndex}`);
27011
+ v2Conditions.push(`"createdAt" >= $${paramIndex}`);
26282
27012
  values.push(fromDate);
26283
27013
  paramIndex++;
26284
27014
  }
26285
27015
  if (toDate) {
26286
27016
  conditions.push(`"createdAt" <= $${paramIndex}`);
27017
+ v2Conditions.push(`"createdAt" <= $${paramIndex}`);
26287
27018
  values.push(toDate);
26288
27019
  paramIndex++;
26289
27020
  }
26290
27021
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
26291
- let total = 0;
27022
+ const v2WhereClause = v2Conditions.length > 0 ? `WHERE ${v2Conditions.join(" AND ")}` : "";
26292
27023
  const usePagination = typeof perPage === "number" && typeof page === "number";
26293
- if (usePagination) {
26294
- const countResult = await this.#db.client.one(`SELECT COUNT(*) as count FROM ${getTableName({
26295
- indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
26296
- schemaName: getSchemaName(this.#schema)
26297
- })} ${whereClause}`, values);
26298
- total = Number(countResult.count);
26299
- }
26300
27024
  const normalizedPerPage = usePagination ? (0, _mastra_core_storage.normalizePerPage)(perPage, Number.MAX_SAFE_INTEGER) : 0;
26301
- const offset = usePagination ? page * normalizedPerPage : void 0;
26302
- let selectList = "*";
27025
+ const offset = usePagination ? page * normalizedPerPage : 0;
27026
+ let v1Snapshot = "snapshot";
26303
27027
  if (summary) {
26304
27028
  const snapshotJson = await this.#db.getColumnType(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT, "snapshot") === "jsonb" ? "snapshot" : `regexp_replace(snapshot::text, '\\\\u(0000|[Dd][89A-Fa-f][0-9A-Fa-f]{2})', '', 'g')::jsonb`;
26305
- selectList = `workflow_name, run_id, "resourceId", "createdAt", "createdAtZ", "updatedAt", "updatedAtZ", jsonb_build_object('status', ${snapshotJson} -> 'status', 'timestamp', ${snapshotJson} -> 'timestamp') AS snapshot`;
27029
+ v1Snapshot = `jsonb_build_object('status', ${snapshotJson} -> 'status', 'timestamp', ${snapshotJson} -> 'timestamp')`;
26306
27030
  }
26307
- const query = `
26308
- SELECT ${selectList} FROM ${getTableName({
26309
- indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
26310
- schemaName: getSchemaName(this.#schema)
26311
- })}
26312
- ${whereClause}
26313
- ORDER BY "createdAt" DESC
26314
- ${usePagination ? ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}` : ""}
26315
- `;
27031
+ const query = await this.#runsQuery({
27032
+ v1Where: whereClause,
27033
+ v2Where: v2WhereClause,
27034
+ v1Snapshot,
27035
+ summary,
27036
+ page: usePagination ? `LIMIT $${paramIndex} OFFSET $${paramIndex + 1}` : ""
27037
+ });
26316
27038
  const queryValues = usePagination ? [
26317
27039
  ...values,
26318
27040
  normalizedPerPage,
26319
27041
  offset
26320
27042
  ] : values;
26321
- const runs = (await this.#db.client.manyOrNone(query, queryValues) || []).map((row) => {
26322
- return this.parseWorkflowRun(row);
26323
- });
27043
+ const result = await this.#db.client.manyOrNone(query, queryValues);
27044
+ const runs = await Promise.all((result || []).map((row) => this.#parseRun(this.#db.client, row)));
27045
+ let total = 0;
27046
+ if (usePagination) {
27047
+ total = offset + runs.length;
27048
+ if (runs.length === normalizedPerPage || runs.length === 0 && offset > 0) {
27049
+ const countResult = await this.#db.client.one(`SELECT (SELECT COUNT(*) FROM ${getTableName({
27050
+ indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
27051
+ schemaName: getSchemaName(this.#schema)
27052
+ })} ${whereClause})
27053
+ + (SELECT COUNT(*) FROM ${this.#runsTable()} ${v2WhereClause}) AS count`, values);
27054
+ total = Number(countResult.count);
27055
+ }
27056
+ }
26324
27057
  return {
26325
27058
  runs,
26326
27059
  total: total || runs.length