@mastra/mysql 0.13.1-alpha.0 → 0.13.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/index.cjs CHANGED
@@ -6109,9 +6109,81 @@ function isAppendOnlySince(storedObservations, snapshotObservations) {
6109
6109
  return stored === snapshot || stored.startsWith(snapshot);
6110
6110
  }
6111
6111
  //#endregion
6112
+ //#region src/storage/db/run-fencing.ts
6113
+ /**
6114
+ * Epoch milliseconds on the database clock. `UTC_TIMESTAMP` is the time the
6115
+ * statement started and doesn't depend on the session time zone. Every lease
6116
+ * comparison runs in a statement that already holds the run's owner row, so
6117
+ * no lock wait sits between that start time and the comparison.
6118
+ */
6119
+ const DB_NOW_MS = `(TIMESTAMPDIFF(MICROSECOND, '1970-01-01 00:00:00', UTC_TIMESTAMP(3)) DIV 1000)`;
6120
+ const ER_DUP_ENTRY = 1062;
6121
+ const ER_LOCK_DEADLOCK = 1213;
6122
+ const MAX_CLAIM_ATTEMPTS = 5;
6123
+ /**
6124
+ * The check half of a fenced write; call it first inside the write's
6125
+ * transaction. The shared lock holds the claim row until commit, so a claim
6126
+ * that updates the row waits for the write to finish, and every later write
6127
+ * sees the new claim.
6128
+ */
6129
+ async function assertRunFence(q, claimsTable, fence, operation) {
6130
+ const [rows] = await q.execute(`SELECT generation, ${quoteIdentifier("ownerId", "column name")} AS ownerId FROM ${claimsTable}
6131
+ WHERE ${quoteIdentifier("runId", "column name")} = ? LOCK IN SHARE MODE`, [fence.runId]);
6132
+ const row = rows[0];
6133
+ if (!(0, _mastra_core_storage.matchesRunFence)(row ? {
6134
+ generation: Number(row.generation),
6135
+ ownerId: String(row.ownerId)
6136
+ } : null, fence)) throw new _mastra_core_storage.RunFenceConflictError(fence, operation);
6137
+ }
6138
+ /** Runs `fn` in a transaction on its own pooled connection. */
6139
+ async function inTransaction(pool, fn) {
6140
+ const connection = await pool.getConnection();
6141
+ try {
6142
+ await connection.beginTransaction();
6143
+ const result = await fn(connection);
6144
+ await connection.commit();
6145
+ return result;
6146
+ } catch (error) {
6147
+ await connection.rollback().catch(() => {});
6148
+ throw error;
6149
+ } finally {
6150
+ connection.release();
6151
+ }
6152
+ }
6153
+ /**
6154
+ * Runs `write` directly when it carries no fence, or in a transaction behind
6155
+ * the fence check when it does.
6156
+ */
6157
+ function withRunFence(pool, claimsTable, fence, operation, write) {
6158
+ if (!fence) return write(pool);
6159
+ return inTransaction(pool, async (connection) => {
6160
+ await assertRunFence(connection, claimsTable, fence, operation);
6161
+ return write(connection);
6162
+ });
6163
+ }
6164
+ /**
6165
+ * Runs a transaction that creates or updates a run's claim row, retrying when
6166
+ * two first claims race to insert it. Under REPEATABLE READ both lock the gap
6167
+ * where the row would go and one is chosen as a deadlock victim; under READ
6168
+ * COMMITTED the later insert hits the duplicate key. On retry the row exists
6169
+ * and the claim serializes behind its row lock.
6170
+ */
6171
+ async function claimTransaction(pool, fn) {
6172
+ for (let attempt = 1;; attempt++) try {
6173
+ return await inTransaction(pool, fn);
6174
+ } catch (error) {
6175
+ const errno = error.errno;
6176
+ if (attempt >= MAX_CLAIM_ATTEMPTS || errno !== ER_DUP_ENTRY && errno !== ER_LOCK_DEADLOCK) throw error;
6177
+ }
6178
+ }
6179
+ //#endregion
6112
6180
  //#region src/storage/domains/memory/index.ts
6113
6181
  const OM_TABLE = "mastra_observational_memory";
6114
6182
  const OM_TABLE_QUOTED = quoteIdentifier(OM_TABLE, "table name");
6183
+ const RUN_FENCES_TABLE = formatTableName(_mastra_core_storage.TABLE_MEMORY_RUN_FENCES);
6184
+ const RUN_ID$1 = quoteIdentifier("runId", "column name");
6185
+ const OWNER_ID$1 = quoteIdentifier("ownerId", "column name");
6186
+ const RETIRED_AT = quoteIdentifier("retiredAt", "column name");
6115
6187
  /** Canonical head order: newest generation, then earliest createdAt, then lowest id. */
6116
6188
  const OM_HEAD_ORDER = `${quoteIdentifier("generationCount", "column name")} DESC, ${quoteIdentifier("createdAt", "column name")} ASC, ${quoteIdentifier("id", "column name")} ASC`;
6117
6189
  /** Max `supersededBy` hops when redirecting a write aimed at a retired record. */
@@ -6218,6 +6290,69 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6218
6290
  this.#skipDefaultIndexes = skipDefaultIndexes;
6219
6291
  this.#indexes = indexes?.filter((idx) => MemoryMySQL.MANAGED_TABLES.includes(idx.table));
6220
6292
  }
