@mastra/libsql 1.29.0-alpha.2 → 1.30.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.
Files changed (44) hide show
  1. package/dist/docs/SKILL.md +58 -0
  2. package/dist/docs/assets/SOURCE_MAP.json +152 -0
  3. package/dist/docs/references/docs-agents-human-in-the-loop.md +773 -0
  4. package/dist/docs/references/docs-agents-networks.md +186 -0
  5. package/dist/docs/references/docs-deployment-workers.md +404 -0
  6. package/dist/docs/references/docs-knowledge-load-your-content.md +86 -0
  7. package/dist/docs/references/docs-knowledge-overview.md +106 -0
  8. package/dist/docs/references/docs-knowledge-search.md +63 -0
  9. package/dist/docs/references/docs-memory-memory-processors.md +442 -0
  10. package/dist/docs/references/docs-memory-message-history.md +424 -0
  11. package/dist/docs/references/docs-memory-multi-user-threads.md +210 -0
  12. package/dist/docs/references/docs-memory-overview.md +317 -0
  13. package/dist/docs/references/docs-memory-semantic-recall.md +485 -0
  14. package/dist/docs/references/docs-memory-working-memory.md +450 -0
  15. package/dist/docs/references/docs-signals-overview.md +94 -0
  16. package/dist/docs/references/docs-storage.md +224 -0
  17. package/dist/docs/references/docs-studio-editor.md +372 -0
  18. package/dist/docs/references/docs-workflows-snapshots.md +244 -0
  19. package/dist/docs/references/integrations-channels-github.md +156 -0
  20. package/dist/docs/references/integrations-channels-ring.md +322 -0
  21. package/dist/docs/references/integrations-databases-dynamodb.md +286 -0
  22. package/dist/docs/references/integrations-databases-libsql.md +161 -0
  23. package/dist/docs/references/integrations-vectors-libsql.md +325 -0
  24. package/dist/docs/references/reference-ai-sdk-overview.md +144 -0
  25. package/dist/docs/references/reference-core-getMemory.md +53 -0
  26. package/dist/docs/references/reference-core-listMemory.md +59 -0
  27. package/dist/docs/references/reference-core-mastra-class.md +177 -0
  28. package/dist/docs/references/reference-file-based-agents-memory.md +60 -0
  29. package/dist/docs/references/reference-file-based-agents-storage.md +32 -0
  30. package/dist/docs/references/reference-memory-memory-class.md +237 -0
  31. package/dist/docs/references/reference-rag-retrieval.md +672 -0
  32. package/dist/docs/references/reference-storage-composite.md +433 -0
  33. package/dist/docs/references/reference-storage-retention.md +310 -0
  34. package/dist/index.cjs +821 -44
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.js +822 -45
  37. package/dist/index.js.map +1 -1
  38. package/dist/storage/db/index.d.ts +4 -4
  39. package/dist/storage/db/index.d.ts.map +1 -1
  40. package/dist/storage/domains/workflows/index.d.ts +100 -6
  41. package/dist/storage/domains/workflows/index.d.ts.map +1 -1
  42. package/dist/storage/retention.d.ts +3 -1
  43. package/dist/storage/retention.d.ts.map +1 -1
  44. package/package.json +7 -7
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createClient } from "@libsql/client";
2
2
  import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error";
