@mastra/pg 1.35.0 → 1.36.0-alpha.0

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
@@ -1,6 +1,6 @@
1
1
  import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error";
2
2
  import * as coreStorage from "@mastra/core/storage";
3
- import { AgentsStorage, BRANCH_SPAN_TYPES, BackgroundTasksStorage, BlobStore, ChannelsStorage, DATASETS_SCHEMA, DATASET_ITEMS_SCHEMA, DATASET_VERSIONS_SCHEMA, DatasetsStorage, EXPERIMENTS_SCHEMA, EXPERIMENT_RESULTS_SCHEMA, EntityType, ExperimentsStorage, FactoryStorage, FavoritesStorage, KnowledgeConflictError, KnowledgeNotFoundError, KnowledgeStorage, MCPClientsStorage, MCPServersStorage, METRIC_DISTINCT_COLUMNS, MastraCompositeStore, MemoryStorage, NotificationsStorage, OBSERVATIONAL_MEMORY_TABLE_SCHEMA, ObservabilityStorage, PromptBlocksStorage, RUN_FENCING_TABLE_SCHEMAS, RunFenceConflictError, SchedulesStorage, ScorerDefinitionsStorage, ScoresStorage, SkillsStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_BACKGROUND_TASKS, TABLE_CHANNEL_CONFIG, TABLE_CHANNEL_INSTALLATIONS, TABLE_CONFIGS, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, TABLE_FAVORITES, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, TABLE_MEMORY_RUN_FENCES, TABLE_MESSAGES, TABLE_NOTIFICATIONS, TABLE_PROMPT_BLOCKS, TABLE_PROMPT_BLOCK_VERSIONS, TABLE_RESOURCES, TABLE_SCHEDULES, TABLE_SCHEDULE_TRIGGERS, TABLE_SCHEMAS, TABLE_SCORERS, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, TABLE_SKILLS, TABLE_SKILL_BLOBS, TABLE_SKILL_VERSIONS, TABLE_SPANS, TABLE_THREADS, TABLE_THREAD_STATE, TABLE_TOOL_PROVIDER_CONNECTIONS, TABLE_WORKFLOW_DEFINITIONS, TABLE_WORKFLOW_RUN_OWNERS, TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKSPACES, TABLE_WORKSPACE_VERSIONS, ThreadStateStorage, ToolProviderConnectionsStorage, TraceStatus, UniqueViolationError, WorkflowDefinitionsStorage, WorkflowsStorage, WorkspacesStorage, assertWorkflowStepGuard, calculatePagination, computeTraceStatus, createKnowledgeUlid, createStorageErrorId, createVectorErrorId, ensureDate, executeRetentionPrune, getDefaultValue, getSqlType, getStringAttribute, hasErrorCode, isRunFenceConflictError, isWorkflowStepConflictError, listBranchesArgsSchema, listFeedbackArgsSchema, listLogsArgsSchema, listMetricsArgsSchema, listScoresArgsSchema, listTracesArgsSchema, matchesExpectedWorkflowStatus, matchesRunFence, mergeWorkflowStepResult, normalizePerPage, normalizeScheduleTarget, parseDuration, resolveListOrderBy, resolveRetentionTargets, resolveRunFence, retentionCutoffMs, runRetentionBatches, safelyParseJSON, storageMessageMatchesMetadataFilter, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
3
+ import { AgentsStorage, BRANCH_SPAN_TYPES, BackgroundTasksStorage, BlobStore, ChannelsStorage, DATASETS_SCHEMA, DATASET_ITEMS_SCHEMA, DATASET_VERSIONS_SCHEMA, DatasetsStorage, EXPERIMENTS_SCHEMA, EXPERIMENT_RESULTS_SCHEMA, EntityType, ExperimentsStorage, FactoryStorage, FavoritesStorage, KnowledgeConflictError, KnowledgeNotFoundError, KnowledgeStorage, MCPClientsStorage, MCPServersStorage, METRIC_DISTINCT_COLUMNS, MastraCompositeStore, MemoryStorage, NotificationsStorage, OBSERVATIONAL_MEMORY_TABLE_SCHEMA, ObservabilityStorage, PromptBlocksStorage, RUN_FENCING_TABLE_SCHEMAS, RunFenceConflictError, STEP_RESULT_SUMMARY_FIELDS, SchedulesStorage, ScorerDefinitionsStorage, ScoresStorage, SkillsStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_BACKGROUND_TASKS, TABLE_CHANNEL_CONFIG, TABLE_CHANNEL_INSTALLATIONS, TABLE_CONFIGS, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, TABLE_FAVORITES, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, TABLE_MEMORY_RUN_FENCES, TABLE_MESSAGES, TABLE_NOTIFICATIONS, TABLE_PROMPT_BLOCKS, TABLE_PROMPT_BLOCK_VERSIONS, TABLE_RESOURCES, TABLE_SCHEDULES, TABLE_SCHEDULE_TRIGGERS, TABLE_SCHEMAS, TABLE_SCORERS, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, TABLE_SKILLS, TABLE_SKILL_BLOBS, TABLE_SKILL_VERSIONS, TABLE_SPANS, TABLE_THREADS, TABLE_THREAD_STATE, TABLE_TOOL_PROVIDER_CONNECTIONS, TABLE_WORKFLOW_DEFINITIONS, TABLE_WORKFLOW_RUNS, TABLE_WORKFLOW_RUN_ENTRIES, TABLE_WORKFLOW_RUN_OWNERS, TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKSPACES, TABLE_WORKSPACE_VERSIONS, ThreadStateStorage, ToolProviderConnectionsStorage, TraceStatus, UniqueViolationError, WORKFLOW_RUN_JOURNAL_TABLE_CONFIGS, WorkflowDefinitionsStorage, WorkflowsStorage, WorkspacesStorage, assertWorkflowStepGuard, calculatePagination, computeTraceStatus, createKnowledgeUlid, createStorageErrorId, createVectorErrorId, ensureDate, executeRetentionPrune, getDefaultValue, getSqlType, getStringAttribute, hasErrorCode, isRunFenceConflictError, isWorkflowStepConflictError, latestStepResults, listBranchesArgsSchema, listFeedbackArgsSchema, listLogsArgsSchema, listMetricsArgsSchema, listScoresArgsSchema, listTracesArgsSchema, matchesExpectedWorkflowStatus, matchesRunFence, materializeState, materializeWorkflowRun, mergeWorkflowStepResult, normalizePerPage, normalizeScheduleTarget, parseDuration, resolveListOrderBy, resolveRetentionTargets, resolveRunFence, retentionCutoffMs, runRetentionBatches, safelyParseJSON, stateBaseOf, storageMessageMatchesMetadataFilter, summarizeStepResult, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
4
4
  import { parseFieldKey, parseSqlIdentifier } from "@mastra/core/utils";
5
5
  import { MastraVector, validateTopK, validateUpsertInput } from "@mastra/core/vector";
6
6
  import { Mutex } from "async-mutex";
@@ -5219,13 +5219,13 @@ function cutoffFor(policy, anchorType, now = Date.now()) {
5219
5219
  return anchorType === "epoch-ms" ? cutoffMs : new Date(cutoffMs);
5220
5220
  }
5221
5221
  const runBatchedDelete = runRetentionBatches;
5222
- function runPrune({ db, domain, targets, options }) {
5222
+ function runPrune({ db, domain, targets, options, deleteBatch }) {
5223
5223
  return executeRetentionPrune({
5224
5224
  domain,
5225
5225
  targets,
5226
5226
  options,
5227
5227
  cutoffFor: (target, now) => cutoffFor(target.policy, target.anchorType ?? "timestamp", now),
5228
- deleteBatch: (target, cutoff, limit) => db.pruneBatch({
5228
+ deleteBatch: (target, cutoff, limit) => deleteBatch?.(target, cutoff, limit) ?? db.pruneBatch({
5229
5229
  tableName: target.table,
5230
5230
  column: target.column,
5231
5231
  cutoff,
@@ -25602,6 +25602,16 @@ function workflowSnapshotStatusIndexSQL(indexName, schemaName, concurrently = fa
25602
25602
  });
25603
25603
  return `CREATE INDEX ${concurrently ? "CONCURRENTLY " : ""}IF NOT EXISTS "${indexName}" ON ${tableName} (workflow_name, (snapshot ->> 'status'), "createdAt" DESC)`;
25604
25604
  }
25605
+ /**
25606
+ * Storage v2 index names share the long `<schema>_mastra_workflow_runs_` prefix, so a long
25607
+ * schema name would collapse them to one 63-byte identifier under plain truncation; hash instead.
25608
+ */
25609
+ function runIndexName(schemaPrefix, baseName) {
25610
+ return buildConstraintName({
25611
+ baseName: `${schemaPrefix}${baseName}`,
25612
+ hashWhenTruncated: true
25613
+ });
25614
+ }
25605
25615
  /** Base name (before any schema prefix) of the expression index backing the threadId filter. */
25606
25616
  const WORKFLOW_SNAPSHOT_THREAD_ID_INDEX = "mastra_workflow_snapshot_threadid_idx";
25607
25617
  /**
@@ -25646,13 +25656,126 @@ function workflowSnapshotThreadIdIndexSQL(indexName, schemaName, concurrently =
25646
25656
  });
25647
25657
  return `CREATE INDEX ${concurrently ? "CONCURRENTLY " : ""}IF NOT EXISTS "${indexName}" ON ${tableName} ((${WORKFLOW_SNAPSHOT_THREAD_ID_EXPR}))`;
25648
25658
  }
25659
+ /** Base name (before any schema prefix) of the index finding the `state-base` entries built on a run. */
25660
+ const WORKFLOW_RUN_ENTRIES_STATE_BASE_INDEX = "mastra_workflow_run_entries_state_base_idx";
25661
+ /** The run a `state-base` entry builds on; `#detachStateChildren` must use these exact expressions. */
25662
+ const STATE_BASE_RUN_EXPR = `(payload ->> 'workflowName'), (payload ->> 'runId')`;
25663
+ /**
25664
+ * Partial expression index finding the `state-base` entries built on a run, so deleting a run whose
25665
+ * state others build on doesn't scan every entry. See {@link workflowSnapshotStatusIndexSQL} for
25666
+ * `concurrently`.
25667
+ */
25668
+ function stateBaseIndexSQL(indexName, schemaName, concurrently = false) {
25669
+ const tableName = getTableName({
25670
+ indexName: TABLE_WORKFLOW_RUN_ENTRIES,
25671
+ schemaName: getSchemaName(schemaName)
25672
+ });
25673
+ return `CREATE INDEX ${concurrently ? "CONCURRENTLY " : ""}IF NOT EXISTS "${indexName}" ON ${tableName} (${STATE_BASE_RUN_EXPR}) WHERE kind = 'state-base'`;
25674
+ }
25675
+ /**
25676
+ * The entries a storage v2 write adds: one per step, in order, then one per step patch, then the
25677
+ * state or state base (a checkpoint with neither starts the state over at `{}`), then the state
25678
+ * patch unless a full state was given.
25679
+ */
25680
+ function journalEntries(steps, { state, stateBase, statePatch }, checkpoint, stepPatches = {}) {
25681
+ const entries = Object.entries(steps).map(([stepId, result]) => ({
25682
+ kind: "step",
25683
+ step_id: stepId,
25684
+ payload: result
25685
+ }));
25686
+ for (const [stepId, ops] of Object.entries(stepPatches)) entries.push({
25687
+ kind: "step-patch",
25688
+ step_id: stepId,
25689
+ payload: ops
25690
+ });
25691
+ if (state !== void 0) entries.push({
25692
+ kind: "state",
25693
+ step_id: null,
25694
+ payload: state
25695
+ });
25696
+ else if (stateBase !== void 0) entries.push({
25697
+ kind: "state-base",
25698
+ step_id: null,
25699
+ payload: stateBase
25700
+ });
25701
+ else if (checkpoint) entries.push({
25702
+ kind: "state",
25703
+ step_id: null,
25704
+ payload: {}
25705
+ });
25706
+ if (state === void 0 && statePatch !== void 0) entries.push({
25707
+ kind: "state-patch",
25708
+ step_id: null,
25709
+ payload: statePatch
25710
+ });
25711
+ return entries;
25712
+ }
25713
+ /**
25714
+ * Rows `e(elem, ord)` of the jsonb array parameter `param` of {@link EntryInput}, and the
25715
+ * entry columns they insert. `elem -> 'payload'` keeps a JSON null apart from an undefined
25716
+ * payload (dropped by `JSON.stringify`), which becomes SQL NULL.
25717
+ */
25718
+ const entryRows = (param) => `jsonb_array_elements(${param}::jsonb) WITH ORDINALITY AS e(elem, ord)`;
25719
+ const ENTRY_VALUES = `e.elem ->> 'kind', e.elem ->> 'step_id', e.elem -> 'payload'`;
25720
+ /** The entries of {@link EntryRow} rows; an undefined payload reads back as undefined, as in v1. */
25721
+ function toEntries(rows) {
25722
+ return rows.map((row) => {
25723
+ const payload = row.absent ? void 0 : row.payload;
25724
+ switch (row.kind) {
25725
+ case "step": return {
25726
+ seq: row.seq,
25727
+ kind: "step",
25728
+ stepId: row.step_id,
25729
+ result: payload
25730
+ };
25731
+ case "step-patch": return {
25732
+ seq: row.seq,
25733
+ kind: "step-patch",
25734
+ stepId: row.step_id,
25735
+ ops: payload
25736
+ };
25737
+ case "state": return {
25738
+ seq: row.seq,
25739
+ kind: "state",
25740
+ state: payload
25741
+ };
25742
+ case "state-patch": return {
25743
+ seq: row.seq,
25744
+ kind: "state-patch",
25745
+ ops: payload
25746
+ };
25747
+ case "state-base": return {
25748
+ seq: row.seq,
25749
+ kind: "state-base",
25750
+ base: payload
25751
+ };
25752
+ case "state-draft": {
25753
+ const draft = payload;
25754
+ return "state" in draft ? {
25755
+ seq: row.seq,
25756
+ kind: "state-draft",
25757
+ state: draft.state
25758
+ } : {
25759
+ seq: row.seq,
25760
+ kind: "state-draft",
25761
+ on: draft.on,
25762
+ ops: draft.ops
25763
+ };
25764
+ }
25765
+ }
25766
+ });
25767
+ }
25649
25768
  var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
25650
25769
  #db;
25651
25770
  #schema;
25652
25771
  #skipDefaultIndexes;
25653
25772
  #indexes;
25654
25773
  /** Tables managed by this domain */
25655
- static MANAGED_TABLES = [TABLE_WORKFLOW_SNAPSHOT];
25774
+ static MANAGED_TABLES = [
25775
+ TABLE_WORKFLOW_SNAPSHOT,
25776
+ TABLE_WORKFLOW_RUNS,
25777
+ TABLE_WORKFLOW_RUN_ENTRIES
25778
+ ];
25656
25779
  /**
25657
25780
  * Workflow run snapshots accumulate as runs execute. Anchored on the
25658
25781
  * timezone-aware `updatedAtZ` mirror column (last activity) so suspended or
@@ -25715,11 +25838,43 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
25715
25838
  };
25716
25839
  }
25717
25840
  static getDefaultIndexDefs(schemaPrefix) {
25718
- return [{
25719
- name: `${schemaPrefix}mastra_workflow_snapshot_name_createdat_idx`,
25720
- table: TABLE_WORKFLOW_SNAPSHOT,
25721
- columns: ["workflow_name", "createdAt DESC"]
25722
- }];
25841
+ return [
25842
+ {
25843
+ name: `${schemaPrefix}mastra_workflow_snapshot_name_createdat_idx`,
25844
+ table: TABLE_WORKFLOW_SNAPSHOT,
25845
+ columns: ["workflow_name", "createdAt DESC"]
25846
+ },
25847
+ {
25848
+ name: runIndexName(schemaPrefix, "mastra_workflow_runs_name_createdat_idx"),
25849
+ table: TABLE_WORKFLOW_RUNS,
25850
+ columns: ["workflow_name", "createdAt DESC"]
25851
+ },
25852
+ {
25853
+ name: runIndexName(schemaPrefix, "mastra_workflow_runs_name_status_createdat_idx"),
25854
+ table: TABLE_WORKFLOW_RUNS,
25855
+ columns: [
25856
+ "workflow_name",
25857
+ "status",
25858
+ "createdAt DESC"
25859
+ ]
25860
+ },
25861
+ {
25862
+ name: runIndexName(schemaPrefix, "mastra_workflow_runs_threadid_idx"),
25863
+ table: TABLE_WORKFLOW_RUNS,
25864
+ columns: ["threadId"]
25865
+ },
25866
+ {
25867
+ name: runIndexName(schemaPrefix, "mastra_workflow_run_entries_step_idx"),
25868
+ table: TABLE_WORKFLOW_RUN_ENTRIES,
25869
+ columns: [
25870
+ "workflow_name",
25871
+ "run_id",
25872
+ "kind",
25873
+ "step_id",
25874
+ "seq"
25875
+ ]
25876
+ }
25877
+ ];
25723
25878
  }
25724
25879
  /**
25725
25880
  * Returns all DDL statements for this domain: table with unique constraint.
@@ -25735,9 +25890,17 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
25735
25890
  schemaName,
25736
25891
  includeAllConstraints: true
25737
25892
  }));
25893
+ for (const [tableName, config] of Object.entries(WORKFLOW_RUN_JOURNAL_TABLE_CONFIGS)) statements.push(generateTableSQL({
25894
+ tableName,
25895
+ schema: config.columns,
25896
+ schemaName,
25897
+ compositePrimaryKey: config.compositePrimaryKey,
25898
+ includeAllConstraints: true
25899
+ }));
25738
25900
  for (const idx of WorkflowsPG.getDefaultIndexDefs(schemaPrefix)) statements.push(generateIndexSQL(idx, schemaName));
25739
25901
  statements.push(`${workflowSnapshotStatusIndexSQL(workflowSnapshotStatusIndexName(parsedSchema), schemaName)};`);
25740
25902
  statements.push(`${workflowSnapshotThreadIdIndexSQL(workflowSnapshotThreadIdIndexName(parsedSchema), schemaName)};`);
25903
+ statements.push(`${stateBaseIndexSQL(runIndexName(schemaPrefix, WORKFLOW_RUN_ENTRIES_STATE_BASE_INDEX), schemaName)};`);
25741
25904
  statements.push(generateTableSQL({
25742
25905
  tableName: TABLE_WORKFLOW_RUN_OWNERS,
25743
25906
  schema: RUN_FENCING_TABLE_SCHEMAS[TABLE_WORKFLOW_RUN_OWNERS],
@@ -25763,6 +25926,12 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
25763
25926
  } catch (error) {
25764
25927
  this.logger?.warn?.(`Failed to create index ${indexDef.name}:`, error);
25765
25928
  }
25929
+ const stateBaseIndexName = runIndexName(this.#schema !== "public" ? `${schemaNamePrefix(this.#schema)}_` : "", WORKFLOW_RUN_ENTRIES_STATE_BASE_INDEX);
25930
+ try {
25931
+ await this.#db.createIndexFromStatement(stateBaseIndexName, stateBaseIndexSQL(stateBaseIndexName, this.#schema, true));
25932
+ } catch (error) {
25933
+ this.logger?.warn?.(`Failed to create index ${stateBaseIndexName}:`, error);
25934
+ }
25766
25935
  if (await this.#db.getColumnType(TABLE_WORKFLOW_SNAPSHOT, "snapshot") !== "jsonb") return;
25767
25936
  const indexName = workflowSnapshotStatusIndexName(this.#schema);
25768
25937
  try {
@@ -25791,6 +25960,11 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
25791
25960
  tableName: TABLE_WORKFLOW_RUN_OWNERS,
25792
25961
  schema: RUN_FENCING_TABLE_SCHEMAS[TABLE_WORKFLOW_RUN_OWNERS]
25793
25962
  });
25963
+ for (const [tableName, config] of Object.entries(WORKFLOW_RUN_JOURNAL_TABLE_CONFIGS)) await this.#db.createTable({
25964
+ tableName,
25965
+ schema: config.columns,
25966
+ compositePrimaryKey: config.compositePrimaryKey
25967
+ });
25794
25968
  await this.createDefaultIndexes();
25795
25969
  await this.createCustomIndexes();
25796
25970
  }
@@ -25817,8 +25991,22 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
25817
25991
  this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
25818
25992
  }
25819
25993
  }
25994
+ if (!policies.workflowSnapshot) return;
25995
+ try {
25996
+ await this.#db.ensureIndex({
25997
+ indexName: `${prefix}mastra_workflow_runs_retention_idx`,
25998
+ tableName: TABLE_WORKFLOW_RUNS,
25999
+ column: "updatedAtZ"
26000
+ });
26001
+ } catch (error) {
26002
+ this.logger?.warn?.(`Failed to create retention index for ${TABLE_WORKFLOW_RUNS}:`, error);
26003
+ }
25820
26004
  }
25821
- /** Delete workflow run snapshots and run ownership rows older than their policy's `maxAge`, batched. */
26005
+ /**
26006
+ * Delete workflow runs and run ownership rows older than their policy's `maxAge`, batched.
26007
+ * The `workflowSnapshot` policy covers both storage versions: storage v2 runs are pruned by
26008
+ * their header's last write, together with their entries.
26009
+ */
25822
26010
  async prune(policies, options) {
25823
26011
  await this.ensureRetentionIndexes(policies);
25824
26012
  const targets = resolveTargets({
@@ -25826,11 +26014,40 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
25826
26014
  descriptor: WorkflowsPG.retentionTables,
25827
26015
  order: ["workflowSnapshot", "runOwnership"]
25828
26016
  });
26017
+ const snapshotTarget = targets.findIndex((target) => target.table === TABLE_WORKFLOW_SNAPSHOT);
26018
+ if (snapshotTarget !== -1) targets.splice(snapshotTarget + 1, 0, {
26019
+ ...targets[snapshotTarget],
26020
+ table: TABLE_WORKFLOW_RUNS
26021
+ });
25829
26022
  return runPrune({
25830
26023
  db: this.#db,
25831
26024
  domain: "workflows",
25832
26025
  targets,
25833
- options
26026
+ options,
26027
+ deleteBatch: (target, cutoff, limit) => target.table === TABLE_WORKFLOW_RUNS ? this.#pruneRunsBatch(cutoff, limit) : void 0
26028
+ });
26029
+ }
26030
+ /**
26031
+ * Deletes up to `limit` aged storage v2 runs, header and entries together, first detaching the
26032
+ * runs whose state others build on (see `#detachStateChildren`).
26033
+ */
26034
+ async #pruneRunsBatch(cutoff, limit) {
26035
+ return this.#db.client.tx(async (t) => {
26036
+ const doomed = await t.manyOrNone(`SELECT workflow_name, run_id, "stateShared" FROM ${this.#runsTable()}
26037
+ WHERE "updatedAtZ" < $1
26038
+ ORDER BY "updatedAtZ"
26039
+ LIMIT $2
26040
+ FOR UPDATE SKIP LOCKED`, [cutoff, limit]);
26041
+ if (!doomed.length) return 0;
26042
+ for (const run of doomed) if (run.stateShared) await this.#detachStateChildren(t, run.workflow_name, run.run_id);
26043
+ return (await t.query(`WITH doomed AS (
26044
+ SELECT * FROM unnest($1::text[], $2::text[]) AS d(workflow_name, run_id)
26045
+ ), entries AS (
26046
+ DELETE FROM ${this.#entriesTable()} e USING doomed d
26047
+ WHERE e.workflow_name = d.workflow_name AND e.run_id = d.run_id
26048
+ )
26049
+ DELETE FROM ${this.#runsTable()} r USING doomed d
26050
+ 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;
25834
26051
  });
25835
26052
  }
25836
26053
  /**
@@ -25847,6 +26064,459 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
25847
26064
  async dangerouslyClearAll() {
25848
26065
  await this.#db.clearTable({ tableName: TABLE_WORKFLOW_SNAPSHOT });
25849
26066
  await this.#db.clearTable({ tableName: TABLE_WORKFLOW_RUN_OWNERS });
26067
+ await this.#db.clearTable({ tableName: TABLE_WORKFLOW_RUNS });
26068
+ await this.#db.clearTable({ tableName: TABLE_WORKFLOW_RUN_ENTRIES });
26069
+ }
26070
+ supportsRunJournal() {
26071
+ return true;
26072
+ }
26073
+ #runsTable() {
26074
+ return getTableName({
26075
+ indexName: TABLE_WORKFLOW_RUNS,
26076
+ schemaName: getSchemaName(this.#schema)
26077
+ });
26078
+ }
26079
+ #entriesTable() {
26080
+ return getTableName({
26081
+ indexName: TABLE_WORKFLOW_RUN_ENTRIES,
26082
+ schemaName: getSchemaName(this.#schema)
26083
+ });
26084
+ }
26085
+ /**
26086
+ * A jsonb array of {@link EntryRow} for the storage v2 run `alias` refers to: each step's
26087
+ * latest entry, ordered by when each step was first written so `context` keys keep their write
26088
+ * order, the step patches after it (in `seq` order, for `latestStepResults`), and the entries
26089
+ * `materializeState` uses for the latest state: the latest full state or state base and every
26090
+ * state patch after it, in `seq` order. Drafts are left out.
26091
+ *
26092
+ * With `summaryFieldsParam` (the parameter holding `STEP_RESULT_SUMMARY_FIELDS`), successful
26093
+ * steps other than `input` come back as their summary, built here so their data is never sent,
26094
+ * unless step patches follow it (the caller summarizes the patched result instead).
26095
+ * Like `summarizeStepResult`, it keeps a field that is JSON null and leaves out one that is absent.
26096
+ */
26097
+ #entriesSql(alias, { summaryFieldsParam } = {}) {
26098
+ let payload = "x.payload";
26099
+ if (summaryFieldsParam) {
26100
+ const summarized = `(x.kind = 'step' AND x.step_id <> 'input' AND x.payload ->> 'status' = 'success'
26101
+ AND (x.patch_seq IS NULL OR x.patch_seq < x.seq))`;
26102
+ payload = `CASE WHEN ${summarized} THEN (
26103
+ SELECT COALESCE(jsonb_object_agg(f.key, f.value), '{}'::jsonb)
26104
+ FROM jsonb_each(x.payload) f WHERE f.key = ANY(${summaryFieldsParam}::text[])
26105
+ ) ELSE x.payload END, 'summarized', ${summarized}`;
26106
+ }
26107
+ return `(SELECT COALESCE(jsonb_agg(jsonb_build_object(
26108
+ 'seq', x.seq, 'kind', x.kind, 'step_id', x.step_id, 'absent', x.payload IS NULL, 'payload', ${payload}
26109
+ ) ORDER BY CASE WHEN x.kind = 'step' THEN x.first_seq ELSE x.seq END), '[]'::jsonb)
26110
+ FROM (
26111
+ SELECT e.seq, e.kind, e.step_id, e.payload,
26112
+ ROW_NUMBER() OVER (PARTITION BY e.kind, e.step_id ORDER BY e.seq DESC) AS rn,
26113
+ MIN(e.seq) OVER (PARTITION BY e.kind, e.step_id) AS first_seq,
26114
+ MAX(CASE WHEN e.kind = 'step' THEN e.seq END) OVER (PARTITION BY e.step_id) AS step_seq,
26115
+ MAX(CASE WHEN e.kind = 'step-patch' THEN e.seq END) OVER (PARTITION BY e.step_id) AS patch_seq,
26116
+ MAX(CASE WHEN e.kind IN ('state', 'state-base') THEN e.seq END) OVER () AS base_seq
26117
+ FROM ${this.#entriesTable()} e
26118
+ WHERE e.workflow_name = ${alias}.workflow_name AND e.run_id = ${alias}.run_id AND e.kind <> 'state-draft'
26119
+ ) x
26120
+ WHERE CASE x.kind
26121
+ WHEN 'step' THEN x.rn = 1
26122
+ WHEN 'step-patch' THEN x.seq > x.step_seq
26123
+ ELSE x.base_seq IS NULL OR x.seq >= x.base_seq
26124
+ END)`;
26125
+ }
26126
+ /**
26127
+ * A jsonb array of {@link EntryRow} with the entries `materializeState` uses for the state of the
26128
+ * storage v2 run `alias` refers to as of a version: `seqParam` its `seq`, `draftParam` whether
26129
+ * it is a draft. For a draft, every state entry up to it, as the drafts it is on can build on
26130
+ * any earlier version; otherwise the latest full state or state base up to it and the state
26131
+ * patches after it.
26132
+ */
26133
+ #stateEntriesSql(alias, seqParam, draftParam) {
26134
+ return `(SELECT COALESCE(jsonb_agg(jsonb_build_object(
26135
+ 'seq', x.seq, 'kind', x.kind, 'step_id', x.step_id, 'absent', x.payload IS NULL, 'payload', x.payload
26136
+ ) ORDER BY x.seq), '[]'::jsonb)
26137
+ FROM (
26138
+ SELECT e.seq, e.kind, e.step_id, e.payload,
26139
+ MAX(CASE WHEN e.kind IN ('state', 'state-base') THEN e.seq END) OVER () AS base_seq
26140
+ FROM ${this.#entriesTable()} e
26141
+ WHERE e.workflow_name = ${alias}.workflow_name AND e.run_id = ${alias}.run_id
26142
+ AND e.kind NOT IN ('step', 'step-patch') AND e.seq <= ${seqParam}::int
26143
+ ) x
26144
+ WHERE ${draftParam}::boolean OR (x.kind <> 'state-draft' AND (x.base_seq IS NULL OR x.seq >= x.base_seq)))`;
26145
+ }
26146
+ /**
26147
+ * Selects v1 snapshot rows and storage v2 runs as one result, newest first. Each version
26148
+ * has its own WHERE clause; `null` leaves it out. Storage v2 rows carry their header and
26149
+ * entries, or in summary mode only the summary snapshot. `page` is appended to the
26150
+ * combined, ordered rows (e.g. `LIMIT 1`).
26151
+ */
26152
+ async #runsQuery({ v1Where, v2Where, v1Snapshot = "snapshot", summary, page = "" }) {
26153
+ const branches = [];
26154
+ if (v1Where !== null) {
26155
+ const optional = {
26156
+ resourceId: "text",
26157
+ createdAtZ: "timestamptz",
26158
+ updatedAtZ: "timestamptz"
26159
+ };
26160
+ const columns = {};
26161
+ for (const [column, type] of Object.entries(optional)) columns[column] = await this.#db.getColumnType(TABLE_WORKFLOW_SNAPSHOT, column) !== null ? `"${column}"` : `NULL::${type} AS "${column}"`;
26162
+ branches.push(`SELECT workflow_name, run_id, ${columns.resourceId}, ${v1Snapshot} AS snapshot, NULL::jsonb AS header,
26163
+ "createdAt", ${columns.createdAtZ}, "updatedAt", ${columns.updatedAtZ}, 1 AS version
26164
+ FROM ${getTableName({
26165
+ indexName: TABLE_WORKFLOW_SNAPSHOT,
26166
+ schemaName: getSchemaName(this.#schema)
26167
+ })} ${v1Where}`);
26168
+ }
26169
+ if (v2Where !== null) {
26170
+ const columns = summary ? `jsonb_build_object('status', header -> 'status', 'timestamp', header -> 'timestamp') AS snapshot, NULL::jsonb AS header` : `NULL AS snapshot, header`;
26171
+ branches.push(`SELECT workflow_name, run_id, "resourceId", ${columns},
26172
+ "createdAt", "createdAtZ", "updatedAt", "updatedAtZ", 2 AS version
26173
+ FROM ${this.#runsTable()} ${v2Where}`);
26174
+ }
26175
+ return `SELECT p.*${summary ? "" : `, CASE WHEN p.version = 2 THEN ${this.#entriesSql("p")} END AS entries`}
26176
+ FROM (${branches.join(" UNION ALL ")} ORDER BY "createdAt" DESC ${page}) p
26177
+ ORDER BY p."createdAt" DESC`;
26178
+ }
26179
+ /**
26180
+ * The `base` that `materializeState` takes for a run's entries (as of `at`, or latest): the state
26181
+ * version of another run it builds on (`stateBaseOf`), resolved recursively, else undefined. `{}`
26182
+ * when that run isn't stored. One query per run in the chain.
26183
+ */
26184
+ async #stateBase(q, entries, at) {
26185
+ const ref = stateBaseOf(entries, at);
26186
+ if (!ref) return void 0;
26187
+ return (await this.#stateAt(q, ref.workflowName, ref.runId, ref))?.state ?? {};
26188
+ }
26189
+ /** A storage v2 run's state as of the version `at`, or null when the run isn't stored. */
26190
+ async #stateAt(q, workflowName, runId, at) {
26191
+ const row = await q.oneOrNone(`SELECT ${this.#stateEntriesSql("r", "$3", "$4")} AS entries
26192
+ FROM ${this.#runsTable()} r WHERE r.workflow_name = $1 AND r.run_id = $2`, [
26193
+ workflowName,
26194
+ runId,
26195
+ at.seq,
26196
+ at.draft === true
26197
+ ]);
26198
+ if (!row) return null;
26199
+ const entries = toEntries(row.entries);
26200
+ return materializeState(entries, {
26201
+ at,
26202
+ base: await this.#stateBase(q, entries, at)
26203
+ });
26204
+ }
26205
+ /**
26206
+ * Before a run is deleted, stores the full state in place of every `state-base` entry built on
26207
+ * it, so the runs built on it keep reading the same state. Runs inside the delete's transaction.
26208
+ */
26209
+ async #detachStateChildren(q, workflowName, runId) {
26210
+ const children = await q.manyOrNone(`SELECT workflow_name, run_id, seq, payload FROM ${this.#entriesTable()}
26211
+ WHERE kind = 'state-base' AND (${STATE_BASE_RUN_EXPR}) = ($1, $2)`, [workflowName, runId]);
26212
+ const states = /* @__PURE__ */ new Map();
26213
+ for (const child of children) {
26214
+ const version = `${child.payload.seq}:${child.payload.draft === true}`;
26215
+ let state = states.get(version);
26216
+ if (!state) {
26217
+ state = (await this.#stateAt(q, workflowName, runId, child.payload))?.state ?? {};
26218
+ states.set(version, state);
26219
+ }
26220
+ await q.none(`UPDATE ${this.#entriesTable()} SET kind = 'state', payload = $4::jsonb
26221
+ WHERE workflow_name = $1 AND run_id = $2 AND seq = $3`, [
26222
+ child.workflow_name,
26223
+ child.run_id,
26224
+ child.seq,
26225
+ toPgJson(state)
26226
+ ]);
26227
+ }
26228
+ }
26229
+ /** Rebuilds a storage v2 run's `WorkflowRunState` from its header and `#entriesSql` rows. */
26230
+ async #materializeRun(client, header, rows) {
26231
+ const entries = toEntries(rows);
26232
+ return materializeWorkflowRun(header, entries, await this.#stateBase(client, entries));
26233
+ }
26234
+ /** Parses a row of `#runsQuery`. */
26235
+ async #parseRun(client, row) {
26236
+ if (row.version !== 2) return this.parseWorkflowRun(row);
26237
+ const snapshot = row.entries ? await this.#materializeRun(client, row.header, row.entries) : row.snapshot;
26238
+ return {
26239
+ ...this.parseWorkflowRun({
26240
+ ...row,
26241
+ snapshot
26242
+ }),
26243
+ storageVersion: 2
26244
+ };
26245
+ }
26246
+ #runError(operation, workflowName, runId, error) {
26247
+ return new MastraError({
26248
+ id: createStorageErrorId("PG", operation, "FAILED"),
26249
+ domain: ErrorDomain.STORAGE,
26250
+ category: ErrorCategory.THIRD_PARTY,
26251
+ details: {
26252
+ workflowName,
26253
+ runId
26254
+ }
26255
+ }, error);
26256
+ }
26257
+ async loadWorkflowRunHeader({ workflowName, runId }) {
26258
+ try {
26259
+ return await this.#db.client.oneOrNone(`SELECT header, "lastSeq", "startStateSeq", "runToken" FROM ${this.#runsTable()} WHERE workflow_name = $1 AND run_id = $2`, [workflowName, runId]);
26260
+ } catch (error) {
26261
+ throw this.#runError("LOAD_WORKFLOW_RUN_HEADER", workflowName, runId, error);
26262
+ }
26263
+ }
26264
+ async loadWorkflowRun({ workflowName, runId, successfulSteps }) {
26265
+ try {
26266
+ const summary = successfulSteps === "summary";
26267
+ 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
26268
+ FROM ${this.#runsTable()} r WHERE r.workflow_name = $1 AND r.run_id = $2`, summary ? [
26269
+ workflowName,
26270
+ runId,
26271
+ STEP_RESULT_SUMMARY_FIELDS
26272
+ ] : [workflowName, runId]);
26273
+ if (!run) return null;
26274
+ const entries = toEntries(run.entries);
26275
+ const summarizedInQuery = new Set(run.entries.filter((row) => row.summarized).map((row) => row.step_id));
26276
+ const steps = {};
26277
+ const summarizedStepIds = [];
26278
+ for (const [stepId, result] of latestStepResults(entries)) {
26279
+ steps[stepId] = result;
26280
+ if (summary && stepId !== "input" && result?.status === "success") {
26281
+ if (!summarizedInQuery.has(stepId)) steps[stepId] = summarizeStepResult(result);
26282
+ summarizedStepIds.push(stepId);
26283
+ }
26284
+ }
26285
+ const base = await this.#stateBase(this.#db.client, entries);
26286
+ const { state, statePatchBytes } = materializeState(entries, { base });
26287
+ return {
26288
+ header: run.header,
26289
+ lastSeq: run.lastSeq,
26290
+ startStateSeq: run.startStateSeq,
26291
+ runToken: run.runToken,
26292
+ state,
26293
+ statePatchBytes,
26294
+ steps,
26295
+ summarizedStepIds
26296
+ };
26297
+ } catch (error) {
26298
+ throw this.#runError("LOAD_WORKFLOW_RUN", workflowName, runId, error);
26299
+ }
26300
+ }
26301
+ async loadWorkflowRunSteps({ workflowName, runId, stepIds, expectedLastSeq, expectedRunToken }) {
26302
+ try {
26303
+ const run = await this.#db.client.oneOrNone(`SELECT r."lastSeq", r."runToken", (
26304
+ SELECT COALESCE(jsonb_agg(jsonb_build_object(
26305
+ 'seq', x.seq, 'kind', x.kind, 'step_id', x.step_id, 'absent', x.payload IS NULL, 'payload', x.payload
26306
+ ) ORDER BY x.seq), '[]'::jsonb)
26307
+ FROM (
26308
+ SELECT e.seq, e.kind, e.step_id, e.payload,
26309
+ MAX(CASE WHEN e.kind = 'step' THEN e.seq END) OVER (PARTITION BY e.step_id) AS step_seq
26310
+ FROM ${this.#entriesTable()} e
26311
+ WHERE e.workflow_name = r.workflow_name AND e.run_id = r.run_id
26312
+ AND e.kind IN ('step', 'step-patch') AND e.step_id = ANY($3::text[])
26313
+ ) x
26314
+ WHERE x.seq >= x.step_seq
26315
+ ) AS entries
26316
+ FROM ${this.#runsTable()} r WHERE r.workflow_name = $1 AND r.run_id = $2`, [
26317
+ workflowName,
26318
+ runId,
26319
+ stepIds
26320
+ ]);
26321
+ if (!run) return null;
26322
+ if (expectedLastSeq !== void 0 && run.lastSeq !== expectedLastSeq) return null;
26323
+ if (expectedRunToken !== void 0 && run.runToken !== expectedRunToken) return null;
26324
+ return Object.fromEntries(latestStepResults(toEntries(run.entries)));
26325
+ } catch (error) {
26326
+ throw this.#runError("LOAD_WORKFLOW_RUN_STEPS", workflowName, runId, error);
26327
+ }
26328
+ }
26329
+ async updateWorkflowRunHeader({ workflowName, runId, opts, fence: explicitFence }) {
26330
+ const fence = resolveRunFence(this, explicitFence, runId);
26331
+ try {
26332
+ return await this.#db.client.tx(async (t) => {
26333
+ if (fence) await assertRunFence(t, this.#runOwnersTable(), fence, "updateWorkflowRunHeader");
26334
+ const run = await t.oneOrNone(`SELECT header FROM ${this.#runsTable()} WHERE workflow_name = $1 AND run_id = $2 FOR UPDATE`, [workflowName, runId]);
26335
+ if (!run) return void 0;
26336
+ const { expectedStatus, ...patch } = opts;
26337
+ if (!matchesExpectedWorkflowStatus(run.header.status, expectedStatus)) return void 0;
26338
+ const header = {
26339
+ ...run.header,
26340
+ ...patch
26341
+ };
26342
+ const now = /* @__PURE__ */ new Date();
26343
+ return await t.one(`UPDATE ${this.#runsTable()}
26344
+ SET header = $3::jsonb, status = $4, "updatedAt" = $5, "updatedAtZ" = $6
26345
+ WHERE workflow_name = $1 AND run_id = $2
26346
+ RETURNING header, "lastSeq", "startStateSeq", "runToken"`, [
26347
+ workflowName,
26348
+ runId,
26349
+ toPgJson(header),
26350
+ header.status,
26351
+ now,
26352
+ now
26353
+ ]);
26354
+ });
26355
+ } catch (error) {
26356
+ if (isRunFenceConflictError(error)) throw error;
26357
+ throw this.#runError("UPDATE_WORKFLOW_RUN_HEADER", workflowName, runId, error);
26358
+ }
26359
+ }
26360
+ /**
26361
+ * Upserts a storage v2 run's header from `$1`–`$12` ({@link #headerParams}), adds `$7` (the
26362
+ * number of entries the write adds) to `lastSeq`, and returns the new `lastSeq`, `stateShared`
26363
+ * and `runToken`. `$12` is the run token of a run the write creates; a stored run keeps its own.
26364
+ * A checkpoint's state entries come last, so its `startStateSeq` is the new `lastSeq`; an append
26365
+ * keeps the stored one unless given `$13`. The header's row lock is held
26366
+ * until commit, so the writes to one run commit in `seq` order. With `expectedParam` (a text
26367
+ * array of statuses), a stored run whose status isn't in it is left as is and nothing is returned.
26368
+ */
26369
+ #upsertHeaderSql(checkpoint, expectedParam) {
26370
+ const startStateSeq = checkpoint ? {
26371
+ insert: "$7::int",
26372
+ update: "r.\"lastSeq\" + EXCLUDED.\"lastSeq\""
26373
+ } : {
26374
+ insert: "COALESCE($13::int, 0)",
26375
+ update: "COALESCE($13::int, r.\"startStateSeq\")"
26376
+ };
26377
+ return `INSERT INTO ${this.#runsTable()} AS r
26378
+ (workflow_name, run_id, "resourceId", "threadId", status, header, "lastSeq", "startStateSeq", "stateShared",
26379
+ "runToken", "createdAt", "updatedAt", "createdAtZ", "updatedAtZ")
26380
+ VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7::int, ${startStateSeq.insert}, false, $12, $8, $9, $10, $11)
26381
+ ON CONFLICT (workflow_name, run_id) DO UPDATE
26382
+ SET "resourceId" = COALESCE(EXCLUDED."resourceId", r."resourceId"), "threadId" = EXCLUDED."threadId",
26383
+ status = EXCLUDED.status, header = EXCLUDED.header, "lastSeq" = r."lastSeq" + EXCLUDED."lastSeq",
26384
+ "startStateSeq" = ${startStateSeq.update},
26385
+ "updatedAt" = EXCLUDED."updatedAt", "updatedAtZ" = EXCLUDED."updatedAtZ"
26386
+ ${expectedParam ? `WHERE r.status = ANY(${expectedParam}::text[])` : ""}
26387
+ RETURNING "lastSeq", "stateShared", "runToken"`;
26388
+ }
26389
+ #headerParams(run, entryCount) {
26390
+ const now = /* @__PURE__ */ new Date();
26391
+ return [
26392
+ run.workflowName,
26393
+ run.runId,
26394
+ run.resourceId,
26395
+ run.threadId ?? null,
26396
+ run.header.status,
26397
+ toPgJson(run.header),
26398
+ entryCount,
26399
+ now,
26400
+ now,
26401
+ now,
26402
+ now,
26403
+ crypto.randomUUID()
26404
+ ];
26405
+ }
26406
+ async writeWorkflowRunCheckpoint({ fence: explicitFence, expectedStatus, steps, state, stateBase, statePatch, ...run }) {
26407
+ const entries = journalEntries(steps, {
26408
+ state,
26409
+ stateBase,
26410
+ statePatch
26411
+ }, true);
26412
+ const fence = resolveRunFence(this, explicitFence, run.runId);
26413
+ const expected = expectedStatus === void 0 ? [] : [[expectedStatus].flat()];
26414
+ try {
26415
+ return await this.#db.client.tx(async (t) => {
26416
+ if (fence) await assertRunFence(t, this.#runOwnersTable(), fence, "writeWorkflowRunCheckpoint");
26417
+ const base = state === void 0 ? stateBase : void 0;
26418
+ let baseLastSeq;
26419
+ 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;
26420
+ const header = await t.oneOrNone(this.#upsertHeaderSql(true, expected.length ? "$13" : void 0), [...this.#headerParams(run, entries.length), ...expected]);
26421
+ if (!header) return null;
26422
+ if (base) {
26423
+ await t.none(`UPDATE ${this.#runsTable()} SET "stateShared" = true
26424
+ WHERE workflow_name = $1 AND run_id = $2 AND NOT "stateShared"`, [base.workflowName, base.runId]);
26425
+ const { stored } = await t.one(`SELECT EXISTS (
26426
+ SELECT 1 FROM ${this.#entriesTable()} WHERE workflow_name = $1 AND run_id = $2
26427
+ AND CASE WHEN $4::boolean THEN kind = 'state-draft' AND seq = $3::int
26428
+ ELSE kind IN ('state', 'state-base') AND seq <= $3::int END
26429
+ ) AS stored`, [
26430
+ base.workflowName,
26431
+ base.runId,
26432
+ base.seq,
26433
+ base.draft === true
26434
+ ]);
26435
+ 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`);
26436
+ }
26437
+ const { lastSeq, stateShared, runToken } = header;
26438
+ await t.none(`DELETE FROM ${this.#entriesTable()}
26439
+ WHERE workflow_name = $1 AND run_id = $2${stateShared ? ` AND kind IN ('step', 'step-patch')` : ""}`, [run.workflowName, run.runId]);
26440
+ await t.none(`INSERT INTO ${this.#entriesTable()} (workflow_name, run_id, seq, kind, step_id, payload)
26441
+ SELECT $1, $2, $3::int - $4::int + e.ord, ${ENTRY_VALUES}
26442
+ FROM ${entryRows("$5")}`, [
26443
+ run.workflowName,
26444
+ run.runId,
26445
+ lastSeq,
26446
+ entries.length,
26447
+ toPgJson(entries)
26448
+ ]);
26449
+ return {
26450
+ lastSeq,
26451
+ runToken
26452
+ };
26453
+ });
26454
+ } catch (error) {
26455
+ if (isRunFenceConflictError(error)) throw error;
26456
+ throw this.#runError("WRITE_WORKFLOW_RUN_CHECKPOINT", run.workflowName, run.runId, error);
26457
+ }
26458
+ }
26459
+ /** One statement: the header upsert numbers the entries inserted after it. */
26460
+ async appendWorkflowRunEntries({ fence, steps, stepPatches, state, statePatch, startStateSeq, expectedStatus, ...run }) {
26461
+ const entries = journalEntries(steps, {
26462
+ state,
26463
+ statePatch
26464
+ }, false, stepPatches);
26465
+ const expected = expectedStatus === void 0 ? [] : [[expectedStatus].flat()];
26466
+ try {
26467
+ return await withRunFence(this.#db.client, this.#runOwnersTable(), resolveRunFence(this, fence, run.runId), "appendWorkflowRunEntries", (q) => q.oneOrNone(`WITH h AS (${this.#upsertHeaderSql(false, expected.length ? "$15" : void 0)}),
26468
+ e AS (
26469
+ INSERT INTO ${this.#entriesTable()} (workflow_name, run_id, seq, kind, step_id, payload)
26470
+ SELECT $1, $2, h."lastSeq" - $7::int + e.ord, ${ENTRY_VALUES}
26471
+ FROM h, ${entryRows("$14")}
26472
+ )
26473
+ SELECT "lastSeq", "runToken" FROM h`, [
26474
+ ...this.#headerParams(run, entries.length),
26475
+ startStateSeq ?? null,
26476
+ toPgJson(entries),
26477
+ ...expected
26478
+ ]));
26479
+ } catch (error) {
26480
+ if (isRunFenceConflictError(error)) throw error;
26481
+ throw this.#runError("APPEND_WORKFLOW_RUN_ENTRIES", run.workflowName, run.runId, error);
26482
+ }
26483
+ }
26484
+ async loadWorkflowRunState({ workflowName, runId, seq }) {
26485
+ try {
26486
+ return (await this.#stateAt(this.#db.client, workflowName, runId, { seq }))?.state ?? null;
26487
+ } catch (error) {
26488
+ throw this.#runError("LOAD_WORKFLOW_RUN_STATE", workflowName, runId, error);
26489
+ }
26490
+ }
26491
+ async appendWorkflowRunStateDraft(args) {
26492
+ const { workflowName, runId } = args;
26493
+ const payload = "state" in args ? { state: args.state } : {
26494
+ on: args.on.draft ? {
26495
+ seq: args.on.seq,
26496
+ draft: true
26497
+ } : { seq: args.on.seq },
26498
+ ops: args.ops
26499
+ };
26500
+ let row;
26501
+ try {
26502
+ row = await withRunFence(this.#db.client, this.#runOwnersTable(), resolveRunFence(this, void 0, runId), "appendWorkflowRunStateDraft", (q) => q.oneOrNone(`WITH h AS (
26503
+ UPDATE ${this.#runsTable()} SET "lastSeq" = "lastSeq" + 1, "stateShared" = true
26504
+ WHERE workflow_name = $1 AND run_id = $2
26505
+ RETURNING "lastSeq"
26506
+ )
26507
+ INSERT INTO ${this.#entriesTable()} (workflow_name, run_id, seq, kind, step_id, payload)
26508
+ SELECT $1, $2, h."lastSeq", 'state-draft', NULL, $3::jsonb FROM h
26509
+ RETURNING seq`, [
26510
+ workflowName,
26511
+ runId,
26512
+ toPgJson(payload)
26513
+ ]));
26514
+ } catch (error) {
26515
+ if (isRunFenceConflictError(error)) throw error;
26516
+ throw this.#runError("APPEND_WORKFLOW_RUN_STATE_DRAFT", workflowName, runId, error);
26517
+ }
26518
+ if (!row) throw new Error(`Storage v2 run ${workflowName}/${runId} is not stored`);
26519
+ return row;
25850
26520
  }
25851
26521
  supportsRunFencing() {
25852
26522
  return true;
@@ -25975,6 +26645,7 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
25975
26645
  });
25976
26646
  const existingSnapshotResult = await t.oneOrNone(`SELECT snapshot FROM ${tableName} WHERE workflow_name = $1 AND run_id = $2 FOR UPDATE`, [workflowName, runId]);
25977
26647
  if (!existingSnapshotResult) {
26648
+ 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})`);
25978
26649
  assertWorkflowStepGuard({
25979
26650
  snapshot: null,
25980
26651
  stepId,
@@ -26047,11 +26718,36 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
26047
26718
  }, error);
26048
26719
  }
26049
26720
  }
26050
- async updateWorkflowState({ workflowName, runId, opts, fence: explicitFence }) {
26721
+ async updateWorkflowState({ workflowName, runId, opts, fence: explicitFence, storageVersion }) {
26051
26722
  const fence = resolveRunFence(this, explicitFence, runId);
26052
26723
  try {
26053
26724
  return await this.#db.client.tx(async (t) => {
26054
26725
  if (fence) await assertRunFence(t, this.#runOwnersTable(), fence, "updateWorkflowState");
26726
+ if (storageVersion !== 1) {
26727
+ const run = await t.oneOrNone(`SELECT header FROM ${this.#runsTable()} WHERE workflow_name = $1 AND run_id = $2 FOR UPDATE`, [workflowName, runId]);
26728
+ if (run) {
26729
+ const { expectedStatus, ...patch } = opts;
26730
+ if (!matchesExpectedWorkflowStatus(run.header.status, expectedStatus)) return;
26731
+ const header = {
26732
+ ...run.header,
26733
+ ...patch
26734
+ };
26735
+ const now = /* @__PURE__ */ new Date();
26736
+ const { entries } = await t.one(`UPDATE ${this.#runsTable()} r
26737
+ SET header = $3::jsonb, status = $4, "updatedAt" = $5, "updatedAtZ" = $6
26738
+ WHERE r.workflow_name = $1 AND r.run_id = $2
26739
+ RETURNING ${this.#entriesSql("r")} AS entries`, [
26740
+ workflowName,
26741
+ runId,
26742
+ toPgJson(header),
26743
+ header.status,
26744
+ now,
26745
+ now
26746
+ ]);
26747
+ return this.#materializeRun(t, header, entries);
26748
+ }
26749
+ if (storageVersion === 2) return void 0;
26750
+ }
26055
26751
  const tableName = getTableName({
26056
26752
  indexName: TABLE_WORKFLOW_SNAPSHOT,
26057
26753
  schemaName: getSchemaName(this.#schema)
@@ -26131,10 +26827,14 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
26131
26827
  async loadWorkflowRunStatus({ workflowName, runId }) {
26132
26828
  try {
26133
26829
  const statusExpr = await this.#statusExpression("snapshot");
26134
- return (await this.#db.client.oneOrNone(`SELECT ${statusExpr} AS status FROM ${getTableName({
26830
+ return (await this.#db.client.oneOrNone(`SELECT COALESCE(
26831
+ (SELECT status FROM ${this.#runsTable()} WHERE workflow_name = $1 AND run_id = $2),
26832
+ (SELECT ${statusExpr} FROM ${getTableName({
26135
26833
  indexName: TABLE_WORKFLOW_SNAPSHOT,
26136
26834
  schemaName: getSchemaName(this.#schema)
26137
- })} WHERE "workflow_name" = $1 AND "run_id" = $2 ORDER BY "createdAt" DESC LIMIT 1`, [workflowName, runId]))?.status ?? null;
26835
+ })}
26836
+ WHERE "workflow_name" = $1 AND "run_id" = $2 ORDER BY "createdAt" DESC LIMIT 1)
26837
+ ) AS status`, [workflowName, runId]))?.status ?? null;
26138
26838
  } catch (error) {
26139
26839
  throw new MastraError({
26140
26840
  id: createStorageErrorId("PG", "LOAD_WORKFLOW_RUN_STATUS", "FAILED"),
@@ -26147,8 +26847,14 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
26147
26847
  }, error);
26148
26848
  }
26149
26849
  }
26150
- async loadWorkflowSnapshot({ workflowName, runId }) {
26850
+ async loadWorkflowSnapshot({ workflowName, runId, storageVersion }) {
26151
26851
  try {
26852
+ if (storageVersion !== 1) {
26853
+ const run = await this.#db.client.oneOrNone(`SELECT r.header, ${this.#entriesSql("r")} AS entries
26854
+ FROM ${this.#runsTable()} r WHERE r.workflow_name = $1 AND r.run_id = $2`, [workflowName, runId]);
26855
+ if (run) return await this.#materializeRun(this.#db.client, run.header, run.entries);
26856
+ if (storageVersion === 2) return null;
26857
+ }
26152
26858
  const result = await this.#db.load({
26153
26859
  tableName: TABLE_WORKFLOW_SNAPSHOT,
26154
26860
  keys: {
@@ -26165,7 +26871,7 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
26165
26871
  }, error);
26166
26872
  }
26167
26873
  }
26168
- async getWorkflowRunById({ runId, workflowName }) {
26874
+ async getWorkflowRunById({ runId, workflowName, storageVersion }) {
26169
26875
  try {
26170
26876
  const conditions = [];
26171
26877
  const values = [];
@@ -26181,18 +26887,14 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
26181
26887
  paramIndex++;
26182
26888
  }
26183
26889
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
26184
- const query = `
26185
- SELECT * FROM ${getTableName({
26186
- indexName: TABLE_WORKFLOW_SNAPSHOT,
26187
- schemaName: getSchemaName(this.#schema)
26188
- })}
26189
- ${whereClause}
26190
- ORDER BY "createdAt" DESC LIMIT 1
26191
- `;
26192
- const queryValues = values;
26193
- const result = await this.#db.readClient.oneOrNone(query, queryValues);
26890
+ const query = await this.#runsQuery({
26891
+ v1Where: storageVersion === 2 ? null : whereClause,
26892
+ v2Where: storageVersion === 1 ? null : whereClause,
26893
+ page: "LIMIT 1"
26894
+ });
26895
+ const result = await this.#db.readClient.oneOrNone(query, values);
26194
26896
  if (!result) return null;
26195
- return this.parseWorkflowRun(result);
26897
+ return await this.#parseRun(this.#db.readClient, result);
26196
26898
  } catch (error) {
26197
26899
  throw new MastraError({
26198
26900
  id: createStorageErrorId("PG", "GET_WORKFLOW_RUN_BY_ID", "FAILED"),
@@ -26205,12 +26907,28 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
26205
26907
  }, error);
26206
26908
  }
26207
26909
  }
26208
- async deleteWorkflowRunById({ runId, workflowName, fence }) {
26910
+ async deleteWorkflowRunById({ runId, workflowName, storageVersion, fence }) {
26911
+ const resolvedFence = resolveRunFence(this, fence, runId);
26209
26912
  try {
26210
- await withRunFence(this.#db.client, this.#runOwnersTable(), resolveRunFence(this, fence, runId), "deleteWorkflowRunById", (q) => q.none(`DELETE FROM ${getTableName({
26211
- indexName: TABLE_WORKFLOW_SNAPSHOT,
26212
- schemaName: getSchemaName(this.#schema)
26213
- })} WHERE run_id = $1 AND workflow_name = $2`, [runId, workflowName]));
26913
+ if (storageVersion === 1) {
26914
+ await withRunFence(this.#db.client, this.#runOwnersTable(), resolvedFence, "deleteWorkflowRunById", (q) => q.none(`DELETE FROM ${getTableName({
26915
+ indexName: TABLE_WORKFLOW_SNAPSHOT,
26916
+ schemaName: getSchemaName(this.#schema)
26917
+ })} WHERE run_id = $1 AND workflow_name = $2`, [runId, workflowName]));
26918
+ return;
26919
+ }
26920
+ await this.#db.client.tx(async (t) => {
26921
+ if (resolvedFence) await assertRunFence(t, this.#runOwnersTable(), resolvedFence, "deleteWorkflowRunById");
26922
+ 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);
26923
+ for (const table of [
26924
+ this.#runsTable(),
26925
+ this.#entriesTable(),
26926
+ ...storageVersion === 2 ? [] : [getTableName({
26927
+ indexName: TABLE_WORKFLOW_SNAPSHOT,
26928
+ schemaName: getSchemaName(this.#schema)
26929
+ })]
26930
+ ]) await t.none(`DELETE FROM ${table} WHERE run_id = $1 AND workflow_name = $2`, [runId, workflowName]);
26931
+ });
26214
26932
  } catch (error) {
26215
26933
  if (isRunFenceConflictError(error)) throw error;
26216
26934
  throw new MastraError({
@@ -26227,77 +26945,81 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
26227
26945
  async listWorkflowRuns({ workflowName, fromDate, toDate, perPage, page, resourceId, threadId, status, summary } = {}) {
26228
26946
  try {
26229
26947
  const conditions = [];
26948
+ const v2Conditions = [];
26230
26949
  const values = [];
26231
26950
  let paramIndex = 1;
26232
26951
  if (workflowName) {
26233
26952
  conditions.push(`workflow_name = $${paramIndex}`);
26953
+ v2Conditions.push(`workflow_name = $${paramIndex}`);
26234
26954
  values.push(workflowName);
26235
26955
  paramIndex++;
26236
26956
  }
26237
26957
  if (status) {
26238
26958
  const statusExpr = await this.#db.getColumnType(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'`;
26239
26959
  conditions.push(`${statusExpr} = $${paramIndex}`);
26960
+ v2Conditions.push(`status = $${paramIndex}`);
26240
26961
  values.push(status);
26241
26962
  paramIndex++;
26242
26963
  }
26243
26964
  if (resourceId) {
26244
- if (await this.#db.hasColumn(TABLE_WORKFLOW_SNAPSHOT, "resourceId")) {
26245
- conditions.push(`"resourceId" = $${paramIndex}`);
26246
- values.push(resourceId);
26247
- paramIndex++;
26248
- } else this.logger?.warn?.(`[${TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
26965
+ if (await this.#db.hasColumn(TABLE_WORKFLOW_SNAPSHOT, "resourceId")) conditions.push(`"resourceId" = $${paramIndex}`);
26966
+ else this.logger?.warn?.(`[${TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
26967
+ v2Conditions.push(`"resourceId" = $${paramIndex}`);
26968
+ values.push(resourceId);
26969
+ paramIndex++;
26249
26970
  }
26250
26971
  if (threadId) {
26251
- if (await this.#db.getColumnType(TABLE_WORKFLOW_SNAPSHOT, "snapshot") === "jsonb") {
26252
- conditions.push(`${WORKFLOW_SNAPSHOT_THREAD_ID_EXPR} = $${paramIndex}`);
26253
- values.push(threadId);
26254
- paramIndex++;
26255
- } else this.logger?.warn?.(`[${TABLE_WORKFLOW_SNAPSHOT}] snapshot column is not jsonb. Skipping threadId filter.`);
26972
+ if (await this.#db.getColumnType(TABLE_WORKFLOW_SNAPSHOT, "snapshot") === "jsonb") conditions.push(`${WORKFLOW_SNAPSHOT_THREAD_ID_EXPR} = $${paramIndex}`);
26973
+ else this.logger?.warn?.(`[${TABLE_WORKFLOW_SNAPSHOT}] snapshot column is not jsonb. Skipping threadId filter.`);
26974
+ v2Conditions.push(`"threadId" = $${paramIndex}`);
26975
+ values.push(threadId);
26976
+ paramIndex++;
26256
26977
  }
26257
26978
  if (fromDate) {
26258
26979
  conditions.push(`"createdAt" >= $${paramIndex}`);
26980
+ v2Conditions.push(`"createdAt" >= $${paramIndex}`);
26259
26981
  values.push(fromDate);
26260
26982
  paramIndex++;
26261
26983
  }
26262
26984
  if (toDate) {
26263
26985
  conditions.push(`"createdAt" <= $${paramIndex}`);
26986
+ v2Conditions.push(`"createdAt" <= $${paramIndex}`);
26264
26987
  values.push(toDate);
26265
26988
  paramIndex++;
26266
26989
  }
26267
26990
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
26991
+ const v2WhereClause = v2Conditions.length > 0 ? `WHERE ${v2Conditions.join(" AND ")}` : "";
26268
26992
  let total = 0;
26269
26993
  const usePagination = typeof perPage === "number" && typeof page === "number";
26270
26994
  if (usePagination) {
26271
- const countResult = await this.#db.client.one(`SELECT COUNT(*) as count FROM ${getTableName({
26995
+ const countResult = await this.#db.client.one(`SELECT (SELECT COUNT(*) FROM ${getTableName({
26272
26996
  indexName: TABLE_WORKFLOW_SNAPSHOT,
26273
26997
  schemaName: getSchemaName(this.#schema)
26274
- })} ${whereClause}`, values);
26998
+ })} ${whereClause})
26999
+ + (SELECT COUNT(*) FROM ${this.#runsTable()} ${v2WhereClause}) AS count`, values);
26275
27000
  total = Number(countResult.count);
26276
27001
  }
26277
27002
  const normalizedPerPage = usePagination ? normalizePerPage(perPage, Number.MAX_SAFE_INTEGER) : 0;
26278
27003
  const offset = usePagination ? page * normalizedPerPage : void 0;
26279
- let selectList = "*";
27004
+ let v1Snapshot = "snapshot";
26280
27005
  if (summary) {
26281
27006
  const snapshotJson = await this.#db.getColumnType(TABLE_WORKFLOW_SNAPSHOT, "snapshot") === "jsonb" ? "snapshot" : `regexp_replace(snapshot::text, '\\\\u(0000|[Dd][89A-Fa-f][0-9A-Fa-f]{2})', '', 'g')::jsonb`;
26282
- selectList = `workflow_name, run_id, "resourceId", "createdAt", "createdAtZ", "updatedAt", "updatedAtZ", jsonb_build_object('status', ${snapshotJson} -> 'status', 'timestamp', ${snapshotJson} -> 'timestamp') AS snapshot`;
27007
+ v1Snapshot = `jsonb_build_object('status', ${snapshotJson} -> 'status', 'timestamp', ${snapshotJson} -> 'timestamp')`;
26283
27008
  }
26284
- const query = `
26285
- SELECT ${selectList} FROM ${getTableName({
26286
- indexName: TABLE_WORKFLOW_SNAPSHOT,
26287
- schemaName: getSchemaName(this.#schema)
26288
- })}
26289
- ${whereClause}
26290
- ORDER BY "createdAt" DESC
26291
- ${usePagination ? ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}` : ""}
26292
- `;
27009
+ const query = await this.#runsQuery({
27010
+ v1Where: whereClause,
27011
+ v2Where: v2WhereClause,
27012
+ v1Snapshot,
27013
+ summary,
27014
+ page: usePagination ? `LIMIT $${paramIndex} OFFSET $${paramIndex + 1}` : ""
27015
+ });
26293
27016
  const queryValues = usePagination ? [
26294
27017
  ...values,
26295
27018
  normalizedPerPage,
26296
27019
  offset
26297
27020
  ] : values;
26298
- const runs = (await this.#db.client.manyOrNone(query, queryValues) || []).map((row) => {
26299
- return this.parseWorkflowRun(row);
26300
- });
27021
+ const result = await this.#db.client.manyOrNone(query, queryValues);
27022
+ const runs = await Promise.all((result || []).map((row) => this.#parseRun(this.#db.client, row)));
26301
27023
  return {
26302
27024
  runs,
26303
27025
  total: total || runs.length