@mastra/dsql 1.5.1-alpha.1 → 1.5.2-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1825,6 +1825,49 @@ var AgentsDSQL = class AgentsDSQL extends _mastra_core_storage.AgentsStorage {
1825
1825
  }
1826
1826
  };
1827
1827
  //#endregion
1828
+ //#region src/storage/db/run-fencing.ts
1829
+ /**
1830
+ * Epoch milliseconds on the database clock. Aurora DSQL never waits on a row
1831
+ * lock, so the transaction's start time is the time its statements run, and
1832
+ * every retry starts a new transaction with a fresh clock.
1833
+ */
1834
+ const DB_NOW_MS = `(EXTRACT(EPOCH FROM now()) * 1000)::bigint`;
1835
+ /**
1836
+ * Retry predicate for writes to an ownership or fence row that may not exist
1837
+ * yet. Two first writers both insert the same key; the loser normally fails
1838
+ * at commit with an OCC conflict (40001), but a duplicate-key error (23505)
1839
+ * means the same thing here: the retry sees the row and takes the update path.
1840
+ */
1841
+ function isRetriableRunFenceWrite(error) {
1842
+ return isRetriableError(error) || getErrorCode(error) === "23505";
1843
+ }
1844
+ /**
1845
+ * The check half of a fenced write; call it first inside the write's
1846
+ * transaction. Aurora DSQL has no blocking row locks: `FOR UPDATE` puts the
1847
+ * claim row in this transaction's write set instead, so a claim that commits
1848
+ * against the row while the write is in flight makes the write fail at commit
1849
+ * with an OCC conflict. The retry then sees the new claim and is rejected.
1850
+ */
1851
+ async function assertRunFence(t, claimsTable, fence, operation) {
1852
+ const row = await t.oneOrNone(`SELECT generation, "ownerId" FROM ${claimsTable} WHERE "runId" = $1 FOR UPDATE`, [fence.runId]);
1853
+ if (!(0, _mastra_core_storage.matchesRunFence)(row && {
1854
+ generation: Number(row.generation),
1855
+ ownerId: row.ownerId
1856
+ }, fence)) throw new _mastra_core_storage.RunFenceConflictError(fence, operation);
1857
+ }
1858
+ /**
1859
+ * Runs `write` directly when it carries no fence, or in a transaction behind
1860
+ * the fence check when it does. Callers own retries: wrap the call in
1861
+ * `withRetry` so an OCC conflict re-runs the check as well as the write.
1862
+ */
1863
+ function withRunFence(client, claimsTable, fence, operation, write) {
1864
+ if (!fence) return write(client);
1865
+ return client.tx(async (t) => {
1866
+ await assertRunFence(t, claimsTable, fence, operation);
1867
+ return write(t);
1868
+ });
1869
+ }
1870
+ //#endregion
1828
1871
  //#region src/storage/domains/memory/index.ts