3
- import { AGENTS_SCHEMA, AGENT_VERSIONS_SCHEMA, AgentsStorage, BackgroundTasksStorage, BlobStore, ChannelsStorage, DATASETS_SCHEMA, DATASET_ITEMS_SCHEMA, DATASET_VERSIONS_SCHEMA, DatasetsStorage, EXPERIMENTS_SCHEMA, EXPERIMENT_RESULTS_SCHEMA, ExperimentsStorage, FAVORITES_SCHEMA, FactoryStorage, FavoritesStorage, HarnessStorage, KnowledgeConflictError, KnowledgeNotFoundError, KnowledgeSchemaResetRequiredError, KnowledgeStorage, MCPClientsStorage, MCPServersStorage, MCP_CLIENTS_SCHEMA, MCP_CLIENT_VERSIONS_SCHEMA, MCP_SERVERS_SCHEMA, MCP_SERVER_VERSIONS_SCHEMA, MastraCompositeStore, MemoryStorage, NotificationsStorage, OBSERVATIONAL_MEMORY_TABLE_SCHEMA, ObservabilityStorage, PROMPT_BLOCKS_SCHEMA, PROMPT_BLOCK_VERSIONS_SCHEMA, PromptBlocksStorage, RUN_FENCING_TABLE_SCHEMAS, RunFenceConflictError, SCORERS_SCHEMA, SCORER_DEFINITIONS_SCHEMA, SCORER_DEFINITION_VERSIONS_SCHEMA, SKILLS_SCHEMA, SKILL_BLOBS_SCHEMA, SKILL_VERSIONS_SCHEMA, SPAN_SCHEMA, 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_HARNESS_SESSIONS, 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, THREAD_STATE_SCHEMA, TOOL_PROVIDER_CONNECTIONS_SCHEMA, ThreadStateStorage, ToolProviderConnectionsStorage, TraceStatus, UniqueViolationError, WORKSPACES_SCHEMA, WORKSPACE_VERSIONS_SCHEMA, WorkflowDefinitionsStorage, WorkflowsStorage, WorkspacesStorage, calculatePagination, createKnowledgeUlid, createStorageErrorId, createVectorErrorId, ensureDate, executeRetentionPrune, getSqlType, hasErrorCode, isRunFenceConflictError, listTracesArgsSchema, matchesExpectedWorkflowStatus, matchesRunFence, mergeWorkflowStepResult, normalizePerPage, normalizeScheduleTarget, resolveListOrderBy, resolveRetentionTargets, resolveRunFence, retentionCutoffMs, runRetentionBatches, safelyParseJSON, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
3
+ import { AGENTS_SCHEMA, AGENT_VERSIONS_SCHEMA, AgentsStorage, BackgroundTasksStorage, BlobStore, ChannelsStorage, DATASETS_SCHEMA, DATASET_ITEMS_SCHEMA, DATASET_VERSIONS_SCHEMA, DatasetsStorage, EXPERIMENTS_SCHEMA, EXPERIMENT_RESULTS_SCHEMA, ExperimentsStorage, FAVORITES_SCHEMA, FactoryStorage, FavoritesStorage, HarnessStorage, KnowledgeConflictError, KnowledgeNotFoundError, KnowledgeSchemaResetRequiredError, KnowledgeStorage, MCPClientsStorage, MCPServersStorage, MCP_CLIENTS_SCHEMA, MCP_CLIENT_VERSIONS_SCHEMA, MCP_SERVERS_SCHEMA, MCP_SERVER_VERSIONS_SCHEMA, MastraCompositeStore, MemoryStorage, NotificationsStorage, OBSERVATIONAL_MEMORY_TABLE_SCHEMA, ObservabilityStorage, PROMPT_BLOCKS_SCHEMA, PROMPT_BLOCK_VERSIONS_SCHEMA, PromptBlocksStorage, RUN_FENCING_TABLE_SCHEMAS, RunFenceConflictError, SCORERS_SCHEMA, SCORER_DEFINITIONS_SCHEMA, SCORER_DEFINITION_VERSIONS_SCHEMA, SKILLS_SCHEMA, SKILL_BLOBS_SCHEMA, SKILL_VERSIONS_SCHEMA, SPAN_SCHEMA, 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_HARNESS_SESSIONS, 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, THREAD_STATE_SCHEMA, TOOL_PROVIDER_CONNECTIONS_SCHEMA, ThreadStateStorage, ToolProviderConnectionsStorage, TraceStatus, UniqueViolationError, WORKFLOW_RUN_JOURNAL_TABLE_CONFIGS, WORKSPACES_SCHEMA, WORKSPACE_VERSIONS_SCHEMA, WorkflowDefinitionsStorage, WorkflowsStorage, WorkspacesStorage, calculatePagination, createKnowledgeUlid, createStorageErrorId, createVectorErrorId, ensureDate, executeRetentionPrune, getSqlType, hasErrorCode, isRunFenceConflictError, latestStepResults, listTracesArgsSchema, matchesExpectedWorkflowStatus, matchesRunFence, materializeState, materializeWorkflowRun, mergeWorkflowStepResult, normalizePerPage, normalizeScheduleTarget, resolveListOrderBy, resolveRetentionTargets, resolveRunFence, retentionCutoffMs, runRetentionBatches, safelyParseJSON, stateBaseOf, 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 { basename, dirname, isAbsolute, join, resolve } from "path";
@@ -1861,6 +1861,21 @@ function resolveClient(config) {
1861
1861
  function assertPositiveLimit(limit) {
1862
1862
  if (!Number.isSafeInteger(limit) || limit <= 0) throw new Error(`prune limit must be a positive integer; received ${limit}`);
1863
1863
  }
1864
+ /**
1865
+ * Parses JSON object/array text in the table's `jsonb` columns only. A `text` column, such as a
1866
+ * span preview of `{}`, must read back as the stored string.
1867
+ */
1868
+ function parseJsonColumns(tableName, row) {
1869
+ const schema = TABLE_SCHEMAS[tableName];
1870
+ return Object.fromEntries(Object.entries(row).map(([k, v]) => {
1871
+ if (schema[k]?.type !== "jsonb" || typeof v !== "string" || !(v.startsWith("{") || v.startsWith("["))) return [k, v];
1872
+ try {
1873
+ return [k, JSON.parse(v)];
1874
+ } catch {
1875
+ return [k, v];
1876
+ }
1877
+ }));
1878
+ }
1864
1879
  var LibSQLDB = class extends MastraBase {
1865
1880
  client;
1866
1881
  maxRetries;
@@ -2156,7 +2171,7 @@ var LibSQLDB = class extends MastraBase {
2156
2171
  /**
2157
2172
  * Selects a single record from the specified table by key(s).
2158
2173
  * Returns the most recently created record if multiple matches exist.
2159
- * Automatically parses JSON string values back to objects/arrays.
2174
+ * Parses `jsonb` column values back to objects/arrays.
2160
2175
  *
2161
2176
  * @typeParam R - The expected return type of the record
2162
2177
  * @param args - The select arguments
@@ -2174,14 +2189,7 @@ var LibSQLDB = class extends MastraBase {
2174
2189
  args: values
2175
2190
  });
2176
2191
  if (!result.rows || result.rows.length === 0) return null;
2177
- const row = result.rows[0];
2178
- return Object.fromEntries(Object.entries(row || {}).map(([k, v]) => {
2179
- try {
2180
- return [k, typeof v === "string" ? v.startsWith("{") || v.startsWith("[") ? JSON.parse(v) : v : v];
2181
- } catch {
2182
- return [k, v];
2183
- }
2184
- }));
2192
+ return parseJsonColumns(tableName, result.rows[0] ?? {});
2185
2193
  }
2186
2194
  /**
2187
2195
  * Selects multiple records from the specified table with optional filtering, ordering, and pagination.
@@ -2206,15 +2214,7 @@ var LibSQLDB = class extends MastraBase {
2206
2214
  return ((await this.client.execute({
2207
2215
  sql: statement,
2208
2216
  args: [...whereClause?.args ?? [], ...args ?? []]
2209
- })).rows ?? []).map((row) => {
2210
- return Object.fromEntries(Object.entries(row || {}).map(([k, v]) => {
2211
- try {
2212
- return [k, typeof v === "string" ? v.startsWith("{") || v.startsWith("[") ? JSON.parse(v) : v : v];
2213
- } catch {
2214
- return [k, v];
2215
- }
2216
- }));
2217
- });
2217
+ })).rows ?? []).map((row) => parseJsonColumns(tableName, row ?? {}));
2218
2218
  }
2219
2219
  /**
2220
2220
  * Returns the total count of records matching the optional WHERE clause.
@@ -3362,14 +3362,14 @@ function cutoffFor(policy, anchorType, now = Date.now()) {
3362
3362
  return anchorType === "epoch-ms" ? cutoffMs : new Date(cutoffMs).toISOString();
3363
3363
  }
3364
3364
  const runBatchedDelete = runRetentionBatches;
3365
- function runPrune({ db, domain, targets, options, logger }) {
3365
+ function runPrune({ db, domain, targets, options, logger, deleteBatch }) {
3366
3366
  return executeRetentionPrune({
3367
3367
  domain,
3368
3368
  targets,
3369
3369
  options,
3370
3370
  beforeTarget: (target) => ensureAnchorIndex(db, target, logger),
3371
3371
  cutoffFor: (target, now) => cutoffFor(target.policy, target.anchorType ?? "timestamp", now),
3372
- deleteBatch: (target, cutoff, limit) => db.pruneBatch({
3372
+ deleteBatch: (target, cutoff, limit) => deleteBatch?.(target, cutoff, limit) ?? db.pruneBatch({
3373
3373
  tableName: target.table,
3374
3374
  column: target.column,
3375
3375
  cutoff,
@@ -15135,6 +15135,228 @@ var WorkflowDefinitionsLibSQL = class extends WorkflowDefinitionsStorage {
15135
15135
  };
15136
15136
  // src/storage/domains/workflows/index.ts
15137
15137
  const RUN_OWNER_COLUMNS = `generation, ownerId, leaseExpiresAt, ${DB_NOW_MS} AS nowMs`;
15138
+ /**
15139
+ * The entries a storage v2 write adds: one per step, in order, then one per step patch, then the
15140
+ * state or state base (a checkpoint with neither starts the state over at `{}`), then the state
15141
+ * patch unless a full state was given.
15142
+ */
15143
+ function journalEntries(steps, { state, stateBase, statePatch }, checkpoint, stepPatches = {}) {
15144
+ const entries = Object.entries(steps).map(([stepId, result]) => ({
15145
+ kind: "step",
15146
+ step_id: stepId,
15147
+ payload: result
15148
+ }));
15149
+ for (const [stepId, ops] of Object.entries(stepPatches)) entries.push({
15150
+ kind: "step-patch",
15151
+ step_id: stepId,
15152
+ payload: ops
15153
+ });
15154
+ if (state !== void 0) entries.push({
15155
+ kind: "state",
15156
+ step_id: null,
15157
+ payload: state
15158
+ });
15159
+ else if (stateBase !== void 0) entries.push({
15160
+ kind: "state-base",
15161
+ step_id: null,
15162
+ payload: stateBase
15163
+ });
15164
+ else if (checkpoint) entries.push({
15165
+ kind: "state",
15166
+ step_id: null,
15167
+ payload: {}
15168
+ });
15169
+ if (state === void 0 && statePatch !== void 0) entries.push({
15170
+ kind: "state-patch",
15171
+ step_id: null,
15172
+ payload: statePatch
15173
+ });
15174
+ return entries;
15175
+ }
15176
+ /**
15177
+ * Upserts a storage v2 run's header, adds `entryCount` (the number of entries the write adds) to
15178
+ * its `lastSeq`, for {@link insertEntries} to number them up to, and returns the new `lastSeq` and
15179
+ * the `runToken` (picked when the write creates the run, otherwise the stored one). A checkpoint's
15180
+ * state entries come last, so its `startStateSeq` is the new `lastSeq`; an append keeps the stored
15181
+ * one unless given `startStateSeq`.
15182
+ */
15183
+ function upsertHeader(run, entryCount, write) {
15184
+ const now = (/* @__PURE__ */ new Date()).toISOString();
15185
+ const startStateSeq = write.checkpoint ? {
15186
+ insert: "?",
15187
+ update: `${TABLE_WORKFLOW_RUNS}.lastSeq + excluded.lastSeq`,
15188
+ args: [entryCount]
15189
+ } : {
15190
+ insert: "COALESCE(?, 0)",
15191
+ update: `COALESCE(?, ${TABLE_WORKFLOW_RUNS}.startStateSeq)`,
15192
+ args: [write.startStateSeq ?? null]
15193
+ };
15194
+ return {
15195
+ sql: `INSERT INTO ${TABLE_WORKFLOW_RUNS}
15196
+ (workflow_name, run_id, resourceId, threadId, status, header, lastSeq, startStateSeq, stateShared, runToken, createdAt, updatedAt)
15197
+ VALUES (?, ?, ?, ?, ?, jsonb(?), ?, ${startStateSeq.insert}, 0, ?, ?, ?)
15198
+ ON CONFLICT(workflow_name, run_id) DO UPDATE SET
15199
+ resourceId = COALESCE(excluded.resourceId, ${TABLE_WORKFLOW_RUNS}.resourceId), threadId = excluded.threadId,
15200
+ status = excluded.status, header = excluded.header, lastSeq = ${TABLE_WORKFLOW_RUNS}.lastSeq + excluded.lastSeq,
15201
+ startStateSeq = ${startStateSeq.update}, updatedAt = excluded.updatedAt
15202
+ RETURNING lastSeq, runToken`,
15203
+ args: [
15204
+ run.workflowName,
15205
+ run.runId,
15206
+ run.resourceId ?? null,
15207
+ run.threadId ?? null,
15208
+ run.header.status,
15209
+ safeStringify(run.header),
15210
+ entryCount,
15211
+ ...startStateSeq.args,
15212
+ crypto.randomUUID(),
15213
+ now,
15214
+ now,
15215
+ ...write.checkpoint ? [] : startStateSeq.args
15216
+ ]
15217
+ };
15218
+ }
15219
+ /**
15220
+ * Inserts the entries in the JSON array parameter after the run's header has been bumped by
15221
+ * their count, numbering them up to the header's `lastSeq`. `-> '$.payload'` keeps a JSON null
15222
+ * apart from an undefined payload (dropped by `JSON.stringify`), which becomes SQL NULL.
15223
+ * Args: entry count, entries JSON, workflow name, run id.
15224
+ */
15225
+ const INSERT_ENTRIES = `INSERT INTO ${TABLE_WORKFLOW_RUN_ENTRIES} (workflow_name, run_id, seq, kind, step_id, payload)
15226
+ SELECT h.workflow_name, h.run_id, h.lastSeq - ? + j.key + 1, j.value ->> '$.kind', j.value ->> '$.step_id', jsonb(j.value -> '$.payload')
15227
+ FROM ${TABLE_WORKFLOW_RUNS} h, json_each(?) j
15228
+ WHERE h.workflow_name = ? AND h.run_id = ?`;
15229
+ function insertEntries(run, entries) {
15230
+ return {
15231
+ sql: INSERT_ENTRIES,
15232
+ args: [
15233
+ entries.length,
15234
+ safeStringify(entries),
15235
+ run.workflowName,
15236
+ run.runId
15237
+ ]
15238
+ };
15239
+ }
15240
+ /**
15241
+ * The run a `state-base` entry's payload names: the expressions of the state base index, which
15242
+ * a lookup must repeat exactly to use it.
15243
+ */
15244
+ const STATE_BASE_WORKFLOW_EXPR = `payload ->> '$.workflowName'`;
15245
+ const STATE_BASE_RUN_EXPR = `payload ->> '$.runId'`;
15246
+ /**
15247
+ * A JSON array of {@link EntryRow} for the storage v2 run `alias` refers to: each step's
15248
+ * latest entry, the step patches after it (for `latestStepResults`), and the entries
15249
+ * `materializeState` uses for the latest state: the latest full state or state base and every
15250
+ * state patch after it. Drafts are left out.
15251
+ *
15252
+ * With `summary`, successful steps other than `input` come back as their summary, built here
15253
+ * so their data is never sent, unless step patches follow it (the caller summarizes the patched
15254
+ * result instead). Like `summarizeStepResult`, it keeps a field that is JSON null
15255
+ * and leaves out one that is absent. It then takes one arg, `STEP_RESULT_SUMMARY_FIELDS` as
15256
+ * JSON, bound where this SQL sits in the statement.
15257
+ */
15258
+ function entriesSql(alias, { summary } = {}) {
15259
+ let payload = "json(x.payload)";
15260
+ if (summary) {
15261
+ const summarized = `(x.kind = 'step' AND x.step_id <> 'input' AND x.payload ->> '$.status' = 'success'
15262
+ AND (x.patch_seq IS NULL OR x.patch_seq < x.seq))`;
15263
+ payload = `json(CASE WHEN ${summarized} THEN (
15264
+ SELECT json_group_object(f.key, json(x.payload -> f.fullkey))
15265
+ FROM json_each(x.payload) f WHERE f.key IN (SELECT value FROM json_each(?))
15266
+ ) ELSE x.payload END), 'summarized', ${summarized}`;
15267
+ }
15268
+ return `(SELECT json_group_array(json_object(
15269
+ 'seq', x.seq, 'ord', CASE WHEN x.kind = 'step' THEN x.first_seq ELSE x.seq END, 'kind', x.kind, 'step_id', x.step_id,
15270
+ 'absent', x.payload IS NULL, 'payload', ${payload}
15271
+ ))
15272
+ FROM (
15273
+ SELECT e.seq, e.kind, e.step_id, e.payload,
15274
+ ROW_NUMBER() OVER (PARTITION BY e.kind, e.step_id ORDER BY e.seq DESC) AS rn,
15275
+ MIN(e.seq) OVER (PARTITION BY e.kind, e.step_id) AS first_seq,
15276
+ MAX(CASE WHEN e.kind = 'step' THEN e.seq END) OVER (PARTITION BY e.step_id) AS step_seq,
15277
+ MAX(CASE WHEN e.kind = 'step-patch' THEN e.seq END) OVER (PARTITION BY e.step_id) AS patch_seq,
15278
+ MAX(CASE WHEN e.kind IN ('state', 'state-base') THEN e.seq END) OVER () AS base_seq
15279
+ FROM ${TABLE_WORKFLOW_RUN_ENTRIES} e
15280
+ WHERE e.workflow_name = ${alias}.workflow_name AND e.run_id = ${alias}.run_id AND e.kind <> 'state-draft'
15281
+ ) x
15282
+ WHERE CASE x.kind
15283
+ WHEN 'step' THEN x.rn = 1
15284
+ WHEN 'step-patch' THEN x.seq > x.step_seq
15285
+ ELSE x.base_seq IS NULL OR x.seq >= x.base_seq
15286
+ END)`;
15287
+ }
15288
+ /**
15289
+ * A JSON array of {@link EntryRow} with the entries `materializeState` uses for the state of the
15290
+ * storage v2 run `alias` refers to as of a version. For a draft, every state entry up to it, as
15291
+ * the drafts it is on can build on any earlier version; otherwise the latest full state or state
15292
+ * base up to it and the state patches after it. Takes two args, bound where this SQL sits in the
15293
+ * statement: the version's `seq`, then 1 when it is a draft, else 0.
15294
+ */
15295
+ function stateEntriesSql(alias) {
15296
+ return `(SELECT json_group_array(json_object(
15297
+ 'seq', x.seq, 'ord', x.seq, 'kind', x.kind, 'step_id', x.step_id, 'absent', x.payload IS NULL, 'payload', json(x.payload)
15298
+ ))
15299
+ FROM (
15300
+ SELECT e.seq, e.kind, e.step_id, e.payload,
15301
+ MAX(CASE WHEN e.kind IN ('state', 'state-base') THEN e.seq END) OVER () AS base_seq
15302
+ FROM ${TABLE_WORKFLOW_RUN_ENTRIES} e
15303
+ WHERE e.workflow_name = ${alias}.workflow_name AND e.run_id = ${alias}.run_id
15304
+ AND e.kind NOT IN ('step', 'step-patch') AND e.seq <= ?
15305
+ ) x
15306
+ WHERE ? OR (x.kind <> 'state-draft' AND (x.base_seq IS NULL OR x.seq >= x.base_seq)))`;
15307
+ }
15308
+ /** `entriesSql`, `stateEntriesSql` or `loadWorkflowRunSteps` rows, in the order to apply them. */
15309
+ function entryRows(json) {
15310
+ return JSON.parse(json).sort((a, b) => a.ord - b.ord);
15311
+ }
15312
+ /** The entries of {@link entryRows}; an undefined payload reads back as undefined, as in v1. */
15313
+ function toEntries(rows) {
15314
+ return rows.map((row) => {
15315
+ const payload = row.absent ? void 0 : row.payload;
15316
+ switch (row.kind) {
15317
+ case "step": return {
15318
+ seq: row.seq,
15319
+ kind: "step",
15320
+ stepId: row.step_id,
15321
+ result: payload
15322
+ };
15323
+ case "step-patch": return {
15324
+ seq: row.seq,
15325
+ kind: "step-patch",
15326
+ stepId: row.step_id,
15327
+ ops: payload
15328
+ };
15329
+ case "state": return {
15330
+ seq: row.seq,
15331
+ kind: "state",
15332
+ state: payload
15333
+ };
15334
+ case "state-patch": return {
15335
+ seq: row.seq,
15336
+ kind: "state-patch",
15337
+ ops: payload
15338
+ };
15339
+ case "state-base": return {
15340
+ seq: row.seq,
15341
+ kind: "state-base",
15342
+ base: payload
15343
+ };
15344
+ case "state-draft": {
15345
+ const draft = payload;
15346
+ return "state" in draft ? {
15347
+ seq: row.seq,
15348
+ kind: "state-draft",
15349
+ state: draft.state
15350
+ } : {
15351
+ seq: row.seq,
15352
+ kind: "state-draft",
15353
+ on: draft.on,
15354
+ ops: draft.ops
15355
+ };
15356
+ }
15357
+ }
15358
+ });
15359
+ }
15138
15360
  function toRunOwnershipRecord(runId, row) {
15139
15361
  const leaseExpiresAt = row.leaseExpiresAt === null ? null : Number(row.leaseExpiresAt);
15140
15362
  return {
@@ -15225,14 +15447,36 @@ var WorkflowsLibSQL = class WorkflowsLibSQL extends WorkflowsStorage {
15225
15447
  tableName: TABLE_WORKFLOW_RUN_OWNERS,
15226
15448
  schema: RUN_FENCING_TABLE_SCHEMAS[TABLE_WORKFLOW_RUN_OWNERS]
15227
15449
  });
15450
+ for (const [tableName, config] of Object.entries(WORKFLOW_RUN_JOURNAL_TABLE_CONFIGS)) await this.#db.createTable({
15451
+ tableName,
15452
+ schema: config.columns,
15453
+ compositePrimaryKey: config.compositePrimaryKey
15454
+ });
15455
+ await withClientWriteLock(this.#client, () => this.#client.batch([
15456
+ `CREATE INDEX IF NOT EXISTS mastra_workflow_runs_name_createdat_idx ON ${TABLE_WORKFLOW_RUNS} (workflow_name, createdAt DESC)`,
15457
+ `CREATE INDEX IF NOT EXISTS mastra_workflow_runs_name_status_createdat_idx ON ${TABLE_WORKFLOW_RUNS} (workflow_name, status, createdAt DESC)`,
15458
+ `CREATE INDEX IF NOT EXISTS mastra_workflow_runs_threadid_idx ON ${TABLE_WORKFLOW_RUNS} (threadId)`,
15459
+ `CREATE INDEX IF NOT EXISTS mastra_workflow_run_entries_step_idx ON ${TABLE_WORKFLOW_RUN_ENTRIES} (workflow_name, run_id, kind, step_id, seq)`,
15460
+ `CREATE INDEX IF NOT EXISTS mastra_workflow_run_entries_state_base_idx ON ${TABLE_WORKFLOW_RUN_ENTRIES}
15461
+ (${STATE_BASE_WORKFLOW_EXPR}, ${STATE_BASE_RUN_EXPR}) WHERE kind = 'state-base'`
15462
+ ].map((sql) => ({
15463
+ sql,
15464
+ args: []
15465
+ })), "write"));
15228
15466
  }
15229
15467
  async dangerouslyClearAll() {
15230
15468
  await this.#db.deleteData({ tableName: TABLE_WORKFLOW_SNAPSHOT });
15231
15469
  await this.#db.deleteData({ tableName: TABLE_WORKFLOW_RUN_OWNERS });
15470
+ await this.#db.deleteData({ tableName: TABLE_WORKFLOW_RUNS });
15471
+ await this.#db.deleteData({ tableName: TABLE_WORKFLOW_RUN_ENTRIES });
15232
15472
  }
15233
15473
  supportsRunFencing() {
15234
15474
  return true;
15235
15475
  }
15476
+ /** @experimental */
15477
+ supportsRunJournal() {
15478
+ return true;
15479
+ }
15236
15480
  #runFenceCheck(fence, runId, operation) {
15237
15481
  const resolved = resolveRunFence(this, fence, runId);
15238
15482
  return resolved && {
@@ -15241,6 +15485,399 @@ var WorkflowsLibSQL = class WorkflowsLibSQL extends WorkflowsStorage {
15241
15485
  operation
15242
15486
  };
15243
15487
  }
15488
+ /**
15489
+ * Runs a storage v2 write's statements behind the run's fence. With `expectedStatus` or
15490
+ * `stateBase`, in a write transaction that first reads the stored run: it writes nothing
15491
+ * (resolving to null) when the run is stored and its status doesn't match `expectedStatus`, and
15492
+ * throws when the state version `stateBase` names isn't stored.
15493
+ */
15494
+ #writeRun(check, run, { expectedStatus, stateBase }, statements) {
15495
+ if (expectedStatus === void 0 && stateBase === void 0) return withRunFence(this.#client, check, (writer) => writer.batch(statements));
15496
+ return inWriteTransaction(this.#client, async (tx) => {
15497
+ if (check) await assertRunFence(tx, check);
15498
+ if (expectedStatus !== void 0) {
15499
+ const { rows } = await tx.execute({
15500
+ sql: `SELECT status FROM ${TABLE_WORKFLOW_RUNS} WHERE workflow_name = ? AND run_id = ?`,
15501
+ args: [run.workflowName, run.runId]
15502
+ });
15503
+ if (rows[0] && !matchesExpectedWorkflowStatus(rows[0].status, expectedStatus)) return null;
15504
+ }
15505
+ if (stateBase !== void 0) {
15506
+ const { rows } = await tx.execute({
15507
+ sql: `SELECT 1 FROM ${TABLE_WORKFLOW_RUN_ENTRIES} WHERE workflow_name = ? AND run_id = ?
15508
+ AND CASE WHEN ? THEN kind = 'state-draft' AND seq = ? ELSE kind IN ('state', 'state-base') AND seq <= ? END
15509
+ AND ? <= (SELECT lastSeq FROM ${TABLE_WORKFLOW_RUNS} WHERE workflow_name = ? AND run_id = ?)
15510
+ LIMIT 1`,
15511
+ args: [
15512
+ stateBase.workflowName,
15513
+ stateBase.runId,
15514
+ stateBase.draft ? 1 : 0,
15515
+ stateBase.seq,
15516
+ stateBase.seq,
15517
+ stateBase.seq,
15518
+ stateBase.workflowName,
15519
+ stateBase.runId
15520
+ ]
15521
+ });
15522
+ if (!rows[0]) throw new Error(`Workflow run ${run.workflowName}/${run.runId} builds on state version ${stateBase.seq} of ${stateBase.workflowName}/${stateBase.runId}, which is not stored`);
15523
+ }
15524
+ const results = [];
15525
+ for (const statement of statements) results.push(await tx.execute(statement));
15526
+ return results;
15527
+ });
15528
+ }
15529
+ /**
15530
+ * Selects v1 snapshot rows and storage v2 runs as one result, newest first. Each version
15531
+ * has its own WHERE clause; `null` leaves it out. Storage v2 rows carry their header and
15532
+ * entries, or in summary mode only the summary snapshot. `page` is appended to the
15533
+ * combined, ordered rows (e.g. `LIMIT 1`). Bind the v1 args, then the v2 args, then `page`'s.
15534
+ */
15535
+ #runsSql({ v1Where, v2Where, summary, page = "" }) {
15536
+ const branches = [];
15537
+ if (v1Where !== null) {
15538
+ const snapshot = summary ? `json_object('status', json_extract(snapshot, '$.status'), 'timestamp', json_extract(snapshot, '$.timestamp'))` : "json(snapshot)";
15539
+ branches.push(`SELECT workflow_name, run_id, resourceId, ${snapshot} AS snapshot, NULL AS header, createdAt, updatedAt, 1 AS version
15540
+ FROM ${TABLE_WORKFLOW_SNAPSHOT} ${v1Where}`);
15541
+ }
15542
+ if (v2Where !== null) {
15543
+ const columns = summary ? `json_object('status', status, 'timestamp', json_extract(header, '$.timestamp')) AS snapshot, NULL AS header` : "NULL AS snapshot, json(header) AS header";
15544
+ branches.push(`SELECT workflow_name, run_id, resourceId, ${columns}, createdAt, updatedAt, 2 AS version
15545
+ FROM ${TABLE_WORKFLOW_RUNS} ${v2Where}`);
15546
+ }
15547
+ return `SELECT p.*${summary ? "" : `, CASE WHEN p.version = 2 THEN ${entriesSql("p")} END AS entries`}
15548
+ FROM (${branches.join(" UNION ALL ")} ORDER BY createdAt DESC ${page}) p
15549
+ ORDER BY p.createdAt DESC`;
15550
+ }
15551
+ /**
15552
+ * The `base` that `materializeState` takes for a run's entries (as of `at`, or latest): the state
15553
+ * version of another run it builds on (`stateBaseOf`), resolved recursively, else undefined. `{}`
15554
+ * when that run isn't stored. One query per run in the chain.
15555
+ */
15556
+ async #stateBase(q, entries, at) {
15557
+ const ref = stateBaseOf(entries, at);
15558
+ if (!ref) return void 0;
15559
+ return (await this.#stateAt(q, ref.workflowName, ref.runId, ref))?.state ?? {};
15560
+ }
15561
+ /** A storage v2 run's state as of the version `at`, or null when the run isn't stored. */
15562
+ async #stateAt(q, workflowName, runId, at) {
15563
+ const row = (await q.execute({
15564
+ sql: `SELECT ${stateEntriesSql("r")} AS entries
15565
+ FROM ${TABLE_WORKFLOW_RUNS} r WHERE r.workflow_name = ? AND r.run_id = ?`,
15566
+ args: [
15567
+ at.seq,
15568
+ at.draft === true ? 1 : 0,
15569
+ workflowName,
15570
+ runId
15571
+ ]
15572
+ })).rows[0];
15573
+ if (!row) return null;
15574
+ const entries = toEntries(entryRows(row.entries));
15575
+ return materializeState(entries, {
15576
+ at,
15577
+ base: await this.#stateBase(q, entries, at)
15578
+ });
15579
+ }
15580
+ /**
15581
+ * Before a run is deleted, stores the full state in place of every `state-base` entry built on
15582
+ * it, so the runs built on it keep reading the same state. Runs inside the delete's transaction.
15583
+ */
15584
+ async #detachStateChildren(q, workflowName, runId) {
15585
+ const children = await q.execute({
15586
+ sql: `SELECT workflow_name, run_id, seq, json(payload) AS payload FROM ${TABLE_WORKFLOW_RUN_ENTRIES}
15587
+ WHERE kind = 'state-base' AND ${STATE_BASE_WORKFLOW_EXPR} = ? AND ${STATE_BASE_RUN_EXPR} = ?`,
15588
+ args: [workflowName, runId]
15589
+ });
15590
+ const states = /* @__PURE__ */ new Map();
15591
+ for (const child of children.rows) {
15592
+ const ref = JSON.parse(child.payload);
15593
+ const version = `${ref.seq}:${ref.draft === true}`;
15594
+ let state = states.get(version);
15595
+ if (!state) {
15596
+ state = (await this.#stateAt(q, workflowName, runId, ref))?.state ?? {};
15597
+ states.set(version, state);
15598
+ }
15599
+ await q.execute({
15600
+ sql: `UPDATE ${TABLE_WORKFLOW_RUN_ENTRIES} SET kind = 'state', payload = jsonb(?)
15601
+ WHERE workflow_name = ? AND run_id = ? AND seq = ?`,
15602
+ args: [
15603
+ safeStringify(state),
15604
+ child.workflow_name,
15605
+ child.run_id,
15606
+ child.seq
15607
+ ]
15608
+ });
15609
+ }
15610
+ }
15611
+ /** Rebuilds a storage v2 run's `WorkflowRunState` from its header and `entriesSql` JSON. */
15612
+ async #materializeRun(q, header, rows) {
15613
+ const parsedHeader = typeof header === "string" ? JSON.parse(header) : header;
15614
+ const entries = toEntries(entryRows(rows));
15615
+ return materializeWorkflowRun(parsedHeader, entries, await this.#stateBase(q, entries));
15616
+ }
15617
+ /** Parses a row of `#runsSql`. */
15618
+ async #parseRun(row) {
15619
+ if (Number(row.version) !== 2) return this.parseWorkflowRun(row);
15620
+ const snapshot = row.entries ? await this.#materializeRun(this.#client, row.header, row.entries) : row.snapshot;
15621
+ return {
15622
+ ...this.parseWorkflowRun({
15623
+ ...row,
15624
+ snapshot
15625
+ }),
15626
+ storageVersion: 2
15627
+ };
15628
+ }
15629
+ #runError(operation, workflowName, runId, error) {
15630
+ return new MastraError({
15631
+ id: createStorageErrorId("LIBSQL", operation, "FAILED"),
15632
+ domain: ErrorDomain.STORAGE,
15633
+ category: ErrorCategory.THIRD_PARTY,
15634
+ details: {
15635
+ workflowName,
15636
+ runId
15637
+ }
15638
+ }, error);
15639
+ }
15640
+ async loadWorkflowRunHeader({ workflowName, runId }) {
15641
+ try {
15642
+ const row = (await this.#client.execute({
15643
+ sql: `SELECT json(header) AS header, lastSeq, startStateSeq, runToken FROM ${TABLE_WORKFLOW_RUNS} WHERE workflow_name = ? AND run_id = ?`,
15644
+ args: [workflowName, runId]
15645
+ })).rows[0];
15646
+ return row ? {
15647
+ header: JSON.parse(row.header),
15648
+ lastSeq: Number(row.lastSeq),
15649
+ startStateSeq: Number(row.startStateSeq),
15650
+ runToken: row.runToken
15651
+ } : null;
15652
+ } catch (error) {
15653
+ throw this.#runError("LOAD_WORKFLOW_RUN_HEADER", workflowName, runId, error);
15654
+ }
15655
+ }
15656
+ async loadWorkflowRun({ workflowName, runId, successfulSteps }) {
15657
+ try {
15658
+ const summary = successfulSteps === "summary";
15659
+ const run = (await this.#client.execute({
15660
+ sql: `SELECT json(r.header) AS header, r.lastSeq, r.startStateSeq, r.runToken, ${entriesSql("r", { summary })} AS entries
15661
+ FROM ${TABLE_WORKFLOW_RUNS} r WHERE r.workflow_name = ? AND r.run_id = ?`,
15662
+ args: summary ? [
15663
+ JSON.stringify(STEP_RESULT_SUMMARY_FIELDS),
15664
+ workflowName,
15665
+ runId
15666
+ ] : [workflowName, runId]
15667
+ })).rows[0];
15668
+ if (!run) return null;
15669
+ const header = JSON.parse(run.header);
15670
+ const rows = entryRows(run.entries);
15671
+ const entries = toEntries(rows);
15672
+ const summarizedInQuery = new Set(rows.filter((row) => row.summarized).map((row) => row.step_id));
15673
+ const steps = {};
15674
+ const summarizedStepIds = [];
15675
+ for (const [stepId, result] of latestStepResults(entries)) {
15676
+ steps[stepId] = result;
15677
+ if (summary && stepId !== "input" && result?.status === "success") {
15678
+ if (!summarizedInQuery.has(stepId)) steps[stepId] = summarizeStepResult(result);
15679
+ summarizedStepIds.push(stepId);
15680
+ }
15681
+ }
15682
+ const base = await this.#stateBase(this.#client, entries);
15683
+ const { state, statePatchBytes } = materializeState(entries, { base });
15684
+ return {
15685
+ header,
15686
+ lastSeq: Number(run.lastSeq),
15687
+ startStateSeq: Number(run.startStateSeq),
15688
+ runToken: run.runToken,
15689
+ state,
15690
+ statePatchBytes,
15691
+ steps,
15692
+ summarizedStepIds
15693
+ };
15694
+ } catch (error) {
15695
+ throw this.#runError("LOAD_WORKFLOW_RUN", workflowName, runId, error);
15696
+ }
15697
+ }
15698
+ async loadWorkflowRunSteps({ workflowName, runId, stepIds, expectedLastSeq, expectedRunToken }) {
15699
+ try {
15700
+ const run = (await this.#client.execute({
15701
+ sql: `SELECT r.lastSeq, r.runToken, (SELECT json_group_array(json_object(
15702
+ 'seq', x.seq, 'ord', x.seq, 'kind', x.kind, 'step_id', x.step_id, 'absent', x.payload IS NULL, 'payload', json(x.payload)
15703
+ ))
15704
+ FROM (
15705
+ SELECT e.seq, e.kind, e.step_id, e.payload,
15706
+ MAX(CASE WHEN e.kind = 'step' THEN e.seq END) OVER (PARTITION BY e.step_id) AS step_seq
15707
+ FROM ${TABLE_WORKFLOW_RUN_ENTRIES} e
15708
+ WHERE e.workflow_name = r.workflow_name AND e.run_id = r.run_id AND e.kind IN ('step', 'step-patch')
15709
+ AND e.step_id IN (SELECT value FROM json_each(?))
15710
+ ) x
15711
+ WHERE x.seq >= x.step_seq) AS entries
15712
+ FROM ${TABLE_WORKFLOW_RUNS} r WHERE r.workflow_name = ? AND r.run_id = ?`,
15713
+ args: [
15714
+ JSON.stringify(stepIds),
15715
+ workflowName,
15716
+ runId
15717
+ ]
15718
+ })).rows[0];
15719
+ if (!run) return null;
15720
+ if (expectedLastSeq !== void 0 && Number(run.lastSeq) !== expectedLastSeq) return null;
15721
+ if (expectedRunToken !== void 0 && run.runToken !== expectedRunToken) return null;
15722
+ return Object.fromEntries(latestStepResults(toEntries(entryRows(run.entries))));
15723
+ } catch (error) {
15724
+ throw this.#runError("LOAD_WORKFLOW_RUN_STEPS", workflowName, runId, error);
15725
+ }
15726
+ }
15727
+ async updateWorkflowRunHeader({ workflowName, runId, opts, fence }) {
15728
+ const check = this.#runFenceCheck(fence, runId, "updateWorkflowRunHeader");
15729
+ try {
15730
+ return await this.executeWithRetry(() => inWriteTransaction(this.#client, async (tx) => {
15731
+ if (check) await assertRunFence(tx, check);
15732
+ const row = (await tx.execute({
15733
+ sql: `SELECT json(header) AS header, lastSeq, startStateSeq, runToken FROM ${TABLE_WORKFLOW_RUNS} WHERE workflow_name = ? AND run_id = ?`,
15734
+ args: [workflowName, runId]
15735
+ })).rows[0];
15736
+ if (!row) return void 0;
15737
+ const current = JSON.parse(row.header);
15738
+ const { expectedStatus, ...patch } = opts;
15739
+ if (!matchesExpectedWorkflowStatus(current.status, expectedStatus)) return void 0;
15740
+ const header = {
15741
+ ...current,
15742
+ ...patch
15743
+ };
15744
+ await tx.execute({
15745
+ sql: `UPDATE ${TABLE_WORKFLOW_RUNS} SET header = jsonb(?), status = ?, updatedAt = ? WHERE workflow_name = ? AND run_id = ?`,
15746
+ args: [
15747
+ safeStringify(header),
15748
+ header.status,
15749
+ (/* @__PURE__ */ new Date()).toISOString(),
15750
+ workflowName,
15751
+ runId
15752
+ ]
15753
+ });
15754
+ return {
15755
+ header,
15756
+ lastSeq: Number(row.lastSeq),
15757
+ startStateSeq: Number(row.startStateSeq),
15758
+ runToken: row.runToken
15759
+ };
15760
+ }), "updateWorkflowRunHeader");
15761
+ } catch (error) {
15762
+ if (isRunFenceConflictError(error)) throw error;
15763
+ throw this.#runError("UPDATE_WORKFLOW_RUN_HEADER", workflowName, runId, error);
15764
+ }
15765
+ }
15766
+ /**
15767
+ * One batch behind the write lock (see `#writeRun` for `expectedStatus` and `stateBase`): marking
15768
+ * the run a state base builds on as shared, the header upsert, then the deletion of the run's
15769
+ * entries (only its steps when another run builds on its state, see `stateShared`), then the new ones.
15770
+ */
15771
+ async writeWorkflowRunCheckpoint({ fence, expectedStatus, steps, state, stateBase, statePatch, ...run }) {
15772
+ const entries = journalEntries(steps, {
15773
+ state,
15774
+ stateBase,
15775
+ statePatch
15776
+ }, true);
15777
+ const check = this.#runFenceCheck(fence, run.runId, "writeWorkflowRunCheckpoint");
15778
+ try {
15779
+ const base = state === void 0 ? stateBase : void 0;
15780
+ const statements = [];
15781
+ if (base) statements.push({
15782
+ sql: `UPDATE ${TABLE_WORKFLOW_RUNS} SET stateShared = 1 WHERE workflow_name = ? AND run_id = ? AND NOT stateShared`,
15783
+ args: [base.workflowName, base.runId]
15784
+ });
15785
+ statements.push(upsertHeader(run, entries.length, { checkpoint: true }), {
15786
+ sql: `DELETE FROM ${TABLE_WORKFLOW_RUN_ENTRIES} WHERE workflow_name = ? AND run_id = ?
15787
+ AND (kind IN ('step', 'step-patch') OR NOT (SELECT stateShared FROM ${TABLE_WORKFLOW_RUNS} WHERE workflow_name = ? AND run_id = ?))`,
15788
+ args: [
15789
+ run.workflowName,
15790
+ run.runId,
15791
+ run.workflowName,
15792
+ run.runId
15793
+ ]
15794
+ }, insertEntries(run, entries));
15795
+ const results = await this.executeWithRetry(() => this.#writeRun(check, run, {
15796
+ expectedStatus,
15797
+ stateBase: base
15798
+ }, statements), "writeWorkflowRunCheckpoint");
15799
+ if (!results) return null;
15800
+ const header = results[statements.length - 3].rows[0];
15801
+ return {
15802
+ lastSeq: Number(header.lastSeq),
15803
+ runToken: header.runToken
15804
+ };
15805
+ } catch (error) {
15806
+ if (isRunFenceConflictError(error)) throw error;
15807
+ throw this.#runError("WRITE_WORKFLOW_RUN_CHECKPOINT", run.workflowName, run.runId, error);
15808
+ }
15809
+ }
15810
+ /**
15811
+ * One batch behind the write lock (see `#writeRun` for `expectedStatus`): the header upsert adds
15812
+ * the entry count to `lastSeq`, and the insert numbers the entries up to it, so writes to one run
15813
+ * never interleave.
15814
+ */
15815
+ async appendWorkflowRunEntries({ fence, steps, stepPatches, state, statePatch, startStateSeq, expectedStatus, ...run }) {
15816
+ const entries = journalEntries(steps, {
15817
+ state,
15818
+ statePatch
15819
+ }, false, stepPatches);
15820
+ const check = this.#runFenceCheck(fence, run.runId, "appendWorkflowRunEntries");
15821
+ try {
15822
+ const results = await this.executeWithRetry(() => this.#writeRun(check, run, { expectedStatus }, [upsertHeader(run, entries.length, {
15823
+ checkpoint: false,
15824
+ startStateSeq
15825
+ }), insertEntries(run, entries)]), "appendWorkflowRunEntries");
15826
+ if (!results) return null;
15827
+ const header = results[0].rows[0];
15828
+ return {
15829
+ lastSeq: Number(header.lastSeq),
15830
+ runToken: header.runToken
15831
+ };
15832
+ } catch (error) {
15833
+ if (isRunFenceConflictError(error)) throw error;
15834
+ throw this.#runError("APPEND_WORKFLOW_RUN_ENTRIES", run.workflowName, run.runId, error);
15835
+ }
15836
+ }
15837
+ async loadWorkflowRunState({ workflowName, runId, seq }) {
15838
+ try {
15839
+ return (await this.#stateAt(this.#client, workflowName, runId, { seq }))?.state ?? null;
15840
+ } catch (error) {
15841
+ throw this.#runError("LOAD_WORKFLOW_RUN_STATE", workflowName, runId, error);
15842
+ }
15843
+ }
15844
+ /**
15845
+ * One batch behind the write lock (or the run's fence): the header's `lastSeq` bump, then the
15846
+ * draft numbered at it.
15847
+ */
15848
+ async appendWorkflowRunStateDraft(args) {
15849
+ const { workflowName, runId } = args;
15850
+ const payload = "state" in args ? { state: args.state } : {
15851
+ on: args.on.draft ? {
15852
+ seq: args.on.seq,
15853
+ draft: true
15854
+ } : { seq: args.on.seq },
15855
+ ops: args.ops
15856
+ };
15857
+ let row;
15858
+ try {
15859
+ const [header] = await this.executeWithRetry(() => withRunFence(this.#client, this.#runFenceCheck(void 0, runId, "appendWorkflowRunStateDraft"), (writer) => writer.batch([{
15860
+ sql: `UPDATE ${TABLE_WORKFLOW_RUNS} SET lastSeq = lastSeq + 1, stateShared = 1
15861
+ WHERE workflow_name = ? AND run_id = ? RETURNING lastSeq`,
15862
+ args: [workflowName, runId]
15863
+ }, {
15864
+ sql: `INSERT INTO ${TABLE_WORKFLOW_RUN_ENTRIES} (workflow_name, run_id, seq, kind, step_id, payload)
15865
+ SELECT workflow_name, run_id, lastSeq, 'state-draft', NULL, jsonb(?)
15866
+ FROM ${TABLE_WORKFLOW_RUNS} WHERE workflow_name = ? AND run_id = ?`,
15867
+ args: [
15868
+ safeStringify(payload),
15869
+ workflowName,
15870
+ runId
15871
+ ]
15872
+ }])), "appendWorkflowRunStateDraft");
15873
+ row = header.rows[0];
15874
+ } catch (error) {
15875
+ if (isRunFenceConflictError(error)) throw error;
15876
+ throw this.#runError("APPEND_WORKFLOW_RUN_STATE_DRAFT", workflowName, runId, error);
15877
+ }
15878
+ if (!row) throw new Error(`Storage v2 run ${workflowName}/${runId} is not stored`);
15879
+ return { seq: Number(row.lastSeq) };
15880
+ }
15244
15881
  async #readRunOwner(q, runId) {