6293
+ supportsRunFencing() {
6294
+ return true;
6295
+ }
6296
+ async raiseRunFence(fence) {
6297
+ try {
6298
+ return await claimTransaction(this.pool, async (connection) => {
6299
+ const [rows] = await connection.execute(`SELECT generation, ${OWNER_ID$1} AS ownerId FROM ${RUN_FENCES_TABLE} WHERE ${RUN_ID$1} = ? FOR UPDATE`, [fence.runId]);
6300
+ const row = rows[0];
6301
+ if (!row) {
6302
+ await connection.execute(`INSERT INTO ${RUN_FENCES_TABLE} (${RUN_ID$1}, generation, ${OWNER_ID$1}) VALUES (?, ?, ?)`, [
6303
+ fence.runId,
6304
+ fence.generation,
6305
+ fence.ownerId
6306
+ ]);
6307
+ return true;
6308
+ }
6309
+ const generation = Number(row.generation);
6310
+ const raises = generation < fence.generation;
6311
+ const reaffirms = generation === fence.generation && String(row.ownerId) === fence.ownerId;
6312
+ if (!raises && !reaffirms) return false;
6313
+ await connection.execute(`UPDATE ${RUN_FENCES_TABLE} SET generation = ?, ${OWNER_ID$1} = ?, ${RETIRED_AT} = NULL WHERE ${RUN_ID$1} = ?`, [
6314
+ fence.generation,
6315
+ fence.ownerId,
6316
+ fence.runId
6317
+ ]);
6318
+ return true;
6319
+ });
6320
+ } catch (error) {
6321
+ throw new _mastra_core_error.MastraError({
6322
+ id: (0, _mastra_core_storage.createStorageErrorId)("MYSQL", "RAISE_RUN_FENCE", "FAILED"),
6323
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
6324
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
6325
+ details: { runId: fence.runId }
6326
+ }, error);
6327
+ }
6328
+ }
6329
+ async retireRunFence(fence) {
6330
+ try {
6331
+ return await inTransaction(this.pool, async (connection) => {
6332
+ const [rows] = await connection.execute(`SELECT generation, ${OWNER_ID$1} AS ownerId FROM ${RUN_FENCES_TABLE} WHERE ${RUN_ID$1} = ? FOR UPDATE`, [fence.runId]);
6333
+ const row = rows[0];
6334
+ if (!row || Number(row.generation) !== fence.generation || String(row.ownerId) !== fence.ownerId) return false;
6335
+ await connection.execute(`UPDATE ${RUN_FENCES_TABLE} SET ${RETIRED_AT} = ${DB_NOW_MS} WHERE ${RUN_ID$1} = ?`, [fence.runId]);
6336
+ return true;
6337
+ });
6338
+ } catch (error) {
6339
+ throw new _mastra_core_error.MastraError({
6340
+ id: (0, _mastra_core_storage.createStorageErrorId)("MYSQL", "RETIRE_RUN_FENCE", "FAILED"),
6341
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
6342
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
6343
+ details: { runId: fence.runId }
6344
+ }, error);
6345
+ }
6346
+ }
6347
+ /** Runs `write` behind its own fence, otherwise the one in scope for this store. */
6348
+ #fenced(fence, operation, write) {
6349
+ return withRunFence(this.pool, RUN_FENCES_TABLE, (0, _mastra_core_storage.resolveRunFence)(this, fence), operation, write);
6350
+ }
6351
+ /** The fence check for a write that already runs in its own transaction. */
6352
+ async #assertFence(connection, fence, operation) {
6353
+ const resolved = (0, _mastra_core_storage.resolveRunFence)(this, fence);
6354
+ if (resolved) await assertRunFence(connection, RUN_FENCES_TABLE, resolved, operation);
6355
+ }
6221
6356
  async init() {
6222
6357
  await this.operations.createTable({
6223
6358
  tableName: _mastra_core_storage.TABLE_THREADS,
@@ -6231,6 +6366,10 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6231
6366
  tableName: _mastra_core_storage.TABLE_RESOURCES,
6232
6367
  schema: _mastra_core_storage.TABLE_SCHEMAS[_mastra_core_storage.TABLE_RESOURCES]
6233
6368
  });
6369
+ await this.operations.createTable({
6370
+ tableName: _mastra_core_storage.TABLE_MEMORY_RUN_FENCES,
6371
+ schema: _mastra_core_storage.RUN_FENCING_TABLE_SCHEMAS[_mastra_core_storage.TABLE_MEMORY_RUN_FENCES]
6372
+ });
6234
6373
  const omSchema = withObservationalMemoryLifecycleColumns(_mastra_core_storage.OBSERVATIONAL_MEMORY_TABLE_SCHEMA?.[OM_TABLE]);
6235
6374
  if (omSchema) {
6236
6375
  await this.operations.createTable({
@@ -6298,6 +6437,10 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6298
6437
  tableName,
6299
6438
  schema: _mastra_core_storage.TABLE_SCHEMAS[tableName]
6300
6439
  }));
6440
+ statements.push(generateTableSQL({
6441
+ tableName: _mastra_core_storage.TABLE_MEMORY_RUN_FENCES,
6442
+ schema: _mastra_core_storage.RUN_FENCING_TABLE_SCHEMAS[_mastra_core_storage.TABLE_MEMORY_RUN_FENCES]
6443
+ }));
6301
6444
  for (const idx of MemoryMySQL.getDefaultIndexDefs()) statements.push(generateIndexSQL(idx));
6302
6445
  return statements;
6303
6446
  }
@@ -6316,6 +6459,7 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6316
6459
  await this.pool.execute(`DELETE FROM ${formatTableName(_mastra_core_storage.TABLE_MESSAGES)}`);
6317
6460
  await this.pool.execute(`DELETE FROM ${formatTableName(_mastra_core_storage.TABLE_THREADS)}`);
6318
6461
  await this.pool.execute(`DELETE FROM ${formatTableName(_mastra_core_storage.TABLE_RESOURCES)}`);
6462
+ await this.pool.execute(`DELETE FROM ${RUN_FENCES_TABLE}`);
6319
6463
  try {
6320
6464
  await this.pool.execute(`DELETE FROM ${OM_TABLE_QUOTED}`);
6321
6465
  } catch (err) {
@@ -6496,8 +6640,8 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6496
6640
  }
6497
6641
  return includeMessages;
6498
6642
  }