1829
1872
  /**
1830
1873
  * Generate SQL placeholder string for IN clauses.
@@ -1884,6 +1927,10 @@ var MemoryDSQL = class MemoryDSQL extends _mastra_core_storage.MemoryStorage {
1884
1927
  schema: _mastra_core_storage.TABLE_SCHEMAS[_mastra_core_storage.TABLE_MESSAGES],
1885
1928
  ifNotExists: ["resourceId"]
1886
1929
  });
1930
+ await this.#db.createTable({
1931
+ tableName: _mastra_core_storage.TABLE_MEMORY_RUN_FENCES,
1932
+ schema: _mastra_core_storage.RUN_FENCING_TABLE_SCHEMAS[_mastra_core_storage.TABLE_MEMORY_RUN_FENCES]
1933
+ });
1887
1934
  await this.createDefaultIndexes();
1888
1935
  await this.createCustomIndexes();
1889
1936
  }
@@ -1929,6 +1976,62 @@ var MemoryDSQL = class MemoryDSQL extends _mastra_core_storage.MemoryStorage {
1929
1976
  await this.#db.clearTable({ tableName: _mastra_core_storage.TABLE_MESSAGES });
1930
1977
  await this.#db.clearTable({ tableName: _mastra_core_storage.TABLE_THREADS });
1931
1978
  await this.#db.clearTable({ tableName: _mastra_core_storage.TABLE_RESOURCES });
1979
+ await this.#db.clearTable({ tableName: _mastra_core_storage.TABLE_MEMORY_RUN_FENCES });
1980
+ }
1981
+ supportsRunFencing() {
1982
+ return true;
1983
+ }
1984
+ #runFencesTable() {
1985
+ return getTableName({
1986
+ indexName: _mastra_core_storage.TABLE_MEMORY_RUN_FENCES,
1987
+ schemaName: getSchemaName(this.#schema)
1988
+ });
1989
+ }
1990
+ /** Runs `write` behind the run's fence check when it carries a fence. */
1991
+ #fenced(fence, operation, write) {
1992
+ return withRunFence(this.#db.client, this.#runFencesTable(), (0, _mastra_core_storage.resolveRunFence)(this, fence), operation, write);
1993
+ }
1994
+ async raiseRunFence(fence) {
1995
+ try {
1996
+ const { result } = await withRetry(() => this.#db.client.oneOrNone(`INSERT INTO ${this.#runFencesTable()} AS f ("runId", generation, "ownerId")
1997
+ VALUES ($1, $2, $3)
1998
+ ON CONFLICT ("runId") DO UPDATE
1999
+ SET generation = EXCLUDED.generation, "ownerId" = EXCLUDED."ownerId", "retiredAt" = NULL
2000
+ WHERE f.generation < EXCLUDED.generation
2001
+ OR (f.generation = EXCLUDED.generation AND f."ownerId" = EXCLUDED."ownerId")
2002
+ RETURNING "runId"`, [
2003
+ fence.runId,
2004
+ fence.generation,
2005
+ fence.ownerId
2006
+ ]), { isRetriable: isRetriableRunFenceWrite });
2007
+ return result !== null;
2008
+ } catch (error) {
2009
+ throw new _mastra_core_error.MastraError({
2010
+ id: (0, _mastra_core_storage.createStorageErrorId)("DSQL", "RAISE_RUN_FENCE", "FAILED"),
2011
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
2012
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
2013
+ details: { runId: fence.runId }
2014
+ }, error);
2015
+ }
2016
+ }
2017
+ async retireRunFence(fence) {
2018
+ try {
2019
+ const { result } = await withRetry(() => this.#db.client.oneOrNone(`UPDATE ${this.#runFencesTable()} SET "retiredAt" = ${DB_NOW_MS}
2020
+ WHERE "runId" = $1 AND generation = $2 AND "ownerId" = $3
2021
+ RETURNING "runId"`, [
2022
+ fence.runId,
2023
+ fence.generation,
2024
+ fence.ownerId
2025
+ ]), { isRetriable: isRetriableRunFenceWrite });
2026
+ return result !== null;
2027
+ } catch (error) {
2028
+ throw new _mastra_core_error.MastraError({
2029
+ id: (0, _mastra_core_storage.createStorageErrorId)("DSQL", "RETIRE_RUN_FENCE", "FAILED"),
2030
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
2031
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
2032
+ details: { runId: fence.runId }
2033
+ }, error);
2034
+ }
1932
2035
  }
1933
2036
  /**
1934
2037
  * Normalizes message row from database by applying createdAtZ fallback
@@ -2065,7 +2168,7 @@ var MemoryDSQL = class MemoryDSQL extends _mastra_core_storage.MemoryStorage {
2065
2168
  throw mastraError;
2066
2169
  }
2067
2170
  }
2068
- async saveThread({ thread }) {
2171
+ async saveThread({ thread, fence }) {
2069
2172
  const tableName = getTableName({
2070
2173
  indexName: _mastra_core_storage.TABLE_THREADS,
2071
2174
  schemaName: getSchemaName(this.#schema)
@@ -2073,7 +2176,7 @@ var MemoryDSQL = class MemoryDSQL extends _mastra_core_storage.MemoryStorage {
2073
2176
  const createdAt = toUtcISOString(thread.createdAt);
2074
2177
  const updatedAt = toUtcISOString(thread.updatedAt);
2075
2178
  await withRetry(async () => {
2076
- await this.#db.client.none(`INSERT INTO ${tableName} (
2179
+ await this.#fenced(fence, "saveThread", (q) => q.none(`INSERT INTO ${tableName} (
2077
2180
  id,
2078
2181
  "resourceId",
2079
2182
  title,
@@ -2099,10 +2202,11 @@ var MemoryDSQL = class MemoryDSQL extends _mastra_core_storage.MemoryStorage {
2099
2202
  createdAt,
2100
2203
  updatedAt,
2101
2204
  updatedAt
2102
- ]);
2205
+ ]));
2103
2206
  }, { onRetry: (error, attempt, delay) => {
2104
2207
  this.logger?.warn?.(`saveThread retry ${attempt} for ${thread.id} after ${delay}ms: ${error.message}`);
2105
2208
  } }).catch((error) => {
2209
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
2106
2210
  throw new _mastra_core_error.MastraError({
2107
2211
  id: (0, _mastra_core_storage.createStorageErrorId)("DSQL", "SAVE_THREAD", "FAILED"),
2108
2212
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -2112,13 +2216,13 @@ var MemoryDSQL = class MemoryDSQL extends _mastra_core_storage.MemoryStorage {
2112
2216
  });
2113
2217
  return thread;
2114
2218
  }
2115
- async updateThread({ id, title, metadata }) {
2219
+ async updateThread({ id, title, metadata, fence }) {
2116
2220
  const threadTableName = getTableName({
2117
2221
  indexName: _mastra_core_storage.TABLE_THREADS,
2118
2222
  schemaName: getSchemaName(this.#schema)
2119
2223
  });
2120
- const { result } = await withRetry(async () => {
2121
- const existingThread = await this.getThreadById({ threadId: id });
2224
+ const { result } = await withRetry(() => this.#fenced(fence, "updateThread", async (q) => {
2225
+ const existingThread = await q.oneOrNone(`SELECT metadata FROM ${threadTableName} WHERE id = $1`, [id]);
2122
2226
  if (!existingThread) throw new _mastra_core_error.MastraError({
2123
2227
  id: (0, _mastra_core_storage.createStorageErrorId)("DSQL", "UPDATE_THREAD", "NOT_FOUND"),
2124
2228
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -2130,11 +2234,11 @@ var MemoryDSQL = class MemoryDSQL extends _mastra_core_storage.MemoryStorage {
2130
2234
  }
2131
2235
  });
2132
2236
  const mergedMetadata = {
2133
- ...existingThread.metadata,
2237
+ ...typeof existingThread.metadata === "string" ? JSON.parse(existingThread.metadata) : existingThread.metadata,
2134
2238
  ...metadata
2135
2239
  };
2136
2240
  const now = (/* @__PURE__ */ new Date()).toISOString();
2137
- const thread = await this.#db.client.one(`UPDATE ${threadTableName}
2241
+ return q.one(`UPDATE ${threadTableName}
2138
2242
  SET
2139
2243
  title = COALESCE($1, title),
2140
2244
  metadata = $2,
@@ -2149,15 +2253,14 @@ var MemoryDSQL = class MemoryDSQL extends _mastra_core_storage.MemoryStorage {
2149
2253
  now,
2150
2254
  id
2151
2255
  ]);
2152
- return {
2153
- id: thread.id,
2154
- resourceId: thread.resourceId,
2155
- title: thread.title,
2156
- metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata,
2157
- createdAt: thread.createdAtZ || thread.createdAt,
2158
- updatedAt: thread.updatedAtZ || thread.updatedAt
2159
- };
2160
- }, { onRetry: (error, attempt, delay) => {
2256
+ }).then((thread) => ({
2257
+ id: thread.id,
2258
+ resourceId: thread.resourceId,
2259
+ title: thread.title,
2260
+ metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata,
2261
+ createdAt: thread.createdAtZ || thread.createdAt,
2262
+ updatedAt: thread.updatedAtZ || thread.updatedAt
2263
+ })), { onRetry: (error, attempt, delay) => {
2161
2264
  this.logger?.warn?.(`updateThread retry ${attempt} for ${id} after ${delay}ms: ${error.message}`);
2162
2265
  } }).catch((error) => {
2163
2266
  if (error instanceof _mastra_core_error.MastraError) throw error;
@@ -2482,7 +2585,7 @@ var MemoryDSQL = class MemoryDSQL extends _mastra_core_storage.MemoryStorage {
2482
2585
  throw mastraError;
2483
2586
  }
2484
2587
  }
2485
- async saveMessages({ messages }) {
2588
+ async saveMessages({ messages, fence: explicitFence }) {
2486
2589
  if (messages.length === 0) return { messages: [] };
2487
2590
  const threadId = messages[0]?.threadId;
2488
2591
  if (!threadId) throw new _mastra_core_error.MastraError({
@@ -2506,8 +2609,10 @@ var MemoryDSQL = class MemoryDSQL extends _mastra_core_storage.MemoryStorage {
2506
2609
  indexName: _mastra_core_storage.TABLE_THREADS,
2507
2610
  schemaName: getSchemaName(this.#schema)
2508
2611
  });
2612
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence);
2509
2613
  await withRetry(async () => {
2510
2614
  await this.#db.client.tx(async (t) => {
2615
+ if (fence) await assertRunFence(t, this.#runFencesTable(), fence, "saveMessages");
2511
2616
  const messageInserts = messages.map((message) => {
2512
2617
  if (!message.threadId) throw new Error(`Expected to find a threadId for message, but couldn't find one. An unexpected error has occurred.`);
2513
2618
  if (!message.resourceId) throw new Error(`Expected to find a resourceId for message, but couldn't find one. An unexpected error has occurred.`);
@@ -2546,6 +2651,7 @@ var MemoryDSQL = class MemoryDSQL extends _mastra_core_storage.MemoryStorage {
2546
2651
  }, { onRetry: (error, attempt, delay) => {
2547
2652
  this.logger?.warn?.(`saveMessages retry ${attempt} for thread ${threadId} after ${delay}ms: ${error.message}`);
2548
2653
  } }).catch((error) => {
2654
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
2549
2655
  throw new _mastra_core_error.MastraError({
2550
2656
  id: (0, _mastra_core_storage.createStorageErrorId)("DSQL", "SAVE_MESSAGES", "FAILED"),
2551
2657
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -2566,8 +2672,9 @@ var MemoryDSQL = class MemoryDSQL extends _mastra_core_storage.MemoryStorage {
2566
2672
  });
2567
2673
  return { messages: new _mastra_core_agent.MessageList().add(messagesWithParsedContent, "memory").get.all.db() };
2568
2674
  }
2569
- async updateMessages({ messages }) {
2675
+ async updateMessages({ messages, fence: explicitFence }) {
2570
2676
  if (messages.length === 0) return [];
2677
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence);
2571
2678
  const messageIds = messages.map((m) => m.id);
2572
2679
  const selectQuery = `SELECT id, content, role, type, "createdAt", "createdAtZ", thread_id AS "threadId", "resourceId" FROM ${getTableName({
2573
2680
  indexName: _mastra_core_storage.TABLE_MESSAGES,
@@ -2584,6 +2691,7 @@ var MemoryDSQL = class MemoryDSQL extends _mastra_core_storage.MemoryStorage {
2584
2691
  const threadIdsToUpdate = /* @__PURE__ */ new Set();
2585
2692
  await withRetry(async () => {
2586
2693
  await this.#db.client.tx(async (t) => {
2694
+ if (fence) await assertRunFence(t, this.#runFencesTable(), fence, "updateMessages");
2587
2695
  const queries = [];
2588
2696
  const columnMapping = { threadId: "thread_id" };
2589
2697
  for (const existingMessage of existingMessages) {
@@ -2636,6 +2744,7 @@ var MemoryDSQL = class MemoryDSQL extends _mastra_core_storage.MemoryStorage {
2636
2744
  }, { onRetry: (error, attempt, delay) => {
2637
2745
  this.logger?.warn?.(`updateMessages retry ${attempt} for ${messageIds.length} messages after ${delay}ms: ${error.message}`);
2638
2746
  } }).catch((error) => {
2747
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
2639
2748
  throw new _mastra_core_error.MastraError({
2640
2749
  id: (0, _mastra_core_storage.createStorageErrorId)("DSQL", "UPDATE_MESSAGES", "FAILED"),
2641
2750
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -2654,8 +2763,9 @@ var MemoryDSQL = class MemoryDSQL extends _mastra_core_storage.MemoryStorage {
2654
2763
  return message;
2655
2764
  });
2656
2765
  }
2657
- async deleteMessages(messageIds) {
2766
+ async deleteMessages(messageIds, options) {
2658
2767
  if (!messageIds || messageIds.length === 0) return;
2768
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, options?.fence);
2659
2769
  const messageTableName = getTableName({
2660
2770
  indexName: _mastra_core_storage.TABLE_MESSAGES,
2661
2771
  schemaName: getSchemaName(this.#schema)
@@ -2666,6 +2776,7 @@ var MemoryDSQL = class MemoryDSQL extends _mastra_core_storage.MemoryStorage {
2666
2776
  });
2667
2777
  await withRetry(async () => {
2668
2778
  await this.#db.client.tx(async (t) => {
2779
+ if (fence) await assertRunFence(t, this.#runFencesTable(), fence, "deleteMessages");
2669
2780
  const placeholders = messageIds.map((_, idx) => `$${idx + 1}`).join(",");
2670
2781
  const threadIds = (await t.manyOrNone(`SELECT DISTINCT thread_id FROM ${messageTableName} WHERE id IN (${placeholders})`, messageIds))?.map((msg) => msg.thread_id).filter(Boolean) || [];
2671
2782
  await t.none(`DELETE FROM ${messageTableName} WHERE id IN (${placeholders})`, messageIds);
@@ -2677,6 +2788,7 @@ var MemoryDSQL = class MemoryDSQL extends _mastra_core_storage.MemoryStorage {
2677
2788
  }, { onRetry: (error, attempt, delay) => {
2678
2789
  this.logger?.warn?.(`deleteMessages retry ${attempt} for ${messageIds.length} messages after ${delay}ms: ${error.message}`);
2679
2790
  } }).catch((error) => {
2791
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
2680
2792
  throw new _mastra_core_error.MastraError({
2681
2793
  id: (0, _mastra_core_storage.createStorageErrorId)("DSQL", "DELETE_MESSAGES", "FAILED"),
2682
2794
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -2721,23 +2833,42 @@ var MemoryDSQL = class MemoryDSQL extends _mastra_core_storage.MemoryStorage {
2721
2833
  });
2722
2834
  return resource;
2723
2835
  }
2724
- async updateResource({ resourceId, workingMemory, metadata }) {
2836
+ async updateResource({ resourceId, workingMemory, metadata, fence }) {
2725
2837
  const tableName = getTableName({
2726
2838
  indexName: _mastra_core_storage.TABLE_RESOURCES,
2727
2839
  schemaName: getSchemaName(this.#schema)
2728
2840
  });
2729
- const { result } = await withRetry(async () => {
2730
- const existingResource = await this.getResourceById({ resourceId });
2731
- if (!existingResource) {
2841
+ const { result } = await withRetry(() => this.#fenced(fence, "updateResource", async (q) => {
2842
+ const row = await q.oneOrNone(`SELECT * FROM ${tableName} WHERE id = $1`, [resourceId]);
2843
+ if (!row) {
2844
+ const now = /* @__PURE__ */ new Date();
2732
2845
  const newResource = {
2733
2846
  id: resourceId,
2734
2847
  workingMemory,
2735
2848
  metadata: metadata || {},
2736
- createdAt: /* @__PURE__ */ new Date(),
2737
- updatedAt: /* @__PURE__ */ new Date()
2849
+ createdAt: now,
2850
+ updatedAt: now
2738
2851
  };
2739
- return this.saveResource({ resource: newResource });
2852
+ const nowIso = now.toISOString();
2853
+ await q.none(`INSERT INTO ${tableName} (id, "workingMemory", metadata, "createdAt", "createdAtZ", "updatedAt", "updatedAtZ")
2854
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`, [
2855
+ resourceId,
2856
+ workingMemory ?? null,
2857
+ JSON.stringify(newResource.metadata),
2858
+ nowIso,
2859
+ nowIso,
2860
+ nowIso,
2861
+ nowIso
2862
+ ]);
2863
+ return newResource;
2740
2864
  }
2865
+ const existingResource = {
2866
+ id: row.id,
2867
+ createdAt: row.createdAtZ || row.createdAt,
2868
+ updatedAt: row.updatedAtZ || row.updatedAt,
2869
+ workingMemory: row.workingMemory,
2870
+ metadata: typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata
2871
+ };
2741
2872
  const updatedResource = {
2742
2873
  ...existingResource,
2743
2874
  workingMemory: workingMemory !== void 0 ? workingMemory : existingResource.workingMemory,
@@ -2767,9 +2898,9 @@ var MemoryDSQL = class MemoryDSQL extends _mastra_core_storage.MemoryStorage {
2767
2898
  values.push(updatedResource.updatedAt.toISOString());
2768
2899
  paramIndex++;
2769
2900
  values.push(resourceId);
2770
- await this.#db.client.none(`UPDATE ${tableName} SET ${updates.join(", ")} WHERE id = $${paramIndex}`, values);
2901
+ await q.none(`UPDATE ${tableName} SET ${updates.join(", ")} WHERE id = $${paramIndex}`, values);
2771
2902
  return updatedResource;
2772
- }, { onRetry: (error, attempt, delay) => {
2903
+ }), { onRetry: (error, attempt, delay) => {
2773
2904
  this.logger?.warn?.(`updateResource retry ${attempt} for ${resourceId} after ${delay}ms: ${error.message}`);
2774
2905
  } }).catch((error) => {
2775
2906
  if (error instanceof _mastra_core_error.MastraError) throw error;
@@ -3719,6 +3850,17 @@ var ScoresDSQL = class ScoresDSQL extends _mastra_core_storage.ScoresStorage {
3719
3850
  };
3720
3851
  //#endregion
3721
3852
  //#region src/storage/domains/workflows/index.ts
3853
+ const RUN_OWNER_COLUMNS = `generation, "ownerId", "leaseExpiresAt", ${DB_NOW_MS} AS "nowMs"`;
3854
+ function toRunOwnershipRecord(runId, row) {
3855
+ const leaseExpiresAt = row.leaseExpiresAt === null ? null : Number(row.leaseExpiresAt);
3856
+ return {
3857
+ runId,
3858
+ generation: Number(row.generation),
3859
+ ownerId: row.ownerId,
3860
+ leaseExpiresAt: leaseExpiresAt === null ? null : new Date(leaseExpiresAt),
3861
+ live: leaseExpiresAt !== null && leaseExpiresAt > Number(row.nowMs)
3862
+ };
3863
+ }
3722
3864
  function parseWorkflowRun(row) {
3723
3865
  let parsedSnapshot = row.snapshot;
3724
3866
  if (typeof parsedSnapshot === "string") try {
@@ -3780,6 +3922,10 @@ var WorkflowsDSQL = class WorkflowsDSQL extends _mastra_core_storage.WorkflowsSt
3780
3922
  schema: _mastra_core_storage.TABLE_SCHEMAS[_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT],
3781
3923
  ifNotExists: ["resourceId"]
3782
3924
  });
3925
+ await this.#db.createTable({
3926
+ tableName: _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS,
3927
+ schema: _mastra_core_storage.RUN_FENCING_TABLE_SCHEMAS[_mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS]
3928
+ });
3783
3929
  await this.createDefaultIndexes();
3784
3930
  await this.createCustomIndexes();
3785
3931
  }
@@ -3796,11 +3942,130 @@ var WorkflowsDSQL = class WorkflowsDSQL extends _mastra_core_storage.WorkflowsSt
3796
3942
  }
3797
3943
  async dangerouslyClearAll() {
3798
3944
  await this.#db.clearTable({ tableName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT });
3945
+ await this.#db.clearTable({ tableName: _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS });
3799
3946
  }
3800
- async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state }) {
3947
+ supportsRunFencing() {
3948
+ return true;
3949
+ }
3950
+ #runOwnersTable() {
3951
+ return getTableName({
3952
+ indexName: _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS,
3953
+ schemaName: getSchemaName(this.#schema)
3954
+ });
3955
+ }
3956
+ async #readRunOwner(q, runId) {
3957
+ const row = await q.oneOrNone(`SELECT ${RUN_OWNER_COLUMNS} FROM ${this.#runOwnersTable()} WHERE "runId" = $1`, [runId]);
3958
+ return row ? toRunOwnershipRecord(runId, row) : null;
3959
+ }
3960
+ #ownershipError(operation, runId, error) {
3961
+ return new _mastra_core_error.MastraError({
3962
+ id: (0, _mastra_core_storage.createStorageErrorId)("DSQL", operation, "FAILED"),
3963
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
3964
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
3965
+ details: { runId }
3966
+ }, error);
3967
+ }
3968
+ async claimRunOwnership({ runId, ownerId, leaseMs, force, expectedGeneration }) {
3969
+ const table = this.#runOwnersTable();
3970
+ try {
3971
+ const { result } = await withRetry(() => this.#db.client.tx(async (t) => {
3972
+ if (!await t.oneOrNone(`SELECT 1 FROM ${table} WHERE "runId" = $1 FOR UPDATE`, [runId])) {
3973
+ if (expectedGeneration !== void 0 && expectedGeneration !== 0) return {
3974
+ acquired: false,
3975
+ record: null
3976
+ };
3977
+ return {
3978
+ acquired: true,
3979
+ record: toRunOwnershipRecord(runId, await t.one(`INSERT INTO ${table} ("runId", generation, "ownerId", "leaseExpiresAt", "updatedAt")
3980
+ VALUES ($1, ${DB_NOW_MS}, $2, ${DB_NOW_MS} + $3, ${DB_NOW_MS})
3981
+ RETURNING ${RUN_OWNER_COLUMNS}`, [
3982
+ runId,
3983
+ ownerId,
3984
+ leaseMs
3985
+ ]))
3986
+ };
3987
+ }
3988
+ const claimed = await t.oneOrNone(`UPDATE ${table}
3989
+ SET generation = generation + 1, "ownerId" = $2, "leaseExpiresAt" = ${DB_NOW_MS} + $3,
3990
+ "updatedAt" = ${DB_NOW_MS}
3991
+ WHERE "runId" = $1
3992
+ AND ($4::bigint IS NULL OR generation = $4)
3993
+ AND ($5::boolean OR "leaseExpiresAt" IS NULL OR "leaseExpiresAt" <= ${DB_NOW_MS})
3994
+ RETURNING ${RUN_OWNER_COLUMNS}`, [
3995
+ runId,
3996
+ ownerId,
3997
+ leaseMs,
3998
+ expectedGeneration ?? null,
3999
+ force === true
4000
+ ]);
4001
+ if (claimed) return {
4002
+ acquired: true,
4003
+ record: toRunOwnershipRecord(runId, claimed)
4004
+ };
4005
+ return {
4006
+ acquired: false,
4007
+ record: await this.#readRunOwner(t, runId)
4008
+ };
4009
+ }), { isRetriable: isRetriableRunFenceWrite });
4010
+ return result;
4011
+ } catch (error) {
4012
+ throw this.#ownershipError("CLAIM_RUN_OWNERSHIP", runId, error);
4013
+ }
4014
+ }
4015
+ async renewRunOwnership({ leaseMs, ...fence }) {
4016
+ try {
4017
+ const { result } = await withRetry(async () => {
4018
+ const renewed = await this.#db.client.oneOrNone(`UPDATE ${this.#runOwnersTable()}
4019
+ SET "leaseExpiresAt" = ${DB_NOW_MS} + $4, "updatedAt" = ${DB_NOW_MS}
4020
+ WHERE "runId" = $1 AND generation = $2 AND "ownerId" = $3 AND "leaseExpiresAt" IS NOT NULL
4021
+ RETURNING ${RUN_OWNER_COLUMNS}`, [
4022
+ fence.runId,
4023
+ fence.generation,
4024
+ fence.ownerId,
4025
+ leaseMs
4026
+ ]);
4027
+ if (renewed) return {
4028
+ renewed: true,
4029
+ record: toRunOwnershipRecord(fence.runId, renewed)
4030
+ };
4031
+ return {
4032
+ renewed: false,
4033
+ record: await this.#readRunOwner(this.#db.client, fence.runId)
4034
+ };
4035
+ });
4036
+ return result;
4037
+ } catch (error) {
4038
+ throw this.#ownershipError("RENEW_RUN_OWNERSHIP", fence.runId, error);
4039
+ }
4040
+ }
4041
+ async releaseRunOwnership(fence) {
4042
+ try {
4043
+ const { result } = await withRetry(() => this.#db.client.oneOrNone(`UPDATE ${this.#runOwnersTable()}
4044
+ SET "leaseExpiresAt" = NULL, "updatedAt" = ${DB_NOW_MS}
4045
+ WHERE "runId" = $1 AND generation = $2 AND "ownerId" = $3
4046
+ RETURNING "runId"`, [
4047
+ fence.runId,
4048
+ fence.generation,
4049
+ fence.ownerId
4050
+ ]));
4051
+ return result !== null;
4052
+ } catch (error) {
4053
+ throw this.#ownershipError("RELEASE_RUN_OWNERSHIP", fence.runId, error);
4054
+ }
4055
+ }
4056
+ async getRunOwnership({ runId }) {
4057
+ try {
4058
+ return await this.#readRunOwner(this.#db.client, runId);
4059
+ } catch (error) {
4060
+ throw this.#ownershipError("GET_RUN_OWNERSHIP", runId, error);
4061
+ }
4062
+ }
4063
+ async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state, fence: explicitFence }) {
4064
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence, runId);
3801
4065
  try {
3802
4066
  const { result: context } = await withRetry(async () => {
3803
4067
  return this.#db.client.tx(async (t) => {
4068
+ if (fence) await assertRunFence(t, this.#runOwnersTable(), fence, "updateWorkflowResults");
3804
4069
  const tableName = getTableName({
3805
4070
  indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
3806
4071
  schemaName: getSchemaName(this.#schema)
@@ -3849,6 +4114,7 @@ var WorkflowsDSQL = class WorkflowsDSQL extends _mastra_core_storage.WorkflowsSt
3849
4114
  } });
3850
4115
  return context;
3851
4116
  } catch (error) {
4117
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
3852
4118
  throw new _mastra_core_error.MastraError({
3853
4119
  id: (0, _mastra_core_storage.createStorageErrorId)("DSQL", "UPDATE_WORKFLOW_RESULTS", "FAILED"),
3854
4120
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -3861,10 +4127,12 @@ var WorkflowsDSQL = class WorkflowsDSQL extends _mastra_core_storage.WorkflowsSt
3861
4127
  }, error);
3862
4128
  }
3863
4129
  }
3864
- async updateWorkflowState({ workflowName, runId, opts }) {
4130
+ async updateWorkflowState({ workflowName, runId, opts, fence: explicitFence }) {
4131
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence, runId);
3865
4132
  try {
3866
4133
  const { result } = await withRetry(async () => {
3867
4134
  return this.#db.client.tx(async (t) => {
4135
+ if (fence) await assertRunFence(t, this.#runOwnersTable(), fence, "updateWorkflowState");
3868
4136
  const tableName = getTableName({
3869
4137
  indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
3870
4138
  schemaName: getSchemaName(this.#schema)
@@ -3893,6 +4161,7 @@ var WorkflowsDSQL = class WorkflowsDSQL extends _mastra_core_storage.WorkflowsSt
3893
4161
  } });
3894
4162
  return result;
3895
4163
  } catch (error) {
4164
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
3896
4165
  throw new _mastra_core_error.MastraError({
3897
4166
  id: (0, _mastra_core_storage.createStorageErrorId)("DSQL", "UPDATE_WORKFLOW_STATE", "FAILED"),
3898
4167
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -3904,12 +4173,13 @@ var WorkflowsDSQL = class WorkflowsDSQL extends _mastra_core_storage.WorkflowsSt
3904
4173
  }, error);
3905
4174
  }
3906
4175
  }
3907
- async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt }) {
4176
+ async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt, fence }) {
3908
4177
  try {
3909
4178
  const now = /* @__PURE__ */ new Date();
3910
4179
  const createdAtValue = createdAt ? createdAt : now;
3911
4180
  const updatedAtValue = updatedAt ? updatedAt : now;
3912
- await this.#db.client.none(`INSERT INTO ${getTableName({
4181
+ const resolvedFence = (0, _mastra_core_storage.resolveRunFence)(this, fence, runId);
4182
+ await withRetry(() => withRunFence(this.#db.client, this.#runOwnersTable(), resolvedFence, "persistWorkflowSnapshot", (q) => q.none(`INSERT INTO ${getTableName({
3913
4183
  indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
3914
4184
  schemaName: getSchemaName(this.#schema)
3915
4185
  })} (workflow_name, run_id, "resourceId", snapshot, "createdAt", "updatedAt")
@@ -3922,8 +4192,9 @@ var WorkflowsDSQL = class WorkflowsDSQL extends _mastra_core_storage.WorkflowsSt
3922
4192
  JSON.stringify(snapshot),
3923
4193
  createdAtValue.toISOString(),
3924
4194
  updatedAtValue.toISOString()
3925
- ]);
4195
+ ])));
3926
4196
  } catch (error) {
4197
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
3927
4198
  throw new _mastra_core_error.MastraError({
3928
4199
  id: (0, _mastra_core_storage.createStorageErrorId)("DSQL", "PERSIST_WORKFLOW_SNAPSHOT", "FAILED"),
3929
4200
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -3989,13 +4260,15 @@ var WorkflowsDSQL = class WorkflowsDSQL extends _mastra_core_storage.WorkflowsSt
3989
4260
  }, error);
3990
4261
  }
3991
4262
  }
3992
- async deleteWorkflowRunById({ runId, workflowName }) {
4263
+ async deleteWorkflowRunById({ runId, workflowName, fence }) {
3993
4264
  try {
3994
- await this.#db.client.none(`DELETE FROM ${getTableName({
4265
+ const resolvedFence = (0, _mastra_core_storage.resolveRunFence)(this, fence, runId);
4266
+ await withRetry(() => withRunFence(this.#db.client, this.#runOwnersTable(), resolvedFence, "deleteWorkflowRunById", (q) => q.none(`DELETE FROM ${getTableName({
3995
4267
  indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
3996
4268
  schemaName: getSchemaName(this.#schema)
3997
- })} WHERE run_id = $1 AND workflow_name = $2`, [runId, workflowName]);
4269
+ })} WHERE run_id = $1 AND workflow_name = $2`, [runId, workflowName])));
3998
4270
  } catch (error) {
4271
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
3999
4272
  throw new _mastra_core_error.MastraError({
4000
4273
  id: (0, _mastra_core_storage.createStorageErrorId)("DSQL", "DELETE_WORKFLOW_RUN_BY_ID", "FAILED"),
4001
4274
  domain: _mastra_core_error.ErrorDomain.STORAGE,