15245
15882
  const row = (await q.execute({
15246
15883
  sql: `SELECT ${RUN_OWNER_COLUMNS} FROM ${TABLE_WORKFLOW_RUN_OWNERS} WHERE runId = ?`,
@@ -15341,21 +15978,56 @@ var WorkflowsLibSQL = class WorkflowsLibSQL extends WorkflowsStorage {
15341
15978
  throw this.#ownershipError("GET_RUN_OWNERSHIP", runId, error);
15342
15979
  }
15343
15980
  }
15344
- /** Delete workflow run snapshots and run ownership rows older than their policy's `maxAge`, batched. */
15981
+ /**
15982
+ * Delete workflow runs and run ownership rows older than their policy's `maxAge`, batched.
15983
+ * The `workflowSnapshot` policy covers both storage versions: storage v2 runs are pruned by
15984
+ * their header's last write, together with their entries.
15985
+ */
15345
15986
  async prune(policies, options) {
15346
15987
  const targets = resolveTargets({
15347
15988
  policies,
15348
15989
  descriptor: WorkflowsLibSQL.retentionTables,
15349
15990
  order: ["workflowSnapshot", "runOwnership"]
15350
15991
  });
15992
+ const snapshotTarget = targets.findIndex((target) => target.table === TABLE_WORKFLOW_SNAPSHOT);
15993
+ if (snapshotTarget !== -1) targets.splice(snapshotTarget + 1, 0, {
15994
+ ...targets[snapshotTarget],
15995
+ table: TABLE_WORKFLOW_RUNS
15996
+ });
15351
15997
  return runPrune({
15352
15998
  db: this.#db,
15353
15999
  domain: "workflows",
15354
16000
  targets,
15355
16001
  options,
15356
- logger: this.logger
16002
+ logger: this.logger,
16003
+ deleteBatch: (target, cutoff, limit) => target.table === TABLE_WORKFLOW_RUNS ? this.#pruneRunsBatch(cutoff, limit) : void 0
15357
16004
  });
15358
16005
  }
16006
+ /**
16007
+ * Deletes up to `limit` aged storage v2 runs in one write transaction: detaches the runs built on
16008
+ * their state, then deletes their entries and headers. Every statement selects the same runs, and
16009
+ * the transaction holds the write lock, so they agree.
16010
+ */
16011
+ async #pruneRunsBatch(cutoff, limit) {
16012
+ const doomed = `SELECT workflow_name, run_id FROM ${TABLE_WORKFLOW_RUNS}
16013
+ WHERE updatedAt < ? ORDER BY updatedAt, workflow_name, run_id LIMIT ?`;
16014
+ return this.executeWithRetry(() => inWriteTransaction(this.#client, async (tx) => {
16015
+ const shared = await tx.execute({
16016
+ sql: `SELECT d.workflow_name, d.run_id FROM (${doomed}) d
16017
+ JOIN ${TABLE_WORKFLOW_RUNS} r USING (workflow_name, run_id) WHERE r.stateShared`,
16018
+ args: [cutoff, limit]
16019
+ });
16020
+ for (const run of shared.rows) await this.#detachStateChildren(tx, run.workflow_name, run.run_id);
16021
+ await tx.execute({
16022
+ sql: `DELETE FROM ${TABLE_WORKFLOW_RUN_ENTRIES} WHERE (workflow_name, run_id) IN (${doomed})`,
16023
+ args: [cutoff, limit]
16024
+ });
16025
+ return (await tx.execute({
16026
+ sql: `DELETE FROM ${TABLE_WORKFLOW_RUNS} WHERE (workflow_name, run_id) IN (${doomed})`,
16027
+ args: [cutoff, limit]
16028
+ })).rowsAffected;
16029
+ }), `prune ${TABLE_WORKFLOW_RUNS}`);
16030
+ }
15359
16031
  async setupPragmaSettings() {
15360
16032
  try {
15361
16033
  await this.#client.execute("PRAGMA busy_timeout = 10000;");
@@ -15387,6 +16059,10 @@ var WorkflowsLibSQL = class WorkflowsLibSQL extends WorkflowsStorage {
15387
16059
  args: [workflowName, runId]
15388
16060
  });
15389
16061
  if (!existingSnapshotResult.rows?.[0]) {
16062
+ if ((await tx.execute({
16063
+ sql: `SELECT 1 FROM ${TABLE_WORKFLOW_RUNS} WHERE workflow_name = ? AND run_id = ?`,
16064
+ args: [workflowName, runId]
16065
+ })).rows.length > 0) throw new Error(`updateWorkflowResults does not support storage v2 runs (run ${runId})`);
15390
16066
  await tx.rollback();
15391
16067
  return {};
15392
16068
  }
@@ -15421,12 +16097,51 @@ var WorkflowsLibSQL = class WorkflowsLibSQL extends WorkflowsStorage {
15421
16097
  }
15422
16098
  }), "updateWorkflowResults");