6499
- async upsertThread(thread) {
6500
- await this.operations.insert({
6643
+ async upsertThread(q, thread) {
6644
+ const statement = prepareStatement({
6501
6645
  tableName: _mastra_core_storage.TABLE_THREADS,
6502
6646
  record: {
6503
6647
  id: thread.id,
@@ -6508,18 +6652,22 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6508
6652
  updatedAt: thread.updatedAt
6509
6653
  }
6510
6654
  });
6655
+ await q.execute(statement.sql, statement.args);
6656
+ }
6657
+ async #readThread(q, threadId, resourceId) {
6658
+ let sql = `SELECT * FROM ${formatTableName(_mastra_core_storage.TABLE_THREADS)} WHERE ${quoteIdentifier("id", "column name")} = ?`;
6659
+ const params = [threadId];
6660
+ if (resourceId !== void 0) {
6661
+ sql += ` AND ${quoteIdentifier("resourceId", "column name")} = ?`;
6662
+ params.push(resourceId);
6663
+ }
6664
+ const [rows] = await q.execute(sql, params);
6665
+ const row = rows[0];
6666
+ return row ? this.mapThread(row) : null;
6511
6667
  }
6512
6668
  async getThreadById({ threadId, resourceId }) {
6513
6669
  try {
6514
- let sql = `SELECT * FROM ${formatTableName(_mastra_core_storage.TABLE_THREADS)} WHERE ${quoteIdentifier("id", "column name")} = ?`;
6515
- const params = [threadId];
6516
- if (resourceId !== void 0) {
6517
- sql += ` AND ${quoteIdentifier("resourceId", "column name")} = ?`;
6518
- params.push(resourceId);
6519
- }
6520
- const [rows] = await this.pool.execute(sql, params);
6521
- const row = rows[0];
6522
- return row ? this.mapThread(row) : null;
6670
+ return await this.#readThread(this.pool, threadId, resourceId);
6523
6671
  } catch (error) {
6524
6672
  throw new _mastra_core_error.MastraError({
6525
6673
  id: "MYSQL_MEMORY_GET_THREAD_BY_ID_FAILED",
@@ -6672,11 +6820,12 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6672
6820
  } : void 0
6673
6821
  });
6674
6822
  }
6675
- async saveThread({ thread }) {
6823
+ async saveThread({ thread, fence }) {
6676
6824
  try {
6677
- await this.upsertThread(thread);
6825
+ await this.#fenced(fence, "saveThread", (q) => this.upsertThread(q, thread));
6678
6826
  return thread;
6679
6827
  } catch (error) {
6828
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
6680
6829
  throw new _mastra_core_error.MastraError({
6681
6830
  id: "MYSQL_MEMORY_SAVE_THREAD_FAILED",
6682
6831
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -6685,36 +6834,39 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6685
6834
  }, error);
6686
6835
  }
6687
6836
  }
6688
- async updateThread({ id, title, metadata }) {
6837
+ async updateThread({ id, title, metadata, fence }) {
6689
6838
  try {
6690
- const existing = await this.getThreadById({ threadId: id });
6691
- if (!existing) throw new _mastra_core_error.MastraError({
6692
- id: (0, _mastra_core_storage.createStorageErrorId)("MYSQL", "UPDATE_THREAD", "NOT_FOUND"),
6693
- domain: _mastra_core_error.ErrorDomain.STORAGE,
6694
- category: _mastra_core_error.ErrorCategory.USER,
6695
- text: `Thread ${id} not found`,
6696
- details: { threadId: id }
6697
- });
6698
- const mergedMetadata = {
6699
- ...existing.metadata ?? {},
6700
- ...metadata ?? {}
6701
- };
6702
- const updatedAt = /* @__PURE__ */ new Date();
6703
- await this.operations.update({
6704
- tableName: _mastra_core_storage.TABLE_THREADS,
6705
- keys: { id },
6706
- data: {
6839
+ return await this.#fenced(fence, "updateThread", async (q) => {
6840
+ const existing = await this.#readThread(q, id);
6841
+ if (!existing) throw new _mastra_core_error.MastraError({
6842
+ id: (0, _mastra_core_storage.createStorageErrorId)("MYSQL", "UPDATE_THREAD", "NOT_FOUND"),
6843
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
6844
+ category: _mastra_core_error.ErrorCategory.USER,
6845
+ text: `Thread ${id} not found`,
6846
+ details: { threadId: id }
6847
+ });
6848
+ const mergedMetadata = {
6849
+ ...existing.metadata ?? {},
6850
+ ...metadata ?? {}
6851
+ };
6852
+ const updatedAt = /* @__PURE__ */ new Date();
6853
+ const statement = prepareUpdateStatement({
6854
+ tableName: _mastra_core_storage.TABLE_THREADS,
6855
+ keys: { id },
6856
+ updates: {
6857
+ title: title ?? existing.title,
6858
+ metadata: JSON.stringify(mergedMetadata),
6859
+ updatedAt
6860
+ }
6861
+ });
6862
+ await q.execute(statement.sql, statement.args);
6863
+ return {
6864
+ ...existing,
6707
6865
  title: title ?? existing.title,
6708
- metadata: JSON.stringify(mergedMetadata),
6866
+ metadata: mergedMetadata,
6709
6867
  updatedAt
6710
- }
6868
+ };
6711
6869
  });
6712
- return {
6713
- ...existing,
6714
- title: title ?? existing.title,
6715
- metadata: mergedMetadata,
6716
- updatedAt
6717
- };
6718
6870
  } catch (error) {
6719
6871
  if (error instanceof _mastra_core_error.MastraError) throw error;
6720
6872
  throw new _mastra_core_error.MastraError({
@@ -6795,6 +6947,7 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6795
6947
  const insertedIds = [];
6796
6948
  try {
6797
6949
  await connection.beginTransaction();
6950
+ await this.#assertFence(connection, args.fence, "saveMessages");
6798
6951
  for (const message of messages) {
6799
6952
  if (!message.threadId) throw new _mastra_core_error.MastraError({
6800
6953
  id: (0, _mastra_core_storage.createStorageErrorId)("MYSQL", "SAVE_MESSAGES", "INVALID_INPUT"),
@@ -6888,18 +7041,29 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6888
7041
  if (update.threadId && update.threadId !== current.threadId && current.threadId) affectedThreads.set(current.threadId, now);
6889
7042
  }
6890
7043
  if (!updates.length) return existing;
6891
- await this.operations.batchUpdate({
6892
- tableName: _mastra_core_storage.TABLE_MESSAGES,
6893
- items: updates
6894
- });
6895
- for (const [threadId, timestamp] of affectedThreads.entries()) await this.operations.update({
6896
- tableName: _mastra_core_storage.TABLE_THREADS,
6897
- keys: { id: threadId },
6898
- data: { updatedAt: timestamp }
7044
+ await inTransaction(this.pool, async (connection) => {
7045
+ await this.#assertFence(connection, args.fence, "updateMessages");
7046
+ for (const { keys, data } of updates) {
7047
+ const statement = prepareUpdateStatement({
7048
+ tableName: _mastra_core_storage.TABLE_MESSAGES,
7049
+ updates: data,
7050
+ keys
7051
+ });
7052
+ await connection.execute(statement.sql, statement.args);
7053
+ }
7054
+ for (const [threadId, timestamp] of affectedThreads.entries()) {
7055
+ const statement = prepareUpdateStatement({
7056
+ tableName: _mastra_core_storage.TABLE_THREADS,
7057
+ updates: { updatedAt: timestamp },
7058
+ keys: { id: threadId }
7059
+ });
7060
+ await connection.execute(statement.sql, statement.args);
7061
+ }
6899
7062
  });
6900
7063
  const { messages: updated } = await this.listMessagesById({ messageIds: messages.map((m) => m.id) });
6901
7064
  return updated;
6902
7065
  } catch (error) {
7066
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
6903
7067
  throw new _mastra_core_error.MastraError({
6904
7068
  id: "MYSQL_MEMORY_UPDATE_MESSAGES_FAILED",
6905
7069
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -6907,19 +7071,22 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
6907
7071
  }, error);
6908
7072
  }
6909
7073
  }
6910
- async deleteMessages(messageIds) {
7074
+ async deleteMessages(messageIds, options) {
6911
7075
  if (!messageIds.length) return;
6912
7076
  try {
6913
- const placeholders = messageIds.map(() => "?").join(", ");
6914
- const [rows] = await this.pool.execute(`SELECT DISTINCT thread_id FROM ${formatTableName(_mastra_core_storage.TABLE_MESSAGES)} WHERE id IN (${placeholders})`, messageIds);
6915
- const threadIds = rows.map((row) => row.thread_id).filter((threadId) => Boolean(threadId));
6916
- await this.pool.execute(`DELETE FROM ${formatTableName(_mastra_core_storage.TABLE_MESSAGES)} WHERE id IN (${placeholders})`, messageIds);
6917
- if (threadIds.length) {
6918
- const threadPlaceholders = threadIds.map(() => "?").join(", ");
6919
- const timestamp = transformToSqlValue(/* @__PURE__ */ new Date());
6920
- await this.pool.execute(`UPDATE ${formatTableName(_mastra_core_storage.TABLE_THREADS)} SET ${quoteIdentifier("updatedAt", "column name")} = ? WHERE id IN (${threadPlaceholders})`, [timestamp, ...threadIds]);
6921
- }
7077
+ await this.#fenced(options?.fence, "deleteMessages", async (q) => {
7078
+ const placeholders = messageIds.map(() => "?").join(", ");
7079
+ const [rows] = await q.execute(`SELECT DISTINCT thread_id FROM ${formatTableName(_mastra_core_storage.TABLE_MESSAGES)} WHERE id IN (${placeholders})`, messageIds);
7080
+ const threadIds = rows.map((row) => row.thread_id).filter((threadId) => Boolean(threadId));
7081
+ await q.execute(`DELETE FROM ${formatTableName(_mastra_core_storage.TABLE_MESSAGES)} WHERE id IN (${placeholders})`, messageIds);
7082
+ if (threadIds.length) {
7083
+ const threadPlaceholders = threadIds.map(() => "?").join(", ");
7084
+ const timestamp = transformToSqlValue(/* @__PURE__ */ new Date());
7085
+ await q.execute(`UPDATE ${formatTableName(_mastra_core_storage.TABLE_THREADS)} SET ${quoteIdentifier("updatedAt", "column name")} = ? WHERE id IN (${threadPlaceholders})`, [timestamp, ...threadIds]);
7086
+ }
7087
+ });
6922
7088
  } catch (error) {
7089
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
6923
7090
  throw new _mastra_core_error.MastraError({
6924
7091
  id: "MYSQL_MEMORY_DELETE_MESSAGES_FAILED",
6925
7092
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -7318,19 +7485,23 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7318
7485
  }, error);
7319
7486
  }
7320
7487
  }
7488
+ async #insertResource(q, resource) {
7489
+ const metadataValue = resource.metadata === void 0 || resource.metadata === null ? null : JSON.stringify(resource.metadata);
7490
+ const statement = prepareStatement({
7491
+ tableName: _mastra_core_storage.TABLE_RESOURCES,
7492
+ record: {
7493
+ id: resource.id,
7494
+ workingMemory: resource.workingMemory ?? null,
7495
+ metadata: metadataValue,
7496
+ createdAt: resource.createdAt ?? /* @__PURE__ */ new Date(),
7497
+ updatedAt: resource.updatedAt ?? /* @__PURE__ */ new Date()
7498
+ }
7499
+ });
7500
+ await q.execute(statement.sql, statement.args);
7501
+ }
7321
7502
  async saveResource({ resource }) {
7322
7503
  try {
7323
- const metadataValue = resource.metadata === void 0 || resource.metadata === null ? null : JSON.stringify(resource.metadata);
7324
- await this.operations.insert({
7325
- tableName: _mastra_core_storage.TABLE_RESOURCES,
7326
- record: {
7327
- id: resource.id,
7328
- workingMemory: resource.workingMemory ?? null,
7329
- metadata: metadataValue,
7330
- createdAt: resource.createdAt ?? /* @__PURE__ */ new Date(),
7331
- updatedAt: resource.updatedAt ?? /* @__PURE__ */ new Date()
7332
- }
7333
- });
7504
+ await this.#insertResource(this.pool, resource);
7334
7505
  return resource;
7335
7506
  } catch (error) {
7336
7507
  throw new _mastra_core_error.MastraError({
@@ -7341,7 +7512,7 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7341
7512
  }, error);
7342
7513
  }
7343
7514
  }
7344
- async updateResource({ resourceId, workingMemory, metadata }) {
7515
+ async updateResource({ resourceId, workingMemory, metadata, fence }) {
7345
7516
  try {
7346
7517
  const existing = await this.getResourceById({ resourceId });
7347
7518
  if (!existing) {
@@ -7352,22 +7523,24 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7352
7523
  createdAt: /* @__PURE__ */ new Date(),
7353
7524
  updatedAt: /* @__PURE__ */ new Date()
7354
7525
  };
7355
- return await this.saveResource({ resource: newResource });
7526
+ await this.#fenced(fence, "updateResource", (q) => this.#insertResource(q, newResource));
7527
+ return newResource;
7356
7528
  }
7357
7529
  const mergedMetadata = metadata !== void 0 ? {
7358
7530
  ...existing.metadata ?? {},
7359
7531
  ...metadata ?? {}
7360
7532
  } : existing.metadata;
7361
7533
  const metadataValue = metadata !== void 0 ? metadata === null ? null : JSON.stringify(mergedMetadata ?? {}) : void 0;
7362
- await this.operations.update({
7534
+ const statement = prepareUpdateStatement({
7363
7535
  tableName: _mastra_core_storage.TABLE_RESOURCES,
7364
7536
  keys: { id: resourceId },
7365
- data: {
7537
+ updates: {
7366
7538
  ...workingMemory !== void 0 ? { workingMemory } : {},
7367
7539
  ...metadataValue !== void 0 ? { metadata: metadataValue } : {},
7368
7540
  updatedAt: /* @__PURE__ */ new Date()
7369
7541
  }
7370
7542
  });
7543
+ await this.#fenced(fence, "updateResource", (q) => q.execute(statement.sql, statement.args));
7371
7544
  const updated = await this.getResourceById({ resourceId });
7372
7545
  if (!updated) throw new _mastra_core_error.MastraError({
7373
7546
  id: (0, _mastra_core_storage.createStorageErrorId)("MYSQL", "UPDATE_RESOURCE", "NOT_FOUND_AFTER_UPDATE"),
@@ -7572,7 +7745,7 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7572
7745
  input.id
7573
7746
  ]);
7574
7747
  return { applied: true };
7575
- });
7748
+ }, "commitActiveObservations");
7576
7749
  } catch (error) {
7577
7750
  rethrowOrWrapOM(error, input.id, "UPDATE_ACTIVE_OBSERVATIONS");
7578
7751
  }
@@ -7595,11 +7768,95 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7595
7768
  });
7596
7769
  if (!plan) return stored;
7597
7770
  return this.rollOverOMRow(connection, stored.id, plan.observations, plan.tokenCount, input.newRecordId, false);
7598
- });
7771
+ }, "createReflectionGeneration");
7599
7772
  } catch (error) {
7600
7773
  rethrowOrWrapOM(error, input.currentRecord.id, "CREATE_REFLECTION_GENERATION", { currentRecordId: input.currentRecord.id });
7601
7774
  }
7602
7775
  }
7776
+ async #insertReflectionGeneration(q, input) {
7777
+ const id = globalThis.crypto.randomUUID();
7778
+ const now = /* @__PURE__ */ new Date();
7779
+ const lookupKey = this.getOMKey(input.currentRecord.threadId, input.currentRecord.resourceId);
7780
+ const record = {
7781
+ id,
7782
+ scope: input.currentRecord.scope,
7783
+ threadId: input.currentRecord.threadId,
7784
+ resourceId: input.currentRecord.resourceId,
7785
+ createdAt: now,
7786
+ updatedAt: now,
7787
+ lastObservedAt: input.currentRecord.lastObservedAt,
7788
+ originType: "reflection",
7789
+ generationCount: input.currentRecord.generationCount + 1,
7790
+ activeObservations: input.reflection,
7791
+ totalTokensObserved: input.currentRecord.totalTokensObserved,
7792
+ observationTokenCount: input.tokenCount,
7793
+ pendingMessageTokens: 0,
7794
+ isReflecting: false,
7795
+ isObserving: false,
7796
+ isBufferingObservation: false,
7797
+ isBufferingReflection: false,
7798
+ lastBufferedAtTokens: 0,
7799
+ lastBufferedAtTime: null,
7800
+ config: input.currentRecord.config,
7801
+ metadata: input.currentRecord.metadata,
7802
+ observedTimezone: input.currentRecord.observedTimezone
7803
+ };
7804
+ const nowSql = transformToSqlValue(now);
7805
+ const cols = [
7806
+ "id",
7807
+ "lookupKey",
7808
+ "scope",
7809
+ "resourceId",
7810
+ "threadId",
7811
+ "activeObservations",
7812
+ "activeObservationsPendingUpdate",
7813
+ "originType",
7814
+ "config",
7815
+ "generationCount",
7816
+ "lastObservedAt",
7817
+ "lastReflectionAt",
7818
+ "pendingMessageTokens",
7819
+ "totalTokensObserved",
7820
+ "observationTokenCount",
7821
+ "isObserving",
7822
+ "isReflecting",
7823
+ "isBufferingObservation",
7824
+ "isBufferingReflection",
7825
+ "lastBufferedAtTokens",
7826
+ "lastBufferedAtTime",
7827
+ "observedTimezone",
7828
+ "createdAt",
7829
+ "updatedAt"
7830
+ ].map(omCol).join(", ");
7831
+ const placeholders = Array.from({ length: 24 }, () => "?").join(", ");
7832
+ await q.execute(`INSERT INTO ${OM_TABLE_QUOTED} (${cols}) VALUES (${placeholders})`, [
7833
+ id,
7834
+ lookupKey,
7835
+ record.scope,
7836
+ record.resourceId,
7837
+ record.threadId || null,
7838
+ input.reflection,
7839
+ null,
7840
+ "reflection",
7841
+ JSON.stringify(record.config),
7842
+ input.currentRecord.generationCount + 1,
7843
+ record.lastObservedAt ? transformToSqlValue(record.lastObservedAt) : null,
7844
+ nowSql,
7845
+ record.pendingMessageTokens,
7846
+ record.totalTokensObserved,
7847
+ record.observationTokenCount,
7848
+ false,
7849
+ false,
7850
+ false,
7851
+ false,
7852
+ 0,
7853
+ null,
7854
+ record.observedTimezone || null,
7855
+ nowSql,
7856
+ nowSql
7857
+ ]);
7858
+ return record;
7859
+ }
7603
7860
  async setReflectingFlag(id, isReflecting) {
7604
7861
  await this.updateOMFlag(id, "isReflecting", isReflecting, "SET_REFLECTING_FLAG");
7605
7862
  }
