@mastra/mysql 0.13.1-alpha.0 → 0.13.1-alpha.1

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