@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.
- package/dist/docs/SKILL.md +2 -13
- package/dist/docs/assets/SOURCE_MAP.json +158 -2
- package/dist/docs/references/integrations-databases-neon.md +37 -1
- package/dist/docs/references/integrations-databases-postgresql.md +46 -0
- package/dist/docs/references/reference-processors-message-history-processor.md +1 -1
- package/dist/docs/references/reference-processors-semantic-recall-processor.md +1 -1
- package/dist/docs/references/reference-tools-vector-query-tool.md +3 -3
- package/dist/index.cjs +797 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +798 -65
- package/dist/index.js.map +1 -1
- package/dist/storage/db/index.d.ts +3 -3
- package/dist/storage/db/index.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/feedback.d.ts +1 -0
- package/dist/storage/domains/observability/v-next/feedback.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/index.d.ts +1 -0
- package/dist/storage/domains/observability/v-next/index.d.ts.map +1 -1
- package/dist/storage/domains/workflows/index.d.ts +87 -7
- package/dist/storage/domains/workflows/index.d.ts.map +1 -1
- package/dist/storage/retention.d.ts +3 -1
- package/dist/storage/retention.d.ts.map +1 -1
- package/package.json +3 -3
- package/dist/docs/references/docs-deployment-workers.md +0 -404
- package/dist/docs/references/docs-memory-semantic-recall.md +0 -485
- package/dist/docs/references/docs-memory-working-memory.md +0 -450
- package/dist/docs/references/docs-storage.md +0 -224
- package/dist/docs/references/reference-rag-retrieval.md +0 -672
- package/dist/docs/references/reference-rag-vector-databases.md +0 -808
- package/dist/docs/references/reference-storage-composite.md +0 -433
- package/dist/docs/references/reference-storage-retention.md +0 -310
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,
|
|
@@ -18314,6 +18314,11 @@ async function batchCreateFeedback(client, schema, args, partitioning) {
|
|
|
18314
18314
|
const rows = [...new Map(args.feedbacks.map((feedback) => [feedback.feedbackId, feedback])).values()].map(feedbackRecordToRow);
|
|
18315
18315
|
await insertRows(client, schema, TABLE_FEEDBACK_EVENTS, rows, partitioning, FEEDBACK_UPSERT_CLAUSE);
|
|
18316
18316
|
}
|
|
18317
|
+
async function getFeedbackById(client, schema, feedbackId) {
|
|
18318
|
+
const row = await client.oneOrNone(`SELECT ${FEEDBACK_SELECT_COLUMNS} FROM ${qualifiedTable(schema, TABLE_FEEDBACK_EVENTS)}
|
|
18319
|
+
WHERE "feedbackId" = $1 ORDER BY "cursorId" DESC LIMIT 1`, [feedbackId]);
|
|
18320
|
+
return row ? rowToFeedbackRecord(row) : null;
|
|
18321
|
+
}
|
|
18317
18322
|
async function updateFeedbackReviewStatus(client, schema, args) {
|
|
18318
18323
|
const { feedbackId, reviewStatus } = parseUpdateFeedbackReviewStatusArgs(args);
|
|
18319
18324
|
const row = await client.oneOrNone(`UPDATE ${qualifiedTable(schema, TABLE_FEEDBACK_EVENTS)}
|
|
@@ -22149,6 +22154,9 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
|
|
|
22149
22154
|
async getScoreById(scoreId) {
|
|
22150
22155
|
return this.#run("GET_SCORE_BY_ID", () => getScoreById(this.#readClient, this.#schema, scoreId), { scoreId });
|
|
22151
22156
|
}
|
|
22157
|
+
async getFeedbackById(feedbackId) {
|
|
22158
|
+
return this.#run("GET_FEEDBACK_BY_ID", () => getFeedbackById(this.#readClient, this.#schema, feedbackId), { feedbackId });
|
|
22159
|
+
}
|
|
22152
22160
|
async listFeedback(args) {
|
|
22153
22161
|
return this.#run("LIST_FEEDBACK", () => listFeedback(this.#readClient, this.#schema, args));
|
|
22154
22162
|
}
|
|
@@ -25602,6 +25610,16 @@ function workflowSnapshotStatusIndexSQL(indexName, schemaName, concurrently = fa
|
|
|
25602
25610
|
});
|
|
25603
25611
|
return `CREATE INDEX ${concurrently ? "CONCURRENTLY " : ""}IF NOT EXISTS "${indexName}" ON ${tableName} (workflow_name, (snapshot ->> 'status'), "createdAt" DESC)`;
|
|
25604
25612
|
}
|
|
25613
|
+
/**
|
|
25614
|
+
* Storage v2 index names share the long `<schema>_mastra_workflow_runs_` prefix, so a long
|
|
25615
|
+
* schema name would collapse them to one 63-byte identifier under plain truncation; hash instead.
|
|
25616
|
+
*/
|
|
25617
|
+
function runIndexName(schemaPrefix, baseName) {
|
|
25618
|
+
return buildConstraintName({
|
|
25619
|
+
baseName: `${schemaPrefix}${baseName}`,
|
|
25620
|
+
hashWhenTruncated: true
|
|
25621
|
+
});
|
|
25622
|
+
}
|
|
25605
25623
|
/** Base name (before any schema prefix) of the expression index backing the threadId filter. */
|
|
25606
25624
|
const WORKFLOW_SNAPSHOT_THREAD_ID_INDEX = "mastra_workflow_snapshot_threadid_idx";
|
|
25607
25625
|
/**
|
|
@@ -25646,13 +25664,126 @@ function workflowSnapshotThreadIdIndexSQL(indexName, schemaName, concurrently =
|
|
|
25646
25664
|
});
|
|
25647
25665
|
return `CREATE INDEX ${concurrently ? "CONCURRENTLY " : ""}IF NOT EXISTS "${indexName}" ON ${tableName} ((${WORKFLOW_SNAPSHOT_THREAD_ID_EXPR}))`;
|
|
25648
25666
|
}
|
|
25667
|
+
/** Base name (before any schema prefix) of the index finding the `state-base` entries built on a run. */
|
|
25668
|
+
const WORKFLOW_RUN_ENTRIES_STATE_BASE_INDEX = "mastra_workflow_run_entries_state_base_idx";
|
|
25669
|
+
/** The run a `state-base` entry builds on; `#detachStateChildren` must use these exact expressions. */
|
|
25670
|
+
const STATE_BASE_RUN_EXPR = `(payload ->> 'workflowName'), (payload ->> 'runId')`;
|
|
25671
|
+
/**
|
|
25672
|
+
* Partial expression index finding the `state-base` entries built on a run, so deleting a run whose
|
|
25673
|
+
* state others build on doesn't scan every entry. See {@link workflowSnapshotStatusIndexSQL} for
|
|
25674
|
+
* `concurrently`.
|
|
25675
|
+
*/
|
|
25676
|
+
function stateBaseIndexSQL(indexName, schemaName, concurrently = false) {
|
|
25677
|
+
const tableName = getTableName({
|
|
25678
|
+
indexName: TABLE_WORKFLOW_RUN_ENTRIES,
|
|
25679
|
+
schemaName: getSchemaName(schemaName)
|
|
25680
|
+
});
|
|
25681
|
+
return `CREATE INDEX ${concurrently ? "CONCURRENTLY " : ""}IF NOT EXISTS "${indexName}" ON ${tableName} (${STATE_BASE_RUN_EXPR}) WHERE kind = 'state-base'`;
|
|
25682
|
+
}
|
|
25683
|
+
/**
|
|
25684
|
+
* The entries a storage v2 write adds: one per step, in order, then one per step patch, then the
|
|
25685
|
+
* state or state base (a checkpoint with neither starts the state over at `{}`), then the state
|
|
25686
|
+
* patch unless a full state was given.
|
|
25687
|
+
*/
|
|
25688
|
+
function journalEntries(steps, { state, stateBase, statePatch }, checkpoint, stepPatches = {}) {
|
|
25689
|
+
const entries = Object.entries(steps).map(([stepId, result]) => ({
|
|
25690
|
+
kind: "step",
|
|
25691
|
+
step_id: stepId,
|
|
25692
|
+
payload: result
|
|
25693
|
+
}));
|
|
25694
|
+
for (const [stepId, ops] of Object.entries(stepPatches)) entries.push({
|
|
25695
|
+
kind: "step-patch",
|
|
25696
|
+
step_id: stepId,
|
|
25697
|
+
payload: ops
|
|
25698
|
+
});
|
|
25699
|
+
if (state !== void 0) entries.push({
|
|
25700
|
+
kind: "state",
|
|
25701
|
+
step_id: null,
|
|
25702
|
+
payload: state
|
|
25703
|
+
});
|
|
25704
|
+
else if (stateBase !== void 0) entries.push({
|
|
25705
|
+
kind: "state-base",
|
|
25706
|
+
step_id: null,
|
|
25707
|
+
payload: stateBase
|
|
25708
|
+
});
|
|
25709
|
+
else if (checkpoint) entries.push({
|
|
25710
|
+
kind: "state",
|
|
25711
|
+
step_id: null,
|
|
25712
|
+
payload: {}
|
|
25713
|
+
});
|
|
25714
|
+
if (state === void 0 && statePatch !== void 0) entries.push({
|
|
25715
|
+
kind: "state-patch",
|
|
25716
|
+
step_id: null,
|
|
25717
|
+
payload: statePatch
|
|
25718
|
+
});
|
|
25719
|
+
return entries;
|
|
25720
|
+
}
|
|
25721
|
+
/**
|
|
25722
|
+
* Rows `e(elem, ord)` of the jsonb array parameter `param` of {@link EntryInput}, and the
|
|
25723
|
+
* entry columns they insert. `elem -> 'payload'` keeps a JSON null apart from an undefined
|
|
25724
|
+
* payload (dropped by `JSON.stringify`), which becomes SQL NULL.
|
|
25725
|
+
*/
|
|
25726
|
+
const entryRows = (param) => `jsonb_array_elements(${param}::jsonb) WITH ORDINALITY AS e(elem, ord)`;
|
|
25727
|
+
const ENTRY_VALUES = `e.elem ->> 'kind', e.elem ->> 'step_id', e.elem -> 'payload'`;
|
|
25728
|
+
/** The entries of {@link EntryRow} rows; an undefined payload reads back as undefined, as in v1. */
|
|
25729
|
+
function toEntries(rows) {
|
|
25730
|
+
return rows.map((row) => {
|
|
25731
|
+
const payload = row.absent ? void 0 : row.payload;
|
|
25732
|
+
switch (row.kind) {
|
|
25733
|
+
case "step": return {
|
|
25734
|
+
seq: row.seq,
|
|
25735
|
+
kind: "step",
|
|
25736
|
+
stepId: row.step_id,
|
|
25737
|
+
result: payload
|
|
25738
|
+
};
|
|
25739
|
+
case "step-patch": return {
|
|
25740
|
+
seq: row.seq,
|
|
25741
|
+
kind: "step-patch",
|
|
25742
|
+
stepId: row.step_id,
|
|
25743
|
+
ops: payload
|
|
25744
|
+
};
|
|
25745
|
+
case "state": return {
|
|
25746
|
+
seq: row.seq,
|
|
25747
|
+
kind: "state",
|
|
25748
|
+
state: payload
|
|
25749
|
+
};
|
|
25750
|
+
case "state-patch": return {
|
|
25751
|
+
seq: row.seq,
|
|
25752
|
+
kind: "state-patch",
|
|
25753
|
+
ops: payload
|
|
25754
|
+
};
|
|
25755
|
+
case "state-base": return {
|
|
25756
|
+
seq: row.seq,
|
|
25757
|
+
kind: "state-base",
|
|
25758
|
+
base: payload
|
|
25759
|
+
};
|
|
25760
|
+
case "state-draft": {
|
|
25761
|
+
const draft = payload;
|
|
25762
|
+
return "state" in draft ? {
|
|
25763
|
+
seq: row.seq,
|
|
25764
|
+
kind: "state-draft",
|
|
25765
|
+
state: draft.state
|
|
25766
|
+
} : {
|
|
25767
|
+
seq: row.seq,
|
|
25768
|
+
kind: "state-draft",
|
|
25769
|
+
on: draft.on,
|
|
25770
|
+
ops: draft.ops
|
|
25771
|
+
};
|
|
25772
|
+
}
|
|
25773
|
+
}
|
|
25774
|
+
});
|
|
25775
|
+
}
|
|
25649
25776
|
var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
|
|
25650
25777
|
#db;
|
|
25651
25778
|
#schema;
|
|
25652
25779
|
#skipDefaultIndexes;
|
|
25653
25780
|
#indexes;
|
|
25654
25781
|
/** Tables managed by this domain */
|
|
25655
|
-
static MANAGED_TABLES = [
|
|
25782
|
+
static MANAGED_TABLES = [
|
|
25783
|
+
TABLE_WORKFLOW_SNAPSHOT,
|
|
25784
|
+
TABLE_WORKFLOW_RUNS,
|
|
25785
|
+
TABLE_WORKFLOW_RUN_ENTRIES
|
|
25786
|
+
];
|
|
25656
25787
|
/**
|
|
25657
25788
|
* Workflow run snapshots accumulate as runs execute. Anchored on the
|
|
25658
25789
|
* timezone-aware `updatedAtZ` mirror column (last activity) so suspended or
|
|
@@ -25715,11 +25846,43 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
|
|
|
25715
25846
|
};
|
|
25716
25847
|
}
|
|
25717
25848
|
static getDefaultIndexDefs(schemaPrefix) {
|
|
25718
|
-
return [
|
|
25719
|
-
|
|
25720
|
-
|
|
25721
|
-
|
|
25722
|
-
|
|
25849
|
+
return [
|
|
25850
|
+
{
|
|
25851
|
+
name: `${schemaPrefix}mastra_workflow_snapshot_name_createdat_idx`,
|
|
25852
|
+
table: TABLE_WORKFLOW_SNAPSHOT,
|
|
25853
|
+
columns: ["workflow_name", "createdAt DESC"]
|
|
25854
|
+
},
|
|
25855
|
+
{
|
|
25856
|
+
name: runIndexName(schemaPrefix, "mastra_workflow_runs_name_createdat_idx"),
|
|
25857
|
+
table: TABLE_WORKFLOW_RUNS,
|
|
25858
|
+
columns: ["workflow_name", "createdAt DESC"]
|
|
25859
|
+
},
|
|
25860
|
+
{
|
|
25861
|
+
name: runIndexName(schemaPrefix, "mastra_workflow_runs_name_status_createdat_idx"),
|
|
25862
|
+
table: TABLE_WORKFLOW_RUNS,
|
|
25863
|
+
columns: [
|
|
25864
|
+
"workflow_name",
|
|
25865
|
+
"status",
|
|
25866
|
+
"createdAt DESC"
|
|
25867
|
+
]
|
|
25868
|
+
},
|
|
25869
|
+
{
|
|
25870
|
+
name: runIndexName(schemaPrefix, "mastra_workflow_runs_threadid_idx"),
|
|
25871
|
+
table: TABLE_WORKFLOW_RUNS,
|
|
25872
|
+
columns: ["threadId"]
|
|
25873
|
+
},
|
|
25874
|
+
{
|
|
25875
|
+
name: runIndexName(schemaPrefix, "mastra_workflow_run_entries_step_idx"),
|
|
25876
|
+
table: TABLE_WORKFLOW_RUN_ENTRIES,
|
|
25877
|
+
columns: [
|
|
25878
|
+
"workflow_name",
|
|
25879
|
+
"run_id",
|
|
25880
|
+
"kind",
|
|
25881
|
+
"step_id",
|
|
25882
|
+
"seq"
|
|
25883
|
+
]
|
|
25884
|
+
}
|
|
25885
|
+
];
|
|
25723
25886
|
}
|
|
25724
25887
|
/**
|
|
25725
25888
|
* Returns all DDL statements for this domain: table with unique constraint.
|
|
@@ -25735,9 +25898,17 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
|
|
|
25735
25898
|
schemaName,
|
|
25736
25899
|
includeAllConstraints: true
|
|
25737
25900
|
}));
|
|
25901
|
+
for (const [tableName, config] of Object.entries(WORKFLOW_RUN_JOURNAL_TABLE_CONFIGS)) statements.push(generateTableSQL({
|
|
25902
|
+
tableName,
|
|
25903
|
+
schema: config.columns,
|
|
25904
|
+
schemaName,
|
|
25905
|
+
compositePrimaryKey: config.compositePrimaryKey,
|
|
25906
|
+
includeAllConstraints: true
|
|
25907
|
+
}));
|
|
25738
25908
|
for (const idx of WorkflowsPG.getDefaultIndexDefs(schemaPrefix)) statements.push(generateIndexSQL(idx, schemaName));
|
|
25739
25909
|
statements.push(`${workflowSnapshotStatusIndexSQL(workflowSnapshotStatusIndexName(parsedSchema), schemaName)};`);
|
|
25740
25910
|
statements.push(`${workflowSnapshotThreadIdIndexSQL(workflowSnapshotThreadIdIndexName(parsedSchema), schemaName)};`);
|
|
25911
|
+
statements.push(`${stateBaseIndexSQL(runIndexName(schemaPrefix, WORKFLOW_RUN_ENTRIES_STATE_BASE_INDEX), schemaName)};`);
|
|
25741
25912
|
statements.push(generateTableSQL({
|
|
25742
25913
|
tableName: TABLE_WORKFLOW_RUN_OWNERS,
|
|
25743
25914
|
schema: RUN_FENCING_TABLE_SCHEMAS[TABLE_WORKFLOW_RUN_OWNERS],
|
|
@@ -25763,6 +25934,12 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
|
|
|
25763
25934
|
} catch (error) {
|
|
25764
25935
|
this.logger?.warn?.(`Failed to create index ${indexDef.name}:`, error);
|
|
25765
25936
|
}
|
|
25937
|
+
const stateBaseIndexName = runIndexName(this.#schema !== "public" ? `${schemaNamePrefix(this.#schema)}_` : "", WORKFLOW_RUN_ENTRIES_STATE_BASE_INDEX);
|
|
25938
|
+
try {
|
|
25939
|
+
await this.#db.createIndexFromStatement(stateBaseIndexName, stateBaseIndexSQL(stateBaseIndexName, this.#schema, true));
|
|
25940
|
+
} catch (error) {
|
|
25941
|
+
this.logger?.warn?.(`Failed to create index ${stateBaseIndexName}:`, error);
|
|
25942
|
+
}
|
|
25766
25943
|
if (await this.#db.getColumnType(TABLE_WORKFLOW_SNAPSHOT, "snapshot") !== "jsonb") return;
|
|
25767
25944
|
const indexName = workflowSnapshotStatusIndexName(this.#schema);
|
|
25768
25945
|
try {
|
|
@@ -25791,6 +25968,11 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
|
|
|
25791
25968
|
tableName: TABLE_WORKFLOW_RUN_OWNERS,
|
|
25792
25969
|
schema: RUN_FENCING_TABLE_SCHEMAS[TABLE_WORKFLOW_RUN_OWNERS]
|
|
25793
25970
|
});
|
|
25971
|
+
for (const [tableName, config] of Object.entries(WORKFLOW_RUN_JOURNAL_TABLE_CONFIGS)) await this.#db.createTable({
|
|
25972
|
+
tableName,
|
|
25973
|
+
schema: config.columns,
|
|
25974
|
+
compositePrimaryKey: config.compositePrimaryKey
|
|
25975
|
+
});
|
|
25794
25976
|
await this.createDefaultIndexes();
|
|
25795
25977
|
await this.createCustomIndexes();
|
|
25796
25978
|
}
|
|
@@ -25817,8 +25999,22 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
|
|
|
25817
25999
|
this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
|
|
25818
26000
|
}
|
|
25819
26001
|
}
|
|
26002
|
+
if (!policies.workflowSnapshot) return;
|
|
26003
|
+
try {
|
|
26004
|
+
await this.#db.ensureIndex({
|
|
26005
|
+
indexName: `${prefix}mastra_workflow_runs_retention_idx`,
|
|
26006
|
+
tableName: TABLE_WORKFLOW_RUNS,
|
|
26007
|
+
column: "updatedAtZ"
|
|
26008
|
+
});
|
|
26009
|
+
} catch (error) {
|
|
26010
|
+
this.logger?.warn?.(`Failed to create retention index for ${TABLE_WORKFLOW_RUNS}:`, error);
|
|
26011
|
+
}
|
|
25820
26012
|
}
|
|
25821
|
-
/**
|
|
26013
|
+
/**
|
|
26014
|
+
* Delete workflow runs and run ownership rows older than their policy's `maxAge`, batched.
|
|
26015
|
+
* The `workflowSnapshot` policy covers both storage versions: storage v2 runs are pruned by
|
|
26016
|
+
* their header's last write, together with their entries.
|
|
26017
|
+
*/
|
|
25822
26018
|
async prune(policies, options) {
|
|
25823
26019
|
await this.ensureRetentionIndexes(policies);
|
|
25824
26020
|
const targets = resolveTargets({
|
|
@@ -25826,11 +26022,40 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
|
|
|
25826
26022
|
descriptor: WorkflowsPG.retentionTables,
|
|
25827
26023
|
order: ["workflowSnapshot", "runOwnership"]
|
|
25828
26024
|
});
|
|
26025
|
+
const snapshotTarget = targets.findIndex((target) => target.table === TABLE_WORKFLOW_SNAPSHOT);
|
|
26026
|
+
if (snapshotTarget !== -1) targets.splice(snapshotTarget + 1, 0, {
|
|
26027
|
+
...targets[snapshotTarget],
|
|
26028
|
+
table: TABLE_WORKFLOW_RUNS
|
|
26029
|
+
});
|
|
25829
26030
|
return runPrune({
|
|
25830
26031
|
db: this.#db,
|
|
25831
26032
|
domain: "workflows",
|
|
25832
26033
|
targets,
|
|
25833
|
-
options
|
|
26034
|
+
options,
|
|
26035
|
+
deleteBatch: (target, cutoff, limit) => target.table === TABLE_WORKFLOW_RUNS ? this.#pruneRunsBatch(cutoff, limit) : void 0
|
|
26036
|
+
});
|
|
26037
|
+
}
|
|
26038
|
+
/**
|
|
26039
|
+
* Deletes up to `limit` aged storage v2 runs, header and entries together, first detaching the
|
|
26040
|
+
* runs whose state others build on (see `#detachStateChildren`).
|
|
26041
|
+
*/
|
|
26042
|
+
async #pruneRunsBatch(cutoff, limit) {
|
|
26043
|
+
return this.#db.client.tx(async (t) => {
|
|
26044
|
+
const doomed = await t.manyOrNone(`SELECT workflow_name, run_id, "stateShared" FROM ${this.#runsTable()}
|
|
26045
|
+
WHERE "updatedAtZ" < $1
|
|
26046
|
+
ORDER BY "updatedAtZ"
|
|
26047
|
+
LIMIT $2
|
|
26048
|
+
FOR UPDATE SKIP LOCKED`, [cutoff, limit]);
|
|
26049
|
+
if (!doomed.length) return 0;
|
|
26050
|
+
for (const run of doomed) if (run.stateShared) await this.#detachStateChildren(t, run.workflow_name, run.run_id);
|
|
26051
|
+
return (await t.query(`WITH doomed AS (
|
|
26052
|
+
SELECT * FROM unnest($1::text[], $2::text[]) AS d(workflow_name, run_id)
|
|
26053
|
+
), entries AS (
|
|
26054
|
+
DELETE FROM ${this.#entriesTable()} e USING doomed d
|
|
26055
|
+
WHERE e.workflow_name = d.workflow_name AND e.run_id = d.run_id
|
|
26056
|
+
)
|
|
26057
|
+
DELETE FROM ${this.#runsTable()} r USING doomed d
|
|
26058
|
+
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
26059
|
});
|
|
25835
26060
|
}
|
|
25836
26061
|
/**
|
|
@@ -25847,6 +26072,459 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
|
|
|
25847
26072
|
async dangerouslyClearAll() {
|
|
25848
26073
|
await this.#db.clearTable({ tableName: TABLE_WORKFLOW_SNAPSHOT });
|
|
25849
26074
|
await this.#db.clearTable({ tableName: TABLE_WORKFLOW_RUN_OWNERS });
|
|
26075
|
+
await this.#db.clearTable({ tableName: TABLE_WORKFLOW_RUNS });
|
|
26076
|
+
await this.#db.clearTable({ tableName: TABLE_WORKFLOW_RUN_ENTRIES });
|
|
26077
|
+
}
|
|
26078
|
+
supportsRunJournal() {
|
|
26079
|
+
return true;
|
|
26080
|
+
}
|
|
26081
|
+
#runsTable() {
|
|
26082
|
+
return getTableName({
|
|
26083
|
+
indexName: TABLE_WORKFLOW_RUNS,
|
|
26084
|
+
schemaName: getSchemaName(this.#schema)
|
|
26085
|
+
});
|
|
26086
|
+
}
|
|
26087
|
+
#entriesTable() {
|
|
26088
|
+
return getTableName({
|
|
26089
|
+
indexName: TABLE_WORKFLOW_RUN_ENTRIES,
|
|
26090
|
+
schemaName: getSchemaName(this.#schema)
|
|
26091
|
+
});
|
|
26092
|
+
}
|
|
26093
|
+
/**
|
|
26094
|
+
* A jsonb array of {@link EntryRow} for the storage v2 run `alias` refers to: each step's
|
|
26095
|
+
* latest entry, ordered by when each step was first written so `context` keys keep their write
|
|
26096
|
+
* order, the step patches after it (in `seq` order, for `latestStepResults`), and the entries
|
|
26097
|
+
* `materializeState` uses for the latest state: the latest full state or state base and every
|
|
26098
|
+
* state patch after it, in `seq` order. Drafts are left out.
|
|
26099
|
+
*
|
|
26100
|
+
* With `summaryFieldsParam` (the parameter holding `STEP_RESULT_SUMMARY_FIELDS`), successful
|
|
26101
|
+
* steps other than `input` come back as their summary, built here so their data is never sent,
|
|
26102
|
+
* unless step patches follow it (the caller summarizes the patched result instead).
|
|
26103
|
+
* Like `summarizeStepResult`, it keeps a field that is JSON null and leaves out one that is absent.
|
|
26104
|
+
*/
|
|
26105
|
+
#entriesSql(alias, { summaryFieldsParam } = {}) {
|
|
26106
|
+
let payload = "x.payload";
|
|
26107
|
+
if (summaryFieldsParam) {
|
|
26108
|
+
const summarized = `(x.kind = 'step' AND x.step_id <> 'input' AND x.payload ->> 'status' = 'success'
|
|
26109
|
+
AND (x.patch_seq IS NULL OR x.patch_seq < x.seq))`;
|
|
26110
|
+
payload = `CASE WHEN ${summarized} THEN (
|
|
26111
|
+
SELECT COALESCE(jsonb_object_agg(f.key, f.value), '{}'::jsonb)
|
|
26112
|
+
FROM jsonb_each(x.payload) f WHERE f.key = ANY(${summaryFieldsParam}::text[])
|
|
26113
|
+
) ELSE x.payload END, 'summarized', ${summarized}`;
|
|
26114
|
+
}
|
|
26115
|
+
return `(SELECT COALESCE(jsonb_agg(jsonb_build_object(
|
|
26116
|
+
'seq', x.seq, 'kind', x.kind, 'step_id', x.step_id, 'absent', x.payload IS NULL, 'payload', ${payload}
|
|
26117
|
+
) ORDER BY CASE WHEN x.kind = 'step' THEN x.first_seq ELSE x.seq END), '[]'::jsonb)
|
|
26118
|
+
FROM (
|
|
26119
|
+
SELECT e.seq, e.kind, e.step_id, e.payload,
|
|
26120
|
+
ROW_NUMBER() OVER (PARTITION BY e.kind, e.step_id ORDER BY e.seq DESC) AS rn,
|
|
26121
|
+
MIN(e.seq) OVER (PARTITION BY e.kind, e.step_id) AS first_seq,
|
|
26122
|
+
MAX(CASE WHEN e.kind = 'step' THEN e.seq END) OVER (PARTITION BY e.step_id) AS step_seq,
|
|
26123
|
+
MAX(CASE WHEN e.kind = 'step-patch' THEN e.seq END) OVER (PARTITION BY e.step_id) AS patch_seq,
|
|
26124
|
+
MAX(CASE WHEN e.kind IN ('state', 'state-base') THEN e.seq END) OVER () AS base_seq
|
|
26125
|
+
FROM ${this.#entriesTable()} e
|
|
26126
|
+
WHERE e.workflow_name = ${alias}.workflow_name AND e.run_id = ${alias}.run_id AND e.kind <> 'state-draft'
|
|
26127
|
+
) x
|
|
26128
|
+
WHERE CASE x.kind
|
|
26129
|
+
WHEN 'step' THEN x.rn = 1
|
|
26130
|
+
WHEN 'step-patch' THEN x.seq > x.step_seq
|
|
26131
|
+
ELSE x.base_seq IS NULL OR x.seq >= x.base_seq
|
|
26132
|
+
END)`;
|
|
26133
|
+
}
|
|
26134
|
+
/**
|
|
26135
|
+
* A jsonb array of {@link EntryRow} with the entries `materializeState` uses for the state of the
|
|
26136
|
+
* storage v2 run `alias` refers to as of a version: `seqParam` its `seq`, `draftParam` whether
|
|
26137
|
+
* it is a draft. For a draft, every state entry up to it, as the drafts it is on can build on
|
|
26138
|
+
* any earlier version; otherwise the latest full state or state base up to it and the state
|
|
26139
|
+
* patches after it.
|
|
26140
|
+
*/
|
|
26141
|
+
#stateEntriesSql(alias, seqParam, draftParam) {
|
|
26142
|
+
return `(SELECT COALESCE(jsonb_agg(jsonb_build_object(
|
|
26143
|
+
'seq', x.seq, 'kind', x.kind, 'step_id', x.step_id, 'absent', x.payload IS NULL, 'payload', x.payload
|
|
26144
|
+
) ORDER BY x.seq), '[]'::jsonb)
|
|
26145
|
+
FROM (
|
|
26146
|
+
SELECT e.seq, e.kind, e.step_id, e.payload,
|
|
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
|
|
26150
|
+
AND e.kind NOT IN ('step', 'step-patch') AND e.seq <= ${seqParam}::int
|
|
26151
|
+
) x
|
|
26152
|
+
WHERE ${draftParam}::boolean OR (x.kind <> 'state-draft' AND (x.base_seq IS NULL OR x.seq >= x.base_seq)))`;
|
|
26153
|
+
}
|
|
26154
|
+
/**
|
|
26155
|
+
* Selects v1 snapshot rows and storage v2 runs as one result, newest first. Each version
|
|
26156
|
+
* has its own WHERE clause; `null` leaves it out. Storage v2 rows carry their header and
|
|
26157
|
+
* entries, or in summary mode only the summary snapshot. `page` is appended to the
|
|
26158
|
+
* combined, ordered rows (e.g. `LIMIT 1`).
|
|
26159
|
+
*/
|
|
26160
|
+
async #runsQuery({ v1Where, v2Where, v1Snapshot = "snapshot", summary, page = "" }) {
|
|
26161
|
+
const branches = [];
|
|
26162
|
+
if (v1Where !== null) {
|
|
26163
|
+
const optional = {
|
|
26164
|
+
resourceId: "text",
|
|
26165
|
+
createdAtZ: "timestamptz",
|
|
26166
|
+
updatedAtZ: "timestamptz"
|
|
26167
|
+
};
|
|
26168
|
+
const columns = {};
|
|
26169
|
+
for (const [column, type] of Object.entries(optional)) columns[column] = await this.#db.getColumnType(TABLE_WORKFLOW_SNAPSHOT, column) !== null ? `"${column}"` : `NULL::${type} AS "${column}"`;
|
|
26170
|
+
branches.push(`SELECT workflow_name, run_id, ${columns.resourceId}, ${v1Snapshot} AS snapshot, NULL::jsonb AS header,
|
|
26171
|
+
"createdAt", ${columns.createdAtZ}, "updatedAt", ${columns.updatedAtZ}, 1 AS version
|
|
26172
|
+
FROM ${getTableName({
|
|
26173
|
+
indexName: TABLE_WORKFLOW_SNAPSHOT,
|
|
26174
|
+
schemaName: getSchemaName(this.#schema)
|
|
26175
|
+
})} ${v1Where}`);
|
|
26176
|
+
}
|
|
26177
|
+
if (v2Where !== null) {
|
|
26178
|
+
const columns = summary ? `jsonb_build_object('status', header -> 'status', 'timestamp', header -> 'timestamp') AS snapshot, NULL::jsonb AS header` : `NULL AS snapshot, header`;
|
|
26179
|
+
branches.push(`SELECT workflow_name, run_id, "resourceId", ${columns},
|
|
26180
|
+
"createdAt", "createdAtZ", "updatedAt", "updatedAtZ", 2 AS version
|
|
26181
|
+
FROM ${this.#runsTable()} ${v2Where}`);
|
|
26182
|
+
}
|
|
26183
|
+
return `SELECT p.*${summary ? "" : `, CASE WHEN p.version = 2 THEN ${this.#entriesSql("p")} END AS entries`}
|
|
26184
|
+
FROM (${branches.join(" UNION ALL ")} ORDER BY "createdAt" DESC ${page}) p
|
|
26185
|
+
ORDER BY p."createdAt" DESC`;
|
|
26186
|
+
}
|
|
26187
|
+
/**
|
|
26188
|
+
* The `base` that `materializeState` takes for a run's entries (as of `at`, or latest): the state
|
|
26189
|
+
* version of another run it builds on (`stateBaseOf`), resolved recursively, else undefined. `{}`
|
|
26190
|
+
* when that run isn't stored. One query per run in the chain.
|
|
26191
|
+
*/
|
|
26192
|
+
async #stateBase(q, entries, at) {
|
|
26193
|
+
const ref = stateBaseOf(entries, at);
|
|
26194
|
+
if (!ref) return void 0;
|
|
26195
|
+
return (await this.#stateAt(q, ref.workflowName, ref.runId, ref))?.state ?? {};
|
|
26196
|
+
}
|
|
26197
|
+
/** A storage v2 run's state as of the version `at`, or null when the run isn't stored. */
|
|
26198
|
+
async #stateAt(q, workflowName, runId, at) {
|
|
26199
|
+
const row = await q.oneOrNone(`SELECT ${this.#stateEntriesSql("r", "$3", "$4")} AS entries
|
|
26200
|
+
FROM ${this.#runsTable()} r WHERE r.workflow_name = $1 AND r.run_id = $2`, [
|
|
26201
|
+
workflowName,
|
|
26202
|
+
runId,
|
|
26203
|
+
at.seq,
|
|
26204
|
+
at.draft === true
|
|
26205
|
+
]);
|
|
26206
|
+
if (!row) return null;
|
|
26207
|
+
const entries = toEntries(row.entries);
|
|
26208
|
+
return materializeState(entries, {
|
|
26209
|
+
at,
|
|
26210
|
+
base: await this.#stateBase(q, entries, at)
|
|
26211
|
+
});
|
|
26212
|
+
}
|
|
26213
|
+
/**
|
|
26214
|
+
* Before a run is deleted, stores the full state in place of every `state-base` entry built on
|
|
26215
|
+
* it, so the runs built on it keep reading the same state. Runs inside the delete's transaction.
|
|
26216
|
+
*/
|
|
26217
|
+
async #detachStateChildren(q, workflowName, runId) {
|
|
26218
|
+
const children = await q.manyOrNone(`SELECT workflow_name, run_id, seq, payload FROM ${this.#entriesTable()}
|
|
26219
|
+
WHERE kind = 'state-base' AND (${STATE_BASE_RUN_EXPR}) = ($1, $2)`, [workflowName, runId]);
|
|
26220
|
+
const states = /* @__PURE__ */ new Map();
|
|
26221
|
+
for (const child of children) {
|
|
26222
|
+
const version = `${child.payload.seq}:${child.payload.draft === true}`;
|
|
26223
|
+
let state = states.get(version);
|
|
26224
|
+
if (!state) {
|
|
26225
|
+
state = (await this.#stateAt(q, workflowName, runId, child.payload))?.state ?? {};
|
|
26226
|
+
states.set(version, state);
|
|
26227
|
+
}
|
|
26228
|
+
await q.none(`UPDATE ${this.#entriesTable()} SET kind = 'state', payload = $4::jsonb
|
|
26229
|
+
WHERE workflow_name = $1 AND run_id = $2 AND seq = $3`, [
|
|
26230
|
+
child.workflow_name,
|
|
26231
|
+
child.run_id,
|
|
26232
|
+
child.seq,
|
|
26233
|
+
toPgJson(state)
|
|
26234
|
+
]);
|
|
26235
|
+
}
|
|
26236
|
+
}
|
|
26237
|
+
/** Rebuilds a storage v2 run's `WorkflowRunState` from its header and `#entriesSql` rows. */
|
|
26238
|
+
async #materializeRun(client, header, rows) {
|
|
26239
|
+
const entries = toEntries(rows);
|
|
26240
|
+
return materializeWorkflowRun(header, entries, await this.#stateBase(client, entries));
|
|
26241
|
+
}
|
|
26242
|
+
/** Parses a row of `#runsQuery`. */
|
|
26243
|
+
async #parseRun(client, row) {
|
|
26244
|
+
if (row.version !== 2) return this.parseWorkflowRun(row);
|
|
26245
|
+
const snapshot = row.entries ? await this.#materializeRun(client, row.header, row.entries) : row.snapshot;
|
|
26246
|
+
return {
|
|
26247
|
+
...this.parseWorkflowRun({
|
|
26248
|
+
...row,
|
|
26249
|
+
snapshot
|
|
26250
|
+
}),
|
|
26251
|
+
storageVersion: 2
|
|
26252
|
+
};
|
|
26253
|
+
}
|
|
26254
|
+
#runError(operation, workflowName, runId, error) {
|
|
26255
|
+
return new MastraError({
|
|
26256
|
+
id: createStorageErrorId("PG", operation, "FAILED"),
|
|
26257
|
+
domain: ErrorDomain.STORAGE,
|
|
26258
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
26259
|
+
details: {
|
|
26260
|
+
workflowName,
|
|
26261
|
+
runId
|
|
26262
|
+
}
|
|
26263
|
+
}, error);
|
|
26264
|
+
}
|
|
26265
|
+
async loadWorkflowRunHeader({ workflowName, runId }) {
|
|
26266
|
+
try {
|
|
26267
|
+
return await this.#db.client.oneOrNone(`SELECT header, "lastSeq", "startStateSeq", "runToken" FROM ${this.#runsTable()} WHERE workflow_name = $1 AND run_id = $2`, [workflowName, runId]);
|
|
26268
|
+
} catch (error) {
|
|
26269
|
+
throw this.#runError("LOAD_WORKFLOW_RUN_HEADER", workflowName, runId, error);
|
|
26270
|
+
}
|
|
26271
|
+
}
|
|
26272
|
+
async loadWorkflowRun({ workflowName, runId, successfulSteps }) {
|
|
26273
|
+
try {
|
|
26274
|
+
const summary = successfulSteps === "summary";
|
|
26275
|
+
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
|
|
26276
|
+
FROM ${this.#runsTable()} r WHERE r.workflow_name = $1 AND r.run_id = $2`, summary ? [
|
|
26277
|
+
workflowName,
|
|
26278
|
+
runId,
|
|
26279
|
+
STEP_RESULT_SUMMARY_FIELDS
|
|
26280
|
+
] : [workflowName, runId]);
|
|
26281
|
+
if (!run) return null;
|
|
26282
|
+
const entries = toEntries(run.entries);
|
|
26283
|
+
const summarizedInQuery = new Set(run.entries.filter((row) => row.summarized).map((row) => row.step_id));
|
|
26284
|
+
const steps = {};
|
|
26285
|
+
const summarizedStepIds = [];
|
|
26286
|
+
for (const [stepId, result] of latestStepResults(entries)) {
|
|
26287
|
+
steps[stepId] = result;
|
|
26288
|
+
if (summary && stepId !== "input" && result?.status === "success") {
|
|
26289
|
+
if (!summarizedInQuery.has(stepId)) steps[stepId] = summarizeStepResult(result);
|
|
26290
|
+
summarizedStepIds.push(stepId);
|
|
26291
|
+
}
|
|
26292
|
+
}
|
|
26293
|
+
const base = await this.#stateBase(this.#db.client, entries);
|
|
26294
|
+
const { state, statePatchBytes } = materializeState(entries, { base });
|
|
26295
|
+
return {
|
|
26296
|
+
header: run.header,
|
|
26297
|
+
lastSeq: run.lastSeq,
|
|
26298
|
+
startStateSeq: run.startStateSeq,
|
|
26299
|
+
runToken: run.runToken,
|
|
26300
|
+
state,
|
|
26301
|
+
statePatchBytes,
|
|
26302
|
+
steps,
|
|
26303
|
+
summarizedStepIds
|
|
26304
|
+
};
|
|
26305
|
+
} catch (error) {
|
|
26306
|
+
throw this.#runError("LOAD_WORKFLOW_RUN", workflowName, runId, error);
|
|
26307
|
+
}
|
|
26308
|
+
}
|
|
26309
|
+
async loadWorkflowRunSteps({ workflowName, runId, stepIds, expectedLastSeq, expectedRunToken }) {
|
|
26310
|
+
try {
|
|
26311
|
+
const run = await this.#db.client.oneOrNone(`SELECT r."lastSeq", r."runToken", (
|
|
26312
|
+
SELECT COALESCE(jsonb_agg(jsonb_build_object(
|
|
26313
|
+
'seq', x.seq, 'kind', x.kind, 'step_id', x.step_id, 'absent', x.payload IS NULL, 'payload', x.payload
|
|
26314
|
+
) ORDER BY x.seq), '[]'::jsonb)
|
|
26315
|
+
FROM (
|
|
26316
|
+
SELECT e.seq, e.kind, e.step_id, e.payload,
|
|
26317
|
+
MAX(CASE WHEN e.kind = 'step' THEN e.seq END) OVER (PARTITION BY e.step_id) AS step_seq
|
|
26318
|
+
FROM ${this.#entriesTable()} e
|
|
26319
|
+
WHERE e.workflow_name = r.workflow_name AND e.run_id = r.run_id
|
|
26320
|
+
AND e.kind IN ('step', 'step-patch') AND e.step_id = ANY($3::text[])
|
|
26321
|
+
) x
|
|
26322
|
+
WHERE x.seq >= x.step_seq
|
|
26323
|
+
) AS entries
|
|
26324
|
+
FROM ${this.#runsTable()} r WHERE r.workflow_name = $1 AND r.run_id = $2`, [
|
|
26325
|
+
workflowName,
|
|
26326
|
+
runId,
|
|
26327
|
+
stepIds
|
|
26328
|
+
]);
|
|
26329
|
+
if (!run) return null;
|
|
26330
|
+
if (expectedLastSeq !== void 0 && run.lastSeq !== expectedLastSeq) return null;
|
|
26331
|
+
if (expectedRunToken !== void 0 && run.runToken !== expectedRunToken) return null;
|
|
26332
|
+
return Object.fromEntries(latestStepResults(toEntries(run.entries)));
|
|
26333
|
+
} catch (error) {
|
|
26334
|
+
throw this.#runError("LOAD_WORKFLOW_RUN_STEPS", workflowName, runId, error);
|
|
26335
|
+
}
|
|
26336
|
+
}
|
|
26337
|
+
async updateWorkflowRunHeader({ workflowName, runId, opts, fence: explicitFence }) {
|
|
26338
|
+
const fence = resolveRunFence(this, explicitFence, runId);
|
|
26339
|
+
try {
|
|
26340
|
+
return await this.#db.client.tx(async (t) => {
|
|
26341
|
+
if (fence) await assertRunFence(t, this.#runOwnersTable(), fence, "updateWorkflowRunHeader");
|
|
26342
|
+
const run = await t.oneOrNone(`SELECT header FROM ${this.#runsTable()} WHERE workflow_name = $1 AND run_id = $2 FOR UPDATE`, [workflowName, runId]);
|
|
26343
|
+
if (!run) return void 0;
|
|
26344
|
+
const { expectedStatus, ...patch } = opts;
|
|
26345
|
+
if (!matchesExpectedWorkflowStatus(run.header.status, expectedStatus)) return void 0;
|
|
26346
|
+
const header = {
|
|
26347
|
+
...run.header,
|
|
26348
|
+
...patch
|
|
26349
|
+
};
|
|
26350
|
+
const now = /* @__PURE__ */ new Date();
|
|
26351
|
+
return await t.one(`UPDATE ${this.#runsTable()}
|
|
26352
|
+
SET header = $3::jsonb, status = $4, "updatedAt" = $5, "updatedAtZ" = $6
|
|
26353
|
+
WHERE workflow_name = $1 AND run_id = $2
|
|
26354
|
+
RETURNING header, "lastSeq", "startStateSeq", "runToken"`, [
|
|
26355
|
+
workflowName,
|
|
26356
|
+
runId,
|
|
26357
|
+
toPgJson(header),
|
|
26358
|
+
header.status,
|
|
26359
|
+
now,
|
|
26360
|
+
now
|
|
26361
|
+
]);
|
|
26362
|
+
});
|
|
26363
|
+
} catch (error) {
|
|
26364
|
+
if (isRunFenceConflictError(error)) throw error;
|
|
26365
|
+
throw this.#runError("UPDATE_WORKFLOW_RUN_HEADER", workflowName, runId, error);
|
|
26366
|
+
}
|
|
26367
|
+
}
|
|
26368
|
+
/**
|
|
26369
|
+
* Upserts a storage v2 run's header from `$1`–`$12` ({@link #headerParams}), adds `$7` (the
|
|
26370
|
+
* number of entries the write adds) to `lastSeq`, and returns the new `lastSeq`, `stateShared`
|
|
26371
|
+
* and `runToken`. `$12` is the run token of a run the write creates; a stored run keeps its own.
|
|
26372
|
+
* A checkpoint's state entries come last, so its `startStateSeq` is the new `lastSeq`; an append
|
|
26373
|
+
* keeps the stored one unless given `$13`. The header's row lock is held
|
|
26374
|
+
* until commit, so the writes to one run commit in `seq` order. With `expectedParam` (a text
|
|
26375
|
+
* array of statuses), a stored run whose status isn't in it is left as is and nothing is returned.
|
|
26376
|
+
*/
|
|
26377
|
+
#upsertHeaderSql(checkpoint, expectedParam) {
|
|
26378
|
+
const startStateSeq = checkpoint ? {
|
|
26379
|
+
insert: "$7::int",
|
|
26380
|
+
update: "r.\"lastSeq\" + EXCLUDED.\"lastSeq\""
|
|
26381
|
+
} : {
|
|
26382
|
+
insert: "COALESCE($13::int, 0)",
|
|
26383
|
+
update: "COALESCE($13::int, r.\"startStateSeq\")"
|
|
26384
|
+
};
|
|
26385
|
+
return `INSERT INTO ${this.#runsTable()} AS r
|
|
26386
|
+
(workflow_name, run_id, "resourceId", "threadId", status, header, "lastSeq", "startStateSeq", "stateShared",
|
|
26387
|
+
"runToken", "createdAt", "updatedAt", "createdAtZ", "updatedAtZ")
|
|
26388
|
+
VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7::int, ${startStateSeq.insert}, false, $12, $8, $9, $10, $11)
|
|
26389
|
+
ON CONFLICT (workflow_name, run_id) DO UPDATE
|
|
26390
|
+
SET "resourceId" = COALESCE(EXCLUDED."resourceId", r."resourceId"), "threadId" = EXCLUDED."threadId",
|
|
26391
|
+
status = EXCLUDED.status, header = EXCLUDED.header, "lastSeq" = r."lastSeq" + EXCLUDED."lastSeq",
|
|
26392
|
+
"startStateSeq" = ${startStateSeq.update},
|
|
26393
|
+
"updatedAt" = EXCLUDED."updatedAt", "updatedAtZ" = EXCLUDED."updatedAtZ"
|
|
26394
|
+
${expectedParam ? `WHERE r.status = ANY(${expectedParam}::text[])` : ""}
|
|
26395
|
+
RETURNING "lastSeq", "stateShared", "runToken"`;
|
|
26396
|
+
}
|
|
26397
|
+
#headerParams(run, entryCount) {
|
|
26398
|
+
const now = /* @__PURE__ */ new Date();
|
|
26399
|
+
return [
|
|
26400
|
+
run.workflowName,
|
|
26401
|
+
run.runId,
|
|
26402
|
+
run.resourceId,
|
|
26403
|
+
run.threadId ?? null,
|
|
26404
|
+
run.header.status,
|
|
26405
|
+
toPgJson(run.header),
|
|
26406
|
+
entryCount,
|
|
26407
|
+
now,
|
|
26408
|
+
now,
|
|
26409
|
+
now,
|
|
26410
|
+
now,
|
|
26411
|
+
crypto.randomUUID()
|
|
26412
|
+
];
|
|
26413
|
+
}
|
|
26414
|
+
async writeWorkflowRunCheckpoint({ fence: explicitFence, expectedStatus, steps, state, stateBase, statePatch, ...run }) {
|
|
26415
|
+
const entries = journalEntries(steps, {
|
|
26416
|
+
state,
|
|
26417
|
+
stateBase,
|
|
26418
|
+
statePatch
|
|
26419
|
+
}, true);
|
|
26420
|
+
const fence = resolveRunFence(this, explicitFence, run.runId);
|
|
26421
|
+
const expected = expectedStatus === void 0 ? [] : [[expectedStatus].flat()];
|
|
26422
|
+
try {
|
|
26423
|
+
return await this.#db.client.tx(async (t) => {
|
|
26424
|
+
if (fence) await assertRunFence(t, this.#runOwnersTable(), fence, "writeWorkflowRunCheckpoint");
|
|
26425
|
+
const base = state === void 0 ? stateBase : void 0;
|
|
26426
|
+
let baseLastSeq;
|
|
26427
|
+
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;
|
|
26428
|
+
const header = await t.oneOrNone(this.#upsertHeaderSql(true, expected.length ? "$13" : void 0), [...this.#headerParams(run, entries.length), ...expected]);
|
|
26429
|
+
if (!header) return null;
|
|
26430
|
+
if (base) {
|
|
26431
|
+
await t.none(`UPDATE ${this.#runsTable()} SET "stateShared" = true
|
|
26432
|
+
WHERE workflow_name = $1 AND run_id = $2 AND NOT "stateShared"`, [base.workflowName, base.runId]);
|
|
26433
|
+
const { stored } = await t.one(`SELECT EXISTS (
|
|
26434
|
+
SELECT 1 FROM ${this.#entriesTable()} WHERE workflow_name = $1 AND run_id = $2
|
|
26435
|
+
AND CASE WHEN $4::boolean THEN kind = 'state-draft' AND seq = $3::int
|
|
26436
|
+
ELSE kind IN ('state', 'state-base') AND seq <= $3::int END
|
|
26437
|
+
) AS stored`, [
|
|
26438
|
+
base.workflowName,
|
|
26439
|
+
base.runId,
|
|
26440
|
+
base.seq,
|
|
26441
|
+
base.draft === true
|
|
26442
|
+
]);
|
|
26443
|
+
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`);
|
|
26444
|
+
}
|
|
26445
|
+
const { lastSeq, stateShared, runToken } = header;
|
|
26446
|
+
await t.none(`DELETE FROM ${this.#entriesTable()}
|
|
26447
|
+
WHERE workflow_name = $1 AND run_id = $2${stateShared ? ` AND kind IN ('step', 'step-patch')` : ""}`, [run.workflowName, run.runId]);
|
|
26448
|
+
await t.none(`INSERT INTO ${this.#entriesTable()} (workflow_name, run_id, seq, kind, step_id, payload)
|
|
26449
|
+
SELECT $1, $2, $3::int - $4::int + e.ord, ${ENTRY_VALUES}
|
|
26450
|
+
FROM ${entryRows("$5")}`, [
|
|
26451
|
+
run.workflowName,
|
|
26452
|
+
run.runId,
|
|
26453
|
+
lastSeq,
|
|
26454
|
+
entries.length,
|
|
26455
|
+
toPgJson(entries)
|
|
26456
|
+
]);
|
|
26457
|
+
return {
|
|
26458
|
+
lastSeq,
|
|
26459
|
+
runToken
|
|
26460
|
+
};
|
|
26461
|
+
});
|
|
26462
|
+
} catch (error) {
|
|
26463
|
+
if (isRunFenceConflictError(error)) throw error;
|
|
26464
|
+
throw this.#runError("WRITE_WORKFLOW_RUN_CHECKPOINT", run.workflowName, run.runId, error);
|
|
26465
|
+
}
|
|
26466
|
+
}
|
|
26467
|
+
/** One statement: the header upsert numbers the entries inserted after it. */
|
|
26468
|
+
async appendWorkflowRunEntries({ fence, steps, stepPatches, state, statePatch, startStateSeq, expectedStatus, ...run }) {
|
|
26469
|
+
const entries = journalEntries(steps, {
|
|
26470
|
+
state,
|
|
26471
|
+
statePatch
|
|
26472
|
+
}, false, stepPatches);
|
|
26473
|
+
const expected = expectedStatus === void 0 ? [] : [[expectedStatus].flat()];
|
|
26474
|
+
try {
|
|
26475
|
+
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)}),
|
|
26476
|
+
e AS (
|
|
26477
|
+
INSERT INTO ${this.#entriesTable()} (workflow_name, run_id, seq, kind, step_id, payload)
|
|
26478
|
+
SELECT $1, $2, h."lastSeq" - $7::int + e.ord, ${ENTRY_VALUES}
|
|
26479
|
+
FROM h, ${entryRows("$14")}
|
|
26480
|
+
)
|
|
26481
|
+
SELECT "lastSeq", "runToken" FROM h`, [
|
|
26482
|
+
...this.#headerParams(run, entries.length),
|
|
26483
|
+
startStateSeq ?? null,
|
|
26484
|
+
toPgJson(entries),
|
|
26485
|
+
...expected
|
|
26486
|
+
]));
|
|
26487
|
+
} catch (error) {
|
|
26488
|
+
if (isRunFenceConflictError(error)) throw error;
|
|
26489
|
+
throw this.#runError("APPEND_WORKFLOW_RUN_ENTRIES", run.workflowName, run.runId, error);
|
|
26490
|
+
}
|
|
26491
|
+
}
|
|
26492
|
+
async loadWorkflowRunState({ workflowName, runId, seq }) {
|
|
26493
|
+
try {
|
|
26494
|
+
return (await this.#stateAt(this.#db.client, workflowName, runId, { seq }))?.state ?? null;
|
|
26495
|
+
} catch (error) {
|
|
26496
|
+
throw this.#runError("LOAD_WORKFLOW_RUN_STATE", workflowName, runId, error);
|
|
26497
|
+
}
|
|
26498
|
+
}
|
|
26499
|
+
async appendWorkflowRunStateDraft(args) {
|
|
26500
|
+
const { workflowName, runId } = args;
|
|
26501
|
+
const payload = "state" in args ? { state: args.state } : {
|
|
26502
|
+
on: args.on.draft ? {
|
|
26503
|
+
seq: args.on.seq,
|
|
26504
|
+
draft: true
|
|
26505
|
+
} : { seq: args.on.seq },
|
|
26506
|
+
ops: args.ops
|
|
26507
|
+
};
|
|
26508
|
+
let row;
|
|
26509
|
+
try {
|
|
26510
|
+
row = await withRunFence(this.#db.client, this.#runOwnersTable(), resolveRunFence(this, void 0, runId), "appendWorkflowRunStateDraft", (q) => q.oneOrNone(`WITH h AS (
|
|
26511
|
+
UPDATE ${this.#runsTable()} SET "lastSeq" = "lastSeq" + 1, "stateShared" = true
|
|
26512
|
+
WHERE workflow_name = $1 AND run_id = $2
|
|
26513
|
+
RETURNING "lastSeq"
|
|
26514
|
+
)
|
|
26515
|
+
INSERT INTO ${this.#entriesTable()} (workflow_name, run_id, seq, kind, step_id, payload)
|
|
26516
|
+
SELECT $1, $2, h."lastSeq", 'state-draft', NULL, $3::jsonb FROM h
|
|
26517
|
+
RETURNING seq`, [
|
|
26518
|
+
workflowName,
|
|
26519
|
+
runId,
|
|
26520
|
+
toPgJson(payload)
|
|
26521
|
+
]));
|
|
26522
|
+
} catch (error) {
|
|
26523
|
+
if (isRunFenceConflictError(error)) throw error;
|
|
26524
|
+
throw this.#runError("APPEND_WORKFLOW_RUN_STATE_DRAFT", workflowName, runId, error);
|
|
26525
|
+
}
|
|
26526
|
+
if (!row) throw new Error(`Storage v2 run ${workflowName}/${runId} is not stored`);
|
|
26527
|
+
return row;
|
|
25850
26528
|
}
|
|
25851
26529
|
supportsRunFencing() {
|
|
25852
26530
|
return true;
|
|
@@ -25975,6 +26653,7 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
|
|
|
25975
26653
|
});
|
|
25976
26654
|
const existingSnapshotResult = await t.oneOrNone(`SELECT snapshot FROM ${tableName} WHERE workflow_name = $1 AND run_id = $2 FOR UPDATE`, [workflowName, runId]);
|
|
25977
26655
|
if (!existingSnapshotResult) {
|
|
26656
|
+
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
26657
|
assertWorkflowStepGuard({
|
|
25979
26658
|
snapshot: null,
|
|
25980
26659
|
stepId,
|
|
@@ -26047,11 +26726,36 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
|
|
|
26047
26726
|
}, error);
|
|
26048
26727
|
}
|
|
26049
26728
|
}
|
|
26050
|
-
async updateWorkflowState({ workflowName, runId, opts, fence: explicitFence }) {
|
|
26729
|
+
async updateWorkflowState({ workflowName, runId, opts, fence: explicitFence, storageVersion }) {
|
|
26051
26730
|
const fence = resolveRunFence(this, explicitFence, runId);
|
|
26052
26731
|
try {
|
|
26053
26732
|
return await this.#db.client.tx(async (t) => {
|
|
26054
26733
|
if (fence) await assertRunFence(t, this.#runOwnersTable(), fence, "updateWorkflowState");
|
|
26734
|
+
if (storageVersion !== 1) {
|
|
26735
|
+
const run = await t.oneOrNone(`SELECT header FROM ${this.#runsTable()} WHERE workflow_name = $1 AND run_id = $2 FOR UPDATE`, [workflowName, runId]);
|
|
26736
|
+
if (run) {
|
|
26737
|
+
const { expectedStatus, ...patch } = opts;
|
|
26738
|
+
if (!matchesExpectedWorkflowStatus(run.header.status, expectedStatus)) return;
|
|
26739
|
+
const header = {
|
|
26740
|
+
...run.header,
|
|
26741
|
+
...patch
|
|
26742
|
+
};
|
|
26743
|
+
const now = /* @__PURE__ */ new Date();
|
|
26744
|
+
const { entries } = await t.one(`UPDATE ${this.#runsTable()} r
|
|
26745
|
+
SET header = $3::jsonb, status = $4, "updatedAt" = $5, "updatedAtZ" = $6
|
|
26746
|
+
WHERE r.workflow_name = $1 AND r.run_id = $2
|
|
26747
|
+
RETURNING ${this.#entriesSql("r")} AS entries`, [
|
|
26748
|
+
workflowName,
|
|
26749
|
+
runId,
|
|
26750
|
+
toPgJson(header),
|
|
26751
|
+
header.status,
|
|
26752
|
+
now,
|
|
26753
|
+
now
|
|
26754
|
+
]);
|
|
26755
|
+
return this.#materializeRun(t, header, entries);
|
|
26756
|
+
}
|
|
26757
|
+
if (storageVersion === 2) return void 0;
|
|
26758
|
+
}
|
|
26055
26759
|
const tableName = getTableName({
|
|
26056
26760
|
indexName: TABLE_WORKFLOW_SNAPSHOT,
|
|
26057
26761
|
schemaName: getSchemaName(this.#schema)
|
|
@@ -26131,10 +26835,14 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
|
|
|
26131
26835
|
async loadWorkflowRunStatus({ workflowName, runId }) {
|
|
26132
26836
|
try {
|
|
26133
26837
|
const statusExpr = await this.#statusExpression("snapshot");
|
|
26134
|
-
return (await this.#db.client.oneOrNone(`SELECT
|
|
26838
|
+
return (await this.#db.client.oneOrNone(`SELECT COALESCE(
|
|
26839
|
+
(SELECT status FROM ${this.#runsTable()} WHERE workflow_name = $1 AND run_id = $2),
|
|
26840
|
+
(SELECT ${statusExpr} FROM ${getTableName({
|
|
26135
26841
|
indexName: TABLE_WORKFLOW_SNAPSHOT,
|
|
26136
26842
|
schemaName: getSchemaName(this.#schema)
|
|
26137
|
-
})}
|
|
26843
|
+
})}
|
|
26844
|
+
WHERE "workflow_name" = $1 AND "run_id" = $2 ORDER BY "createdAt" DESC LIMIT 1)
|
|
26845
|
+
) AS status`, [workflowName, runId]))?.status ?? null;
|
|
26138
26846
|
} catch (error) {
|
|
26139
26847
|
throw new MastraError({
|
|
26140
26848
|
id: createStorageErrorId("PG", "LOAD_WORKFLOW_RUN_STATUS", "FAILED"),
|
|
@@ -26147,8 +26855,14 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
|
|
|
26147
26855
|
}, error);
|
|
26148
26856
|
}
|
|
26149
26857
|
}
|
|
26150
|
-
async loadWorkflowSnapshot({ workflowName, runId }) {
|
|
26858
|
+
async loadWorkflowSnapshot({ workflowName, runId, storageVersion }) {
|
|
26151
26859
|
try {
|
|
26860
|
+
if (storageVersion !== 1) {
|
|
26861
|
+
const run = await this.#db.client.oneOrNone(`SELECT r.header, ${this.#entriesSql("r")} AS entries
|
|
26862
|
+
FROM ${this.#runsTable()} r WHERE r.workflow_name = $1 AND r.run_id = $2`, [workflowName, runId]);
|
|
26863
|
+
if (run) return await this.#materializeRun(this.#db.client, run.header, run.entries);
|
|
26864
|
+
if (storageVersion === 2) return null;
|
|
26865
|
+
}
|
|
26152
26866
|
const result = await this.#db.load({
|
|
26153
26867
|
tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
26154
26868
|
keys: {
|
|
@@ -26165,7 +26879,7 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
|
|
|
26165
26879
|
}, error);
|
|
26166
26880
|
}
|
|
26167
26881
|
}
|
|
26168
|
-
async getWorkflowRunById({ runId, workflowName }) {
|
|
26882
|
+
async getWorkflowRunById({ runId, workflowName, storageVersion }) {
|
|
26169
26883
|
try {
|
|
26170
26884
|
const conditions = [];
|
|
26171
26885
|
const values = [];
|
|
@@ -26181,18 +26895,14 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
|
|
|
26181
26895
|
paramIndex++;
|
|
26182
26896
|
}
|
|
26183
26897
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
26184
|
-
const query =
|
|
26185
|
-
|
|
26186
|
-
|
|
26187
|
-
|
|
26188
|
-
})
|
|
26189
|
-
|
|
26190
|
-
ORDER BY "createdAt" DESC LIMIT 1
|
|
26191
|
-
`;
|
|
26192
|
-
const queryValues = values;
|
|
26193
|
-
const result = await this.#db.readClient.oneOrNone(query, queryValues);
|
|
26898
|
+
const query = await this.#runsQuery({
|
|
26899
|
+
v1Where: storageVersion === 2 ? null : whereClause,
|
|
26900
|
+
v2Where: storageVersion === 1 ? null : whereClause,
|
|
26901
|
+
page: "LIMIT 1"
|
|
26902
|
+
});
|
|
26903
|
+
const result = await this.#db.readClient.oneOrNone(query, values);
|
|
26194
26904
|
if (!result) return null;
|
|
26195
|
-
return this.
|
|
26905
|
+
return await this.#parseRun(this.#db.readClient, result);
|
|
26196
26906
|
} catch (error) {
|
|
26197
26907
|
throw new MastraError({
|
|
26198
26908
|
id: createStorageErrorId("PG", "GET_WORKFLOW_RUN_BY_ID", "FAILED"),
|
|
@@ -26205,12 +26915,28 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
|
|
|
26205
26915
|
}, error);
|
|
26206
26916
|
}
|
|
26207
26917
|
}
|
|
26208
|
-
async deleteWorkflowRunById({ runId, workflowName, fence }) {
|
|
26918
|
+
async deleteWorkflowRunById({ runId, workflowName, storageVersion, fence }) {
|
|
26919
|
+
const resolvedFence = resolveRunFence(this, fence, runId);
|
|
26209
26920
|
try {
|
|
26210
|
-
|
|
26211
|
-
|
|
26212
|
-
|
|
26213
|
-
|
|
26921
|
+
if (storageVersion === 1) {
|
|
26922
|
+
await withRunFence(this.#db.client, this.#runOwnersTable(), resolvedFence, "deleteWorkflowRunById", (q) => q.none(`DELETE FROM ${getTableName({
|
|
26923
|
+
indexName: TABLE_WORKFLOW_SNAPSHOT,
|
|
26924
|
+
schemaName: getSchemaName(this.#schema)
|
|
26925
|
+
})} WHERE run_id = $1 AND workflow_name = $2`, [runId, workflowName]));
|
|
26926
|
+
return;
|
|
26927
|
+
}
|
|
26928
|
+
await this.#db.client.tx(async (t) => {
|
|
26929
|
+
if (resolvedFence) await assertRunFence(t, this.#runOwnersTable(), resolvedFence, "deleteWorkflowRunById");
|
|
26930
|
+
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);
|
|
26931
|
+
for (const table of [
|
|
26932
|
+
this.#runsTable(),
|
|
26933
|
+
this.#entriesTable(),
|
|
26934
|
+
...storageVersion === 2 ? [] : [getTableName({
|
|
26935
|
+
indexName: TABLE_WORKFLOW_SNAPSHOT,
|
|
26936
|
+
schemaName: getSchemaName(this.#schema)
|
|
26937
|
+
})]
|
|
26938
|
+
]) await t.none(`DELETE FROM ${table} WHERE run_id = $1 AND workflow_name = $2`, [runId, workflowName]);
|
|
26939
|
+
});
|
|
26214
26940
|
} catch (error) {
|
|
26215
26941
|
if (isRunFenceConflictError(error)) throw error;
|
|
26216
26942
|
throw new MastraError({
|
|
@@ -26227,77 +26953,84 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
|
|
|
26227
26953
|
async listWorkflowRuns({ workflowName, fromDate, toDate, perPage, page, resourceId, threadId, status, summary } = {}) {
|
|
26228
26954
|
try {
|
|
26229
26955
|
const conditions = [];
|
|
26956
|
+
const v2Conditions = [];
|
|
26230
26957
|
const values = [];
|
|
26231
26958
|
let paramIndex = 1;
|
|
26232
26959
|
if (workflowName) {
|
|
26233
26960
|
conditions.push(`workflow_name = $${paramIndex}`);
|
|
26961
|
+
v2Conditions.push(`workflow_name = $${paramIndex}`);
|
|
26234
26962
|
values.push(workflowName);
|
|
26235
26963
|
paramIndex++;
|
|
26236
26964
|
}
|
|
26237
26965
|
if (status) {
|
|
26238
26966
|
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
26967
|
conditions.push(`${statusExpr} = $${paramIndex}`);
|
|
26968
|
+
v2Conditions.push(`status = $${paramIndex}`);
|
|
26240
26969
|
values.push(status);
|
|
26241
26970
|
paramIndex++;
|
|
26242
26971
|
}
|
|
26243
26972
|
if (resourceId) {
|
|
26244
|
-
if (await this.#db.hasColumn(TABLE_WORKFLOW_SNAPSHOT, "resourceId")) {
|
|
26245
|
-
|
|
26246
|
-
|
|
26247
|
-
|
|
26248
|
-
|
|
26973
|
+
if (await this.#db.hasColumn(TABLE_WORKFLOW_SNAPSHOT, "resourceId")) conditions.push(`"resourceId" = $${paramIndex}`);
|
|
26974
|
+
else this.logger?.warn?.(`[${TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
|
|
26975
|
+
v2Conditions.push(`"resourceId" = $${paramIndex}`);
|
|
26976
|
+
values.push(resourceId);
|
|
26977
|
+
paramIndex++;
|
|
26249
26978
|
}
|
|
26250
26979
|
if (threadId) {
|
|
26251
|
-
if (await this.#db.getColumnType(TABLE_WORKFLOW_SNAPSHOT, "snapshot") === "jsonb") {
|
|
26252
|
-
|
|
26253
|
-
|
|
26254
|
-
|
|
26255
|
-
|
|
26980
|
+
if (await this.#db.getColumnType(TABLE_WORKFLOW_SNAPSHOT, "snapshot") === "jsonb") conditions.push(`${WORKFLOW_SNAPSHOT_THREAD_ID_EXPR} = $${paramIndex}`);
|
|
26981
|
+
else this.logger?.warn?.(`[${TABLE_WORKFLOW_SNAPSHOT}] snapshot column is not jsonb. Skipping threadId filter.`);
|
|
26982
|
+
v2Conditions.push(`"threadId" = $${paramIndex}`);
|
|
26983
|
+
values.push(threadId);
|
|
26984
|
+
paramIndex++;
|
|
26256
26985
|
}
|
|
26257
26986
|
if (fromDate) {
|
|
26258
26987
|
conditions.push(`"createdAt" >= $${paramIndex}`);
|
|
26988
|
+
v2Conditions.push(`"createdAt" >= $${paramIndex}`);
|
|
26259
26989
|
values.push(fromDate);
|
|
26260
26990
|
paramIndex++;
|
|
26261
26991
|
}
|
|
26262
26992
|
if (toDate) {
|
|
26263
26993
|
conditions.push(`"createdAt" <= $${paramIndex}`);
|
|
26994
|
+
v2Conditions.push(`"createdAt" <= $${paramIndex}`);
|
|
26264
26995
|
values.push(toDate);
|
|
26265
26996
|
paramIndex++;
|
|
26266
26997
|
}
|
|
26267
26998
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
26268
|
-
|
|
26999
|
+
const v2WhereClause = v2Conditions.length > 0 ? `WHERE ${v2Conditions.join(" AND ")}` : "";
|
|
26269
27000
|
const usePagination = typeof perPage === "number" && typeof page === "number";
|
|
26270
|
-
if (usePagination) {
|
|
26271
|
-
const countResult = await this.#db.client.one(`SELECT COUNT(*) as count FROM ${getTableName({
|
|
26272
|
-
indexName: TABLE_WORKFLOW_SNAPSHOT,
|
|
26273
|
-
schemaName: getSchemaName(this.#schema)
|
|
26274
|
-
})} ${whereClause}`, values);
|
|
26275
|
-
total = Number(countResult.count);
|
|
26276
|
-
}
|
|
26277
27001
|
const normalizedPerPage = usePagination ? normalizePerPage(perPage, Number.MAX_SAFE_INTEGER) : 0;
|
|
26278
|
-
const offset = usePagination ? page * normalizedPerPage :
|
|
26279
|
-
let
|
|
27002
|
+
const offset = usePagination ? page * normalizedPerPage : 0;
|
|
27003
|
+
let v1Snapshot = "snapshot";
|
|
26280
27004
|
if (summary) {
|
|
26281
27005
|
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
|
-
|
|
27006
|
+
v1Snapshot = `jsonb_build_object('status', ${snapshotJson} -> 'status', 'timestamp', ${snapshotJson} -> 'timestamp')`;
|
|
26283
27007
|
}
|
|
26284
|
-
const query =
|
|
26285
|
-
|
|
26286
|
-
|
|
26287
|
-
|
|
26288
|
-
|
|
26289
|
-
|
|
26290
|
-
|
|
26291
|
-
${usePagination ? ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}` : ""}
|
|
26292
|
-
`;
|
|
27008
|
+
const query = await this.#runsQuery({
|
|
27009
|
+
v1Where: whereClause,
|
|
27010
|
+
v2Where: v2WhereClause,
|
|
27011
|
+
v1Snapshot,
|
|
27012
|
+
summary,
|
|
27013
|
+
page: usePagination ? `LIMIT $${paramIndex} OFFSET $${paramIndex + 1}` : ""
|
|
27014
|
+
});
|
|
26293
27015
|
const queryValues = usePagination ? [
|
|
26294
27016
|
...values,
|
|
26295
27017
|
normalizedPerPage,
|
|
26296
27018
|
offset
|
|
26297
27019
|
] : values;
|
|
26298
|
-
const
|
|
26299
|
-
|
|
26300
|
-
|
|
27020
|
+
const result = await this.#db.client.manyOrNone(query, queryValues);
|
|
27021
|
+
const runs = await Promise.all((result || []).map((row) => this.#parseRun(this.#db.client, row)));
|
|
27022
|
+
let total = 0;
|
|
27023
|
+
if (usePagination) {
|
|
27024
|
+
total = offset + runs.length;
|
|
27025
|
+
if (runs.length === normalizedPerPage || runs.length === 0 && offset > 0) {
|
|
27026
|
+
const countResult = await this.#db.client.one(`SELECT (SELECT COUNT(*) FROM ${getTableName({
|
|
27027
|
+
indexName: TABLE_WORKFLOW_SNAPSHOT,
|
|
27028
|
+
schemaName: getSchemaName(this.#schema)
|
|
27029
|
+
})} ${whereClause})
|
|
27030
|
+
+ (SELECT COUNT(*) FROM ${this.#runsTable()} ${v2WhereClause}) AS count`, values);
|
|
27031
|
+
total = Number(countResult.count);
|
|
27032
|
+
}
|
|
27033
|
+
}
|
|
26301
27034
|
return {
|
|
26302
27035
|
runs,
|
|
26303
27036
|
total: total || runs.length
|