@@ -7705,7 +7962,7 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7705
7962
  persisted: true,
7706
7963
  recordId: target.id
7707
7964
  };
7708
- });
7965
+ }, "appendBufferedObservations");
7709
7966
  } catch (error) {
7710
7967
  rethrowOrWrapOM(error, input.id, "UPDATE_BUFFERED_OBSERVATIONS");
7711
7968
  }
@@ -7817,7 +8074,7 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7817
8074
  suggestedContinuation: latestChunkHints?.suggestedContinuation ?? void 0,
7818
8075
  currentTask: latestChunkHints?.currentTask ?? void 0
7819
8076
  };
7820
- });
8077
+ }, "swapBufferedToActive");
7821
8078
  } catch (error) {
7822
8079
  rethrowOrWrapOM(error, input.id, "SWAP_BUFFERED_TO_ACTIVE");
7823
8080
  }
@@ -7826,7 +8083,7 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7826
8083
  try {
7827
8084
  const nowSql = transformToSqlValue(/* @__PURE__ */ new Date());
7828
8085
  const br = omCol("bufferedReflection");
7829
- const [result] = await this.pool.execute(`UPDATE ${OM_TABLE_QUOTED} SET
8086
+ const [result] = await this.#fenced(void 0, "updateBufferedReflection", (q) => q.execute(`UPDATE ${OM_TABLE_QUOTED} SET
7830
8087
  ${br} = CASE
7831
8088
  WHEN ${br} IS NOT NULL AND ${br} != ''
7832
8089
  THEN CONCAT(${br}, CHAR(10), CHAR(10), ?)
@@ -7844,7 +8101,7 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7844
8101
  input.reflectedObservationLineCount,
7845
8102
  nowSql,
7846
8103
  input.id
7847
- ]);
8104
+ ]));
7848
8105
  if (result.affectedRows === 0) throwOMNotFound(input.id, "UPDATE_BUFFERED_REFLECTION");