15423
16099
  }
15424
- async updateWorkflowState({ workflowName, runId, opts, fence: explicitFence }) {
16100
+ async updateWorkflowState({ workflowName, runId, opts, fence: explicitFence, storageVersion }) {
15425
16101
  const check = this.#runFenceCheck(explicitFence, runId, "updateWorkflowState");
15426
16102
  return this.executeWithRetry(() => withClientWriteLock(this.#client, async () => {
15427
16103
  const tx = await this.#client.transaction("write");
15428
16104
  try {
15429
16105
  if (check) await assertRunFence(tx, check);
16106
+ if (storageVersion !== 1) {
16107
+ const row = (await tx.execute({
16108
+ sql: `SELECT json(header) AS header FROM ${TABLE_WORKFLOW_RUNS} WHERE workflow_name = ? AND run_id = ?`,
16109
+ args: [workflowName, runId]
16110
+ })).rows[0];
16111
+ if (row) {
16112
+ const current = JSON.parse(row.header);
16113
+ const { expectedStatus, ...patch } = opts;
16114
+ if (!matchesExpectedWorkflowStatus(current.status, expectedStatus)) {
16115
+ await tx.rollback();
16116
+ return;
16117
+ }
16118
+ const header = {
16119
+ ...current,
16120
+ ...patch
16121
+ };
16122
+ await tx.execute({
16123
+ sql: `UPDATE ${TABLE_WORKFLOW_RUNS} SET header = jsonb(?), status = ?, updatedAt = ? WHERE workflow_name = ? AND run_id = ?`,
16124
+ args: [
16125
+ safeStringify(header),
16126
+ header.status,
16127
+ (/* @__PURE__ */ new Date()).toISOString(),
16128
+ workflowName,
16129
+ runId
16130
+ ]
16131
+ });
16132
+ const entries = await tx.execute({
16133
+ sql: `SELECT ${entriesSql("r")} AS entries FROM ${TABLE_WORKFLOW_RUNS} r WHERE r.workflow_name = ? AND r.run_id = ?`,
16134
+ args: [workflowName, runId]
16135
+ });
16136
+ const snapshot = await this.#materializeRun(tx, header, entries.rows[0].entries);
16137
+ await tx.commit();
16138
+ return snapshot;
16139
+ }
16140
+ if (storageVersion === 2) {
16141
+ await tx.rollback();
16142
+ return;
16143
+ }
16144
+ }
15430
16145
  const existingSnapshotResult = await tx.execute({
15431
16146
  sql: `SELECT json(snapshot) as snapshot FROM ${TABLE_WORKFLOW_SNAPSHOT} WHERE workflow_name = ? AND run_id = ?`,
15432
16147
  args: [workflowName, runId]
@@ -15496,15 +16211,32 @@ var WorkflowsLibSQL = class WorkflowsLibSQL extends WorkflowsStorage {
15496
16211
  }
15497
16212
  async loadWorkflowRunStatus({ workflowName, runId }) {
15498
16213
  return (await this.#client.execute({
15499
- sql: `SELECT json_extract(snapshot, '$.status') AS status FROM ${TABLE_WORKFLOW_SNAPSHOT} WHERE workflow_name = ? AND run_id = ?`,
15500
- args: [workflowName, runId]
16214
+ sql: `SELECT COALESCE(
16215
+ (SELECT status FROM ${TABLE_WORKFLOW_RUNS} WHERE workflow_name = ? AND run_id = ?),
16216
+ (SELECT json_extract(snapshot, '$.status') FROM ${TABLE_WORKFLOW_SNAPSHOT} WHERE workflow_name = ? AND run_id = ?)
16217
+ ) AS status`,
16218
+ args: [
16219
+ workflowName,
16220
+ runId,
16221
+ workflowName,
16222
+ runId
16223
+ ]
15501
16224
  })).rows?.[0]?.status ?? null;
15502
16225
  }
15503
- async loadWorkflowSnapshot({ workflowName, runId }) {
16226
+ async loadWorkflowSnapshot({ workflowName, runId, storageVersion }) {
15504
16227
  this.logger.debug("Loading workflow snapshot", {
15505
16228
  workflowName,
15506
16229
  runId
15507
16230
  });
16231
+ if (storageVersion !== 1) {
16232
+ const row = (await this.#client.execute({
16233
+ sql: `SELECT json(r.header) AS header, ${entriesSql("r")} AS entries
16234
+ FROM ${TABLE_WORKFLOW_RUNS} r WHERE r.workflow_name = ? AND r.run_id = ?`,
16235
+ args: [workflowName, runId]
16236
+ })).rows[0];
16237
+ if (row) return this.#materializeRun(this.#client, row.header, row.entries);
16238
+ if (storageVersion === 2) return null;
16239
+ }
15508
16240
  const d = await this.#db.select({
15509
16241
  tableName: TABLE_WORKFLOW_SNAPSHOT,
15510
16242
  keys: {
@@ -15514,7 +16246,7 @@ var WorkflowsLibSQL = class WorkflowsLibSQL extends WorkflowsStorage {
15514
16246
  });
15515
16247
  return d ? d.snapshot : null;
15516
16248
  }
15517
- async getWorkflowRunById({ runId, workflowName }) {
16249
+ async getWorkflowRunById({ runId, workflowName, storageVersion }) {
15518
16250
  const conditions = [];
15519
16251
  const args = [];
15520
16252
  if (runId) {
@@ -15528,11 +16260,15 @@ var WorkflowsLibSQL = class WorkflowsLibSQL extends WorkflowsStorage {
15528
16260
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
15529
16261
  try {
15530
16262
  const result = await this.#client.execute({
15531
- sql: `SELECT workflow_name, run_id, resourceId, json(snapshot) as snapshot, createdAt, updatedAt FROM ${TABLE_WORKFLOW_SNAPSHOT} ${whereClause} ORDER BY createdAt DESC LIMIT 1`,
15532
- args
16263
+ sql: this.#runsSql({
16264
+ v1Where: storageVersion === 2 ? null : whereClause,
16265
+ v2Where: storageVersion === 1 ? null : whereClause,
16266
+ page: "LIMIT 1"
16267
+ }),
16268
+ args: [...storageVersion === 2 ? [] : args, ...storageVersion === 1 ? [] : args]
15533
16269
  });
15534
16270
  if (!result.rows?.[0]) return null;
15535
- return this.parseWorkflowRun(result.rows[0]);
16271
+ return await this.#parseRun(result.rows[0]);
15536
16272
  } catch (error) {
15537
16273
  throw new MastraError({
15538
16274
  id: createStorageErrorId("LIBSQL", "GET_WORKFLOW_RUN_BY_ID", "FAILED"),
@@ -15541,14 +16277,33 @@ var WorkflowsLibSQL = class WorkflowsLibSQL extends WorkflowsStorage {
15541
16277
  }, error);
15542
16278
  }
15543
16279
  }
15544
- async deleteWorkflowRunById({ runId, workflowName, fence }) {
16280
+ async deleteWorkflowRunById({ runId, workflowName, storageVersion, fence }) {
15545
16281
  const check = this.#runFenceCheck(fence, runId, "deleteWorkflowRunById");
15546
16282
  return this.executeWithRetry(async () => {
15547
16283
  try {
15548
- await withRunFence(this.#client, check, (writer) => writer.execute({
15549
- sql: `DELETE FROM ${TABLE_WORKFLOW_SNAPSHOT} WHERE workflow_name = ? AND run_id = ?`,
15550
- args: [workflowName, runId]
15551
- }));
16284
+ if (storageVersion === 1) {
16285
+ await withRunFence(this.#client, check, (writer) => writer.execute({
16286
+ sql: `DELETE FROM ${TABLE_WORKFLOW_SNAPSHOT} WHERE workflow_name = ? AND run_id = ?`,
16287
+ args: [workflowName, runId]
16288
+ }));
16289
+ return;
16290
+ }
16291
+ await inWriteTransaction(this.#client, async (tx) => {
16292
+ if (check) await assertRunFence(tx, check);
16293
+ const run = await tx.execute({
16294
+ sql: `SELECT stateShared FROM ${TABLE_WORKFLOW_RUNS} WHERE workflow_name = ? AND run_id = ?`,
16295
+ args: [workflowName, runId]
16296
+ });
16297
+ if (Number(run.rows[0]?.stateShared)) await this.#detachStateChildren(tx, workflowName, runId);
16298
+ for (const table of [
16299
+ TABLE_WORKFLOW_RUNS,
16300
+ TABLE_WORKFLOW_RUN_ENTRIES,
16301
+ ...storageVersion === 2 ? [] : [TABLE_WORKFLOW_SNAPSHOT]
16302
+ ]) await tx.execute({
16303
+ sql: `DELETE FROM ${table} WHERE workflow_name = ? AND run_id = ?`,
16304
+ args: [workflowName, runId]
16305
+ });
16306
+ });
15552
16307
  } catch (error) {
15553
16308
  if (isRunFenceConflictError(error)) throw error;
15554
16309
  throw new MastraError({
@@ -15567,52 +16322,74 @@ var WorkflowsLibSQL = class WorkflowsLibSQL extends WorkflowsStorage {
15567
16322
  try {
15568
16323
  const conditions = [];
15569
16324
  const args = [];
16325
+ const v2Conditions = [];
16326
+ const v2Args = [];
15570
16327
  if (workflowName) {
15571
16328
  conditions.push("workflow_name = ?");
15572
16329
  args.push(workflowName);
16330
+ v2Conditions.push("workflow_name = ?");
16331
+ v2Args.push(workflowName);
15573
16332
  }
15574
16333
  if (status) {
15575
16334
  conditions.push("json_extract(snapshot, '$.status') = ?");
15576
16335
  args.push(status);
16336
+ v2Conditions.push("status = ?");
16337
+ v2Args.push(status);
15577
16338
  }
15578
16339
  if (fromDate) {
15579
16340
  conditions.push("createdAt >= ?");
15580
16341
  args.push(fromDate.toISOString());
16342
+ v2Conditions.push("createdAt >= ?");
16343
+ v2Args.push(fromDate.toISOString());
15581
16344
  }
15582
16345
  if (toDate) {
15583
16346
  conditions.push("createdAt <= ?");
15584
16347
  args.push(toDate.toISOString());
16348
+ v2Conditions.push("createdAt <= ?");
16349
+ v2Args.push(toDate.toISOString());
15585
16350
  }
15586
16351
  if (resourceId) {
15587
16352
  if (await this.#db.hasColumn(TABLE_WORKFLOW_SNAPSHOT, "resourceId")) {
15588
16353
  conditions.push("resourceId = ?");
15589
16354
  args.push(resourceId);
15590
16355
  } else this.logger.warn(`[${TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
16356
+ v2Conditions.push("resourceId = ?");
16357
+ v2Args.push(resourceId);
15591
16358
  }
15592
16359
  if (threadId) {
15593
16360
  conditions.push(`(EXISTS (SELECT 1 FROM json_each(snapshot, '$.context') AS je WHERE json_extract(je.value, '$.status') = 'suspended' AND json_extract(je.value, '$.suspendPayload.__streamState.messageList.memoryInfo.threadId') = ?) OR json_extract(snapshot, '$.context.input.messageListState.memoryInfo.threadId') = ?)`);
15594
16361
  args.push(threadId, threadId);
16362
+ v2Conditions.push("threadId = ?");
16363
+ v2Args.push(threadId);
15595
16364
  }
15596
16365
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
16366
+ const v2WhereClause = v2Conditions.length > 0 ? `WHERE ${v2Conditions.join(" AND ")}` : "";
15597
16367
  let total = 0;
15598
16368
  const usePagination = typeof perPage === "number" && typeof page === "number";
15599
16369
  if (usePagination) {
15600
16370
  const countResult = await this.#client.execute({
15601
- sql: `SELECT COUNT(*) as count FROM ${TABLE_WORKFLOW_SNAPSHOT} ${whereClause}`,
15602
- args
16371
+ sql: `SELECT (SELECT COUNT(*) FROM ${TABLE_WORKFLOW_SNAPSHOT} ${whereClause})
16372
+ + (SELECT COUNT(*) FROM ${TABLE_WORKFLOW_RUNS} ${v2WhereClause}) AS count`,
16373
+ args: [...args, ...v2Args]
15603
16374
  });
15604
16375
  total = Number(countResult.rows?.[0]?.count ?? 0);
15605
16376
  }
15606
16377
  const normalizedPerPage = usePagination ? normalizePerPage(perPage, Number.MAX_SAFE_INTEGER) : 0;
15607
16378
  const offset = usePagination ? page * normalizedPerPage : 0;
15608
- const runs = ((await this.#client.execute({
15609
- sql: `SELECT workflow_name, run_id, resourceId, ${summary ? `json_object('status', json_extract(snapshot, '$.status'), 'timestamp', json_extract(snapshot, '$.timestamp'))` : "json(snapshot)"} as snapshot, createdAt, updatedAt FROM ${TABLE_WORKFLOW_SNAPSHOT} ${whereClause} ORDER BY createdAt DESC${usePagination ? ` LIMIT ? OFFSET ?` : ""}`,
15610
- args: usePagination ? [
16379
+ const result = await this.#client.execute({
16380
+ sql: this.#runsSql({
16381
+ v1Where: whereClause,
16382
+ v2Where: v2WhereClause,
16383
+ summary,
16384
+ page: usePagination ? "LIMIT ? OFFSET ?" : ""
16385
+ }),
16386
+ args: [
15611
16387
  ...args,
15612
- normalizedPerPage,
15613
- offset
15614
- ] : args
15615
- })).rows || []).map((row) => this.parseWorkflowRun(row));
16388
+ ...v2Args,
16389
+ ...usePagination ? [normalizedPerPage, offset] : []
16390
+ ]
16391
+ });
16392
+ const runs = await Promise.all((result.rows || []).map((row) => this.#parseRun(row)));
15616
16393
  return {
15617
16394
  runs,
15618
16395
  total: total || runs.length