7849
8106
  } catch (error) {
7850
8107
  rethrowOrWrapOM(error, input.id, "UPDATE_BUFFERED_REFLECTION");
@@ -7872,7 +8129,7 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7872
8129
  const newObservations = unreflectedContent ? `${bufferedReflection}\n\n${unreflectedContent}` : bufferedReflection;
7873
8130
  const tokenCount = input.tokenCount + Math.max(0, (stored.observationTokenCount ?? 0) - (currentRecord.observationTokenCount ?? 0));
7874
8131
  return this.rollOverOMRow(connection, stored.id, newObservations, tokenCount, input.newRecordId, true);
7875
- });
8132
+ }, "swapBufferedReflectionToActive");
7876
8133
  } catch (error) {
7877
8134
  rethrowOrWrapOM(error, input.currentRecord.id, "SWAP_BUFFERED_REFLECTION_TO_ACTIVE");
7878
8135
  }
@@ -7883,11 +8140,12 @@ var MemoryMySQL = class MemoryMySQL extends _mastra_core_storage.MemoryStorage {
7883
8140
  * only expected lock conflict is a wait; a deadlock or lock-wait timeout (e.g. against the
7884
8141
  * startup backfill's scan) is retried a bounded number of times.
7885
8142
  */
7886
- async withOMTransaction(fn) {
8143
+ async withOMTransaction(fn, fencedOperation) {
7887
8144
  for (let attempt = 1;; attempt++) {
7888
8145
  const connection = await this.pool.getConnection();
7889
8146
  try {
7890
8147
  await connection.beginTransaction();
8148
+ if (fencedOperation) await this.#assertFence(connection, void 0, fencedOperation);
7891
8149
  const result = await fn(connection);
7892
8150
  await connection.commit();
7893
8151
  return result;
@@ -11064,6 +11322,26 @@ function parseSnapshot(snapshot) {
11064
11322
  }
11065
11323
  return snapshot;
11066
11324
  }
11325
+ const RUN_OWNERS_TABLE = formatTableName(_mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS);
11326
+ const RUN_ID = quoteIdentifier("runId", "column name");
11327
+ const OWNER_ID = quoteIdentifier("ownerId", "column name");
11328
+ const LEASE_EXPIRES_AT = quoteIdentifier("leaseExpiresAt", "column name");
11329
+ const UPDATED_AT = quoteIdentifier("updatedAt", "column name");
11330
+ const RUN_OWNER_COLUMNS = `generation, ${OWNER_ID} AS ownerId, ${LEASE_EXPIRES_AT} AS leaseExpiresAt, ${DB_NOW_MS} AS nowMs`;
11331
+ function toRunOwnershipRecord(runId, row) {
11332
+ const leaseExpiresAt = row.leaseExpiresAt === null ? null : Number(row.leaseExpiresAt);
11333
+ return {
11334
+ runId,
11335
+ generation: Number(row.generation),
11336
+ ownerId: String(row.ownerId),
11337
+ leaseExpiresAt: leaseExpiresAt === null ? null : new Date(leaseExpiresAt),
11338
+ live: leaseExpiresAt !== null && leaseExpiresAt > Number(row.nowMs)
11339
+ };
11340
+ }
11341
+ async function readRunOwner(q, runId) {
11342
+ const [rows] = await q.execute(`SELECT ${RUN_OWNER_COLUMNS} FROM ${RUN_OWNERS_TABLE} WHERE ${RUN_ID} = ?`, [runId]);
11343
+ return rows[0] ? toRunOwnershipRecord(runId, rows[0]) : null;
11344
+ }
11067
11345
  function mapWorkflowRow(row) {
11068
11346
  return {
11069
11347
  workflowName: row.workflow_name,
@@ -11096,6 +11374,9 @@ var WorkflowsMySQL = class WorkflowsMySQL extends _mastra_core_storage.Workflows
11096
11374
  tableName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
11097
11375
  schema: _mastra_core_storage.TABLE_SCHEMAS[_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT],
11098
11376
  compositePrimaryKey: ["workflow_name", "run_id"]
11377
+ }), generateTableSQL({
11378
+ tableName: _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS,
11379
+ schema: _mastra_core_storage.RUN_FENCING_TABLE_SCHEMAS[_mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS]
11099
11380
  })];
11100
11381
  }
11101
11382
  constructor({ operations, pool, skipDefaultIndexes, indexes }) {
@@ -11139,16 +11420,125 @@ var WorkflowsMySQL = class WorkflowsMySQL extends _mastra_core_storage.Workflows
11139
11420
  schema,
11140
11421
  ifNotExists: ["resourceId"]
11141
11422
  });
11423
+ await this.operations.createTable({
11424
+ tableName: _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS,
11425
+ schema: _mastra_core_storage.RUN_FENCING_TABLE_SCHEMAS[_mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS]
11426
+ });
11142
11427
  await this.createDefaultIndexes();
11143
11428
  await this.createCustomIndexes();
11144
11429
  }
11145
11430
  async dangerouslyClearAll() {
11146
11431
  await this.pool.execute(`DELETE FROM ${formatTableName(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT)}`);
11432
+ await this.pool.execute(`DELETE FROM ${RUN_OWNERS_TABLE}`);
11433
+ }
11434
+ supportsRunFencing() {
11435
+ return true;
11436
+ }
11437
+ #ownershipError(operation, runId, error) {
11438
+ return new _mastra_core_error.MastraError({
11439
+ id: (0, _mastra_core_storage.createStorageErrorId)("MYSQL", operation, "FAILED"),
11440
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
11441
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
11442
+ details: { runId }
11443
+ }, error);
11444
+ }
11445
+ async claimRunOwnership({ runId, ownerId, leaseMs, force, expectedGeneration }) {
11446
+ try {
11447
+ return await claimTransaction(this.pool, async (connection) => {
11448
+ const [existing] = await connection.execute(`SELECT generation FROM ${RUN_OWNERS_TABLE} WHERE ${RUN_ID} = ? FOR UPDATE`, [runId]);
11449
+ if (existing.length === 0) {
11450
+ if (expectedGeneration !== void 0 && expectedGeneration !== 0) return {
11451
+ acquired: false,
11452
+ record: null
11453
+ };
11454
+ await connection.execute(`INSERT INTO ${RUN_OWNERS_TABLE} (${RUN_ID}, generation, ${OWNER_ID}, ${LEASE_EXPIRES_AT}, ${UPDATED_AT})
11455
+ VALUES (?, GREATEST(1, ${DB_NOW_MS}), ?, ${DB_NOW_MS} + ?, ${DB_NOW_MS})`, [
11456
+ runId,
11457
+ ownerId,
11458
+ leaseMs
11459
+ ]);
11460
+ return {
11461
+ acquired: true,
11462
+ record: await readRunOwner(connection, runId)
11463
+ };
11464
+ }
11465
+ await connection.execute(`UPDATE ${RUN_OWNERS_TABLE}
11466
+ SET generation = generation + 1, ${OWNER_ID} = ?, ${LEASE_EXPIRES_AT} = ${DB_NOW_MS} + ?,
11467
+ ${UPDATED_AT} = ${DB_NOW_MS}
11468
+ WHERE ${RUN_ID} = ?
11469
+ AND (? IS NULL OR generation = ?)
11470
+ AND (? OR ${LEASE_EXPIRES_AT} IS NULL OR ${LEASE_EXPIRES_AT} <= ${DB_NOW_MS})`, [
11471
+ ownerId,
11472
+ leaseMs,
11473
+ runId,
11474
+ expectedGeneration ?? null,
11475
+ expectedGeneration ?? null,
11476
+ force === true
11477
+ ]);
11478
+ const record = await readRunOwner(connection, runId);
11479
+ return record && record.generation === Number(existing[0].generation) + 1 ? {
11480
+ acquired: true,
11481
+ record
11482
+ } : {
11483
+ acquired: false,
11484
+ record
11485
+ };
11486
+ });
11487
+ } catch (error) {
11488
+ throw this.#ownershipError("CLAIM_RUN_OWNERSHIP", runId, error);
11489
+ }
11147
11490
  }
11148
- async deleteWorkflowRunById({ runId, workflowName }) {
11491
+ async renewRunOwnership({ leaseMs, ...fence }) {
11149
11492
  try {
11150
- await this.pool.execute(`DELETE FROM ${formatTableName(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT)} WHERE ${quoteIdentifier("workflow_name", "column name")} = ? AND ${quoteIdentifier("run_id", "column name")} = ?`, [workflowName, runId]);
11493
+ return await inTransaction(this.pool, async (connection) => {
11494
+ await connection.execute(`UPDATE ${RUN_OWNERS_TABLE}
11495
+ SET ${LEASE_EXPIRES_AT} = ${DB_NOW_MS} + ?, ${UPDATED_AT} = ${DB_NOW_MS}
11496
+ WHERE ${RUN_ID} = ? AND generation = ? AND ${OWNER_ID} = ? AND ${LEASE_EXPIRES_AT} IS NOT NULL`, [
11497
+ leaseMs,
11498
+ fence.runId,
11499
+ fence.generation,
11500
+ fence.ownerId
11501
+ ]);
11502
+ const record = await readRunOwner(connection, fence.runId);
11503
+ return record && (0, _mastra_core_storage.matchesRunFence)(record, fence) && record.leaseExpiresAt !== null ? {
11504
+ renewed: true,
11505
+ record
11506
+ } : {
11507
+ renewed: false,
11508
+ record
11509
+ };
11510
+ });
11151
11511
  } catch (error) {
11512
+ throw this.#ownershipError("RENEW_RUN_OWNERSHIP", fence.runId, error);
11513
+ }
11514
+ }
11515
+ async releaseRunOwnership(fence) {
11516
+ try {
11517
+ return await inTransaction(this.pool, async (connection) => {
11518
+ await connection.execute(`UPDATE ${RUN_OWNERS_TABLE} SET ${LEASE_EXPIRES_AT} = NULL, ${UPDATED_AT} = ${DB_NOW_MS}
11519
+ WHERE ${RUN_ID} = ? AND generation = ? AND ${OWNER_ID} = ?`, [
11520
+ fence.runId,
11521
+ fence.generation,
11522
+ fence.ownerId
11523
+ ]);
11524
+ return (0, _mastra_core_storage.matchesRunFence)(await readRunOwner(connection, fence.runId), fence);
11525
+ });
11526
+ } catch (error) {
11527
+ throw this.#ownershipError("RELEASE_RUN_OWNERSHIP", fence.runId, error);
11528
+ }
11529
+ }
11530
+ async getRunOwnership({ runId }) {
11531
+ try {
11532
+ return await readRunOwner(this.pool, runId);
11533
+ } catch (error) {
11534
+ throw this.#ownershipError("GET_RUN_OWNERSHIP", runId, error);
11535
+ }
11536
+ }
11537
+ async deleteWorkflowRunById({ runId, workflowName, fence }) {
11538
+ try {
11539
+ await withRunFence(this.pool, RUN_OWNERS_TABLE, (0, _mastra_core_storage.resolveRunFence)(this, fence, runId), "deleteWorkflowRunById", (q) => q.execute(`DELETE FROM ${formatTableName(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT)} WHERE ${quoteIdentifier("workflow_name", "column name")} = ? AND ${quoteIdentifier("run_id", "column name")} = ?`, [workflowName, runId]));
11540
+ } catch (error) {
11541
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
11152
11542
  throw new _mastra_core_error.MastraError({
11153
11543
  id: "MYSQL_WORKFLOWS_DELETE_RUN_BY_ID_FAILED",
11154
11544
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -11160,10 +11550,12 @@ var WorkflowsMySQL = class WorkflowsMySQL extends _mastra_core_storage.Workflows
11160
11550
  }, error);
11161
11551
  }
11162
11552
  }
11163
- async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state }) {
11553
+ async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state, fence: explicitFence }) {
11554
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence, runId);
11164
11555
  const connection = await this.pool.getConnection();
11165
11556
  try {
11166
11557
  await connection.beginTransaction();
11558
+ if (fence) await assertRunFence(connection, RUN_OWNERS_TABLE, fence, "updateWorkflowResults");
11167
11559
  const [rows] = await connection.execute(`SELECT snapshot FROM ${formatTableName(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT)} WHERE ${quoteIdentifier("workflow_name", "column name")} = ? AND ${quoteIdentifier("run_id", "column name")} = ? FOR UPDATE`, [workflowName, runId]);
11168
11560
  if (!Array.isArray(rows) || rows.length === 0) {
11169
11561
  await connection.rollback();
@@ -11214,10 +11606,12 @@ var WorkflowsMySQL = class WorkflowsMySQL extends _mastra_core_storage.Workflows
11214
11606
  connection.release();
11215
11607
  }
11216
11608
  }
11217
- async updateWorkflowState({ workflowName, runId, opts }) {
11609
+ async updateWorkflowState({ workflowName, runId, opts, fence: explicitFence }) {
11610
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence, runId);
11218
11611
  const connection = await this.pool.getConnection();
11219
11612
  try {
11220
11613
  await connection.beginTransaction();
11614
+ if (fence) await assertRunFence(connection, RUN_OWNERS_TABLE, fence, "updateWorkflowState");
11221
11615
  const [rows] = await connection.execute(`SELECT snapshot FROM ${formatTableName(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT)} WHERE ${quoteIdentifier("workflow_name", "column name")} = ? AND ${quoteIdentifier("run_id", "column name")} = ? FOR UPDATE`, [workflowName, runId]);
11222
11616
  if (!Array.isArray(rows) || rows.length === 0) {
11223
11617
  await connection.rollback();
@@ -11243,6 +11637,7 @@ var WorkflowsMySQL = class WorkflowsMySQL extends _mastra_core_storage.Workflows
11243
11637
  return updatedSnapshot;
11244
11638
  } catch (error) {
11245
11639
  await connection.rollback();
11640
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
11246
11641
  throw new _mastra_core_error.MastraError({
11247
11642
  id: "MYSQL_WORKFLOWS_UPDATE_STATE_FAILED",
11248
11643
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -11256,35 +11651,38 @@ var WorkflowsMySQL = class WorkflowsMySQL extends _mastra_core_storage.Workflows
11256
11651
  connection.release();
11257
11652
  }
11258
11653
  }
11259
- async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt }) {
11654
+ async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt, fence }) {
11260
11655
  const now = /* @__PURE__ */ new Date();
11261
11656
  const createdAtValue = createdAt ?? now;
11262
11657
  const updatedAtValue = updatedAt ?? now;
11658
+ const tableName = formatTableName(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT);
11263
11659
  try {
11264
- const tableName = formatTableName(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT);
11265
- const [updateResult] = await this.pool.execute(`UPDATE ${tableName}
11266
- SET ${quoteIdentifier("resourceId", "column name")} = ?,
11267
- ${quoteIdentifier("snapshot", "column name")} = ?,
11268
- ${quoteIdentifier("updatedAt", "column name")} = ?
11269
- WHERE ${quoteIdentifier("workflow_name", "column name")} = ? AND ${quoteIdentifier("run_id", "column name")} = ?`, [
11270
- resourceId ?? null,
11271
- JSON.stringify(snapshot),
11272
- transformToSqlValue(updatedAtValue),
11273
- workflowName,
11274
- runId
11275
- ]);
11276
- if (updateResult.affectedRows > 0) return;
11277
- await this.pool.execute(`INSERT INTO ${tableName} (${quoteIdentifier("workflow_name", "column name")}, ${quoteIdentifier("run_id", "column name")}, ${quoteIdentifier("resourceId", "column name")}, ${quoteIdentifier("snapshot", "column name")}, ${quoteIdentifier("createdAt", "column name")}, ${quoteIdentifier("updatedAt", "column name")})
11278
- VALUES (?, ?, ?, ?, ?, ?)
11279
- ON DUPLICATE KEY UPDATE ${quoteIdentifier("resourceId", "column name")} = VALUES(${quoteIdentifier("resourceId", "column name")}), ${quoteIdentifier("snapshot", "column name")} = VALUES(${quoteIdentifier("snapshot", "column name")}), ${quoteIdentifier("updatedAt", "column name")} = VALUES(${quoteIdentifier("updatedAt", "column name")})`, [
11280
- workflowName,
11281
- runId,
11282
- resourceId ?? null,
11283
- JSON.stringify(snapshot),
11284
- transformToSqlValue(createdAtValue),
11285
- transformToSqlValue(updatedAtValue)
11286
- ]);
11660
+ await withRunFence(this.pool, RUN_OWNERS_TABLE, (0, _mastra_core_storage.resolveRunFence)(this, fence, runId), "persistWorkflowSnapshot", async (q) => {
11661
+ const [updateResult] = await q.execute(`UPDATE ${tableName}
11662
+ SET ${quoteIdentifier("resourceId", "column name")} = ?,
11663
+ ${quoteIdentifier("snapshot", "column name")} = ?,
11664
+ ${quoteIdentifier("updatedAt", "column name")} = ?
11665
+ WHERE ${quoteIdentifier("workflow_name", "column name")} = ? AND ${quoteIdentifier("run_id", "column name")} = ?`, [
11666
+ resourceId ?? null,
11667
+ JSON.stringify(snapshot),
11668
+ transformToSqlValue(updatedAtValue),
11669
+ workflowName,
11670
+ runId
11671
+ ]);
11672
+ if (updateResult.affectedRows > 0) return;
11673
+ await q.execute(`INSERT INTO ${tableName} (${quoteIdentifier("workflow_name", "column name")}, ${quoteIdentifier("run_id", "column name")}, ${quoteIdentifier("resourceId", "column name")}, ${quoteIdentifier("snapshot", "column name")}, ${quoteIdentifier("createdAt", "column name")}, ${quoteIdentifier("updatedAt", "column name")})
11674
+ VALUES (?, ?, ?, ?, ?, ?)
11675
+ ON DUPLICATE KEY UPDATE ${quoteIdentifier("resourceId", "column name")} = VALUES(${quoteIdentifier("resourceId", "column name")}), ${quoteIdentifier("snapshot", "column name")} = VALUES(${quoteIdentifier("snapshot", "column name")}), ${quoteIdentifier("updatedAt", "column name")} = VALUES(${quoteIdentifier("updatedAt", "column name")})`, [
11676
+ workflowName,
11677
+ runId,
11678
+ resourceId ?? null,
11679
+ JSON.stringify(snapshot),
11680
+ transformToSqlValue(createdAtValue),
11681
+ transformToSqlValue(updatedAtValue)
11682
+ ]);
11683
+ });
11287
11684
  } catch (error) {
11685
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
11288
11686
  throw new _mastra_core_error.MastraError({
11289
11687
  id: "MYSQL_WORKFLOWS_PERSIST_SNAPSHOT_FAILED",
11290
11688
  domain: _mastra_core_error.ErrorDomain.STORAGE,