@mastra/mssql 1.10.2-alpha.0 → 1.10.3-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2127,6 +2127,73 @@ var BackgroundTasksMSSQL = class BackgroundTasksMSSQL extends _mastra_core_stora
2127
2127
  }
2128
2128
  };
2129
2129
  //#endregion
2130
+ //#region src/storage/db/run-fencing.ts
2131
+ /**
2132
+ * Epoch milliseconds on the database clock. SQL Server evaluates
2133
+ * `SYSUTCDATETIME()` once when a statement starts, so a statement that waits
2134
+ * on a lock compares against the time it started. That error only makes an
2135
+ * existing lease look longer and a new lease shorter.
2136
+ */
2137
+ const DB_NOW_MS = `DATEDIFF_BIG(MILLISECOND, '1970-01-01', SYSUTCDATETIME())`;
2138
+ const ERR_DEADLOCK_VICTIM = 1205;
2139
+ const ERR_UNIQUE_INDEX = 2601;
2140
+ const ERR_PRIMARY_KEY = 2627;
2141
+ const MAX_CLAIM_ATTEMPTS = 5;
2142
+ /**
2143
+ * The check half of a fenced write; call it first inside the write's
2144
+ * transaction. `HOLDLOCK` keeps the shared lock on the claim row until commit,
2145
+ * even on databases with read-committed snapshot on, so a claim that updates
2146
+ * the row waits for the write to finish, and every later write sees the new
2147
+ * claim.
2148
+ */
2149
+ async function assertRunFence(q, claimsTable, fence, operation) {
2150
+ const row = (await q.request().input("runId", fence.runId).query(`SELECT [generation], [ownerId] FROM ${claimsTable} WITH (HOLDLOCK) WHERE [runId] = @runId`)).recordset[0];
2151
+ if (!(0, _mastra_core_storage.matchesRunFence)(row ? {
2152
+ generation: Number(row.generation),
2153
+ ownerId: String(row.ownerId)
2154
+ } : null, fence)) throw new _mastra_core_storage.RunFenceConflictError(fence, operation);
2155
+ }
2156
+ /** Runs `fn` in a transaction. */
2157
+ async function inTransaction(pool, fn) {
2158
+ const transaction = pool.transaction();
2159
+ await transaction.begin();
2160
+ try {
2161
+ const result = await fn(transaction);
2162
+ await transaction.commit();
2163
+ return result;
2164
+ } catch (error) {
2165
+ await transaction.rollback().catch(() => {});
2166
+ throw error;
2167
+ }
2168
+ }
2169
+ /**
2170
+ * Runs `write` directly when it carries no fence, or in a transaction behind
2171
+ * the fence check when it does.
2172
+ */
2173
+ function withRunFence(pool, claimsTable, fence, operation, write) {
2174
+ if (!fence) return write(pool);
2175
+ return inTransaction(pool, async (transaction) => {
2176
+ await assertRunFence(transaction, claimsTable, fence, operation);
2177
+ return write(transaction);
2178
+ });
2179
+ }
2180
+ /**
2181
+ * Runs a transaction that creates or updates a run's claim row, retrying when
2182
+ * two first claims race to insert it. Claims read the row with
2183
+ * `UPDLOCK, HOLDLOCK`, which locks the key range of a missing row, so racing
2184
+ * first claims normally queue; the retry covers a deadlock victim or a
2185
+ * duplicate key if they don't. On retry the row exists and the claim
2186
+ * serializes behind its row lock.
2187
+ */
2188
+ async function claimTransaction(pool, fn) {
2189
+ for (let attempt = 1;; attempt++) try {
2190
+ return await inTransaction(pool, fn);
2191
+ } catch (error) {
2192
+ const number = error.number;
2193
+ if (attempt >= MAX_CLAIM_ATTEMPTS || !(number === ERR_DEADLOCK_VICTIM || number === ERR_PRIMARY_KEY || number === ERR_UNIQUE_INDEX)) throw error;
2194
+ }
2195
+ }
2196
+ //#endregion
2130
2197
  //#region src/storage/domains/memory/index.ts
2131
2198
  function bindMssqlMetadataParams(request, params) {
2132
2199
  for (const [paramName, paramValue] of Object.entries(params)) request.input(paramName, paramValue);
@@ -2216,9 +2283,69 @@ var MemoryMSSQL = class MemoryMSSQL extends _mastra_core_storage.MemoryStorage {
2216
2283
  tableName: _mastra_core_storage.TABLE_RESOURCES,
2217
2284
  schema: _mastra_core_storage.TABLE_SCHEMAS[_mastra_core_storage.TABLE_RESOURCES]
2218
2285
  });
2286
+ await this.db.createTable({
2287
+ tableName: _mastra_core_storage.TABLE_MEMORY_RUN_FENCES,
2288
+ schema: _mastra_core_storage.RUN_FENCING_TABLE_SCHEMAS[_mastra_core_storage.TABLE_MEMORY_RUN_FENCES]
2289
+ });
2219
2290
  await this.createDefaultIndexes();
2220
2291
  await this.createCustomIndexes();
2221
2292
  }
2293
+ supportsRunFencing() {
2294
+ return true;
2295
+ }
2296
+ get #runFencesTable() {
2297
+ return getTableName({
2298
+ indexName: _mastra_core_storage.TABLE_MEMORY_RUN_FENCES,
2299
+ schemaName: getSchemaName(this.schema)
2300
+ });
2301
+ }
2302
+ async raiseRunFence(fence) {
2303
+ const table = this.#runFencesTable;
2304
+ try {
2305
+ return await claimTransaction(this.pool, async (transaction) => {
2306
+ const row = (await transaction.request().input("runId", fence.runId).query(`SELECT [generation], [ownerId] FROM ${table} WITH (UPDLOCK, HOLDLOCK) WHERE [runId] = @runId`)).recordset[0];
2307
+ if (!row) {
2308
+ await transaction.request().input("runId", fence.runId).input("generation", mssql.default.BigInt, fence.generation).input("ownerId", fence.ownerId).query(`INSERT INTO ${table} ([runId], [generation], [ownerId]) VALUES (@runId, @generation, @ownerId)`);
2309
+ return true;
2310
+ }
2311
+ const generation = Number(row.generation);
2312
+ const raises = generation < fence.generation;
2313
+ const reaffirms = generation === fence.generation && String(row.ownerId) === fence.ownerId;
2314
+ if (!raises && !reaffirms) return false;
2315
+ await transaction.request().input("runId", fence.runId).input("generation", mssql.default.BigInt, fence.generation).input("ownerId", fence.ownerId).query(`UPDATE ${table} SET [generation] = @generation, [ownerId] = @ownerId, [retiredAt] = NULL WHERE [runId] = @runId`);
2316
+ return true;
2317
+ });
2318
+ } catch (error) {
2319
+ throw new _mastra_core_error.MastraError({
2320
+ id: (0, _mastra_core_storage.createStorageErrorId)("MSSQL", "RAISE_RUN_FENCE", "FAILED"),
2321
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
2322
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
2323
+ details: { runId: fence.runId }
2324
+ }, error);
2325
+ }
2326
+ }
2327
+ async retireRunFence(fence) {
2328
+ try {
2329
+ return (await this.pool.request().input("runId", fence.runId).input("generation", mssql.default.BigInt, fence.generation).input("ownerId", fence.ownerId).query(`UPDATE ${this.#runFencesTable} SET [retiredAt] = ${DB_NOW_MS}
2330
+ WHERE [runId] = @runId AND [generation] = @generation AND [ownerId] = @ownerId`)).rowsAffected[0] === 1;
2331
+ } catch (error) {
2332
+ throw new _mastra_core_error.MastraError({
2333
+ id: (0, _mastra_core_storage.createStorageErrorId)("MSSQL", "RETIRE_RUN_FENCE", "FAILED"),
2334
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
2335
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
2336
+ details: { runId: fence.runId }
2337
+ }, error);
2338
+ }
2339
+ }
2340
+ /** Runs `write` behind its own fence, otherwise the one in scope for this store. */
2341
+ #fenced(fence, operation, write) {
2342
+ return withRunFence(this.pool, this.#runFencesTable, (0, _mastra_core_storage.resolveRunFence)(this, fence), operation, write);
2343
+ }
2344
+ /** The fence check for a write that already runs in its own transaction. */
2345
+ async #assertFence(transaction, fence, operation) {
2346
+ const resolved = (0, _mastra_core_storage.resolveRunFence)(this, fence);
2347
+ if (resolved) await assertRunFence(transaction, this.#runFencesTable, resolved, operation);
2348
+ }
2222
2349
  /**
2223
2350
  * Returns default index definitions for the memory domain tables.
2224
2351
  * IMPORTANT: Uses seq_id DESC instead of createdAt DESC for MSSQL due to millisecond accuracy limitations
@@ -2261,6 +2388,7 @@ var MemoryMSSQL = class MemoryMSSQL extends _mastra_core_storage.MemoryStorage {
2261
2388
  await this.db.clearTable({ tableName: _mastra_core_storage.TABLE_MESSAGES });
2262
2389
  await this.db.clearTable({ tableName: _mastra_core_storage.TABLE_THREADS });
2263
2390
  await this.db.clearTable({ tableName: _mastra_core_storage.TABLE_RESOURCES });
2391
+ await this.db.clearTable({ tableName: _mastra_core_storage.TABLE_MEMORY_RUN_FENCES });
2264
2392
  }
2265
2393
  async getThreadById({ threadId, resourceId }) {
2266
2394
  try {
@@ -2411,7 +2539,7 @@ var MemoryMSSQL = class MemoryMSSQL extends _mastra_core_storage.MemoryStorage {
2411
2539
  throw mastraError;
2412
2540
  }
2413
2541
  }
2414
- async saveThread({ thread }) {
2542
+ async saveThread({ thread, fence }) {
2415
2543
  try {
2416
2544
  const mergeSql = `MERGE INTO ${getTableName({
2417
2545
  indexName: _mastra_core_storage.TABLE_THREADS,
@@ -2428,18 +2556,21 @@ var MemoryMSSQL = class MemoryMSSQL extends _mastra_core_storage.MemoryStorage {
2428
2556
  WHEN NOT MATCHED THEN
2429
2557
  INSERT (id, [resourceId], title, metadata, [createdAt], [updatedAt])
2430
2558
  VALUES (@id, @resourceId, @title, @metadata, @createdAt, @updatedAt);`;
2431
- const req = this.pool.request();
2432
- req.input("id", thread.id);
2433
- req.input("resourceId", thread.resourceId);
2434
- req.input("title", thread.title);
2435
2559
  const metadata = thread.metadata ? JSON.stringify(thread.metadata) : null;
2436
- if (metadata === null) req.input("metadata", mssql.default.NVarChar, null);
2437
- else req.input("metadata", metadata);
2438
- req.input("createdAt", mssql.default.DateTime2, thread.createdAt);
2439
- req.input("updatedAt", mssql.default.DateTime2, thread.updatedAt);
2440
- await req.query(mergeSql);
2560
+ await this.#fenced(fence, "saveThread", (q) => {
2561
+ const req = q.request();
2562
+ req.input("id", thread.id);
2563
+ req.input("resourceId", thread.resourceId);
2564
+ req.input("title", thread.title);
2565
+ if (metadata === null) req.input("metadata", mssql.default.NVarChar, null);
2566
+ else req.input("metadata", metadata);
2567
+ req.input("createdAt", mssql.default.DateTime2, thread.createdAt);
2568
+ req.input("updatedAt", mssql.default.DateTime2, thread.updatedAt);
2569
+ return req.query(mergeSql);
2570
+ });
2441
2571
  return thread;
2442
2572
  } catch (error) {
2573
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
2443
2574
  throw new _mastra_core_error.MastraError({
2444
2575
  id: (0, _mastra_core_storage.createStorageErrorId)("MSSQL", "SAVE_THREAD", "FAILED"),
2445
2576
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -2451,7 +2582,7 @@ var MemoryMSSQL = class MemoryMSSQL extends _mastra_core_storage.MemoryStorage {
2451
2582
  /**
2452
2583
  * Updates a thread's title and metadata, merging with existing metadata. Returns the updated thread.
2453
2584
  */
2454
- async updateThread({ id, title, metadata }) {
2585
+ async updateThread({ id, title, metadata, fence }) {
2455
2586
  const existingThread = await this.getThreadById({ threadId: id });
2456
2587
  if (!existingThread) throw new _mastra_core_error.MastraError({
2457
2588
  id: (0, _mastra_core_storage.createStorageErrorId)("MSSQL", "UPDATE_THREAD", "NOT_FOUND"),
@@ -2477,12 +2608,7 @@ var MemoryMSSQL = class MemoryMSSQL extends _mastra_core_storage.MemoryStorage {
2477
2608
  [updatedAt] = @updatedAt
2478
2609
  OUTPUT INSERTED.*
2479
2610
  WHERE id = @id`;
2480
- const req = this.pool.request();
2481
- req.input("id", id);
2482
- req.input("title", title ?? existingThread.title);
2483
- req.input("metadata", JSON.stringify(mergedMetadata));
2484
- req.input("updatedAt", /* @__PURE__ */ new Date());
2485
- const result = await req.query(sql);
2611
+ const result = await this.#fenced(fence, "updateThread", (q) => q.request().input("id", id).input("title", title ?? existingThread.title).input("metadata", JSON.stringify(mergedMetadata)).input("updatedAt", /* @__PURE__ */ new Date()).query(sql));
2486
2612
  let thread = result.recordset && result.recordset[0];
2487
2613
  if (thread && "seq_id" in thread) {
2488
2614
  const { seq_id, ...rest } = thread;
@@ -2505,6 +2631,7 @@ var MemoryMSSQL = class MemoryMSSQL extends _mastra_core_storage.MemoryStorage {
2505
2631
  updatedAt: thread.updatedAt
2506
2632
  };
2507
2633
  } catch (error) {
2634
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
2508
2635
  throw new _mastra_core_error.MastraError({
2509
2636
  id: (0, _mastra_core_storage.createStorageErrorId)("MSSQL", "UPDATE_THREAD", "FAILED"),
2510
2637
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -2870,7 +2997,7 @@ var MemoryMSSQL = class MemoryMSSQL extends _mastra_core_storage.MemoryStorage {
2870
2997
  throw mastraError;
2871
2998
  }
2872
2999
  }
2873
- async saveMessages({ messages }) {
3000
+ async saveMessages({ messages, fence }) {
2874
3001
  if (messages.length === 0) return { messages: [] };
2875
3002
  const threadId = messages[0]?.threadId;
2876
3003
  if (!threadId) throw new _mastra_core_error.MastraError({
@@ -2898,6 +3025,7 @@ var MemoryMSSQL = class MemoryMSSQL extends _mastra_core_storage.MemoryStorage {
2898
3025
  const transaction = this.pool.transaction();
2899
3026
  await transaction.begin();
2900
3027
  try {
3028
+ await this.#assertFence(transaction, fence, "saveMessages");
2901
3029
  for (const message of messages) {
2902
3030
  if (!message.threadId) throw new Error(`Expected to find a threadId for message, but couldn't find one. An unexpected error has occurred.`);
2903
3031
  if (!message.resourceId) throw new Error(`Expected to find a resourceId for message, but couldn't find one. An unexpected error has occurred.`);
@@ -2945,6 +3073,7 @@ var MemoryMSSQL = class MemoryMSSQL extends _mastra_core_storage.MemoryStorage {
2945
3073
  });
2946
3074
  return { messages: new _mastra_core_agent.MessageList().add(messagesWithParsedContent, "memory").get.all.db() };
2947
3075
  } catch (error) {
3076
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
2948
3077
  throw new _mastra_core_error.MastraError({
2949
3078
  id: (0, _mastra_core_storage.createStorageErrorId)("MSSQL", "SAVE_MESSAGES", "FAILED"),
2950
3079
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -2953,7 +3082,7 @@ var MemoryMSSQL = class MemoryMSSQL extends _mastra_core_storage.MemoryStorage {
2953
3082
  }, error);
2954
3083
  }
2955
3084
  }
2956
- async updateMessages({ messages }) {
3085
+ async updateMessages({ messages, fence }) {
2957
3086
  if (!messages || messages.length === 0) return [];
2958
3087
  const messageIds = messages.map((m) => m.id);
2959
3088
  const idParams = messageIds.map((_, i) => `@id${i}`).join(", ");
@@ -2977,6 +3106,7 @@ var MemoryMSSQL = class MemoryMSSQL extends _mastra_core_storage.MemoryStorage {
2977
3106
  const transaction = this.pool.transaction();
2978
3107
  try {
2979
3108
  await transaction.begin();
3109
+ await this.#assertFence(transaction, fence, "updateMessages");
2980
3110
  for (const existingMessage of existingMessages) {
2981
3111
  const updatePayload = messages.find((m) => m.id === existingMessage.id);
2982
3112
  if (!updatePayload) continue;
@@ -3029,6 +3159,7 @@ var MemoryMSSQL = class MemoryMSSQL extends _mastra_core_storage.MemoryStorage {
3029
3159
  await transaction.commit();
3030
3160
  } catch (error) {
3031
3161
  await transaction.rollback();
3162
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
3032
3163
  throw new _mastra_core_error.MastraError({
3033
3164
  id: (0, _mastra_core_storage.createStorageErrorId)("MSSQL", "UPDATE_MESSAGES", "FAILED"),
3034
3165
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -3044,7 +3175,7 @@ var MemoryMSSQL = class MemoryMSSQL extends _mastra_core_storage.MemoryStorage {
3044
3175
  return message;
3045
3176
  });
3046
3177
  }
3047
- async deleteMessages(messageIds) {
3178
+ async deleteMessages(messageIds, options) {
3048
3179
  if (!messageIds || messageIds.length === 0) return;
3049
3180
  try {
3050
3181
  const messageTableName = getTableName({
@@ -3064,6 +3195,7 @@ var MemoryMSSQL = class MemoryMSSQL extends _mastra_core_storage.MemoryStorage {
3064
3195
  const transaction = this.pool.transaction();
3065
3196
  await transaction.begin();
3066
3197
  try {
3198
+ await this.#assertFence(transaction, options?.fence, "deleteMessages");
3067
3199
  const deleteRequest = transaction.request();
3068
3200
  messageIds.forEach((id, idx) => {
3069
3201
  deleteRequest.input(`p${idx + 1}`, id);
@@ -3082,6 +3214,7 @@ var MemoryMSSQL = class MemoryMSSQL extends _mastra_core_storage.MemoryStorage {
3082
3214
  throw error;
3083
3215
  }
3084
3216
  } catch (error) {
3217
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
3085
3218
  throw new _mastra_core_error.MastraError({
3086
3219
  id: (0, _mastra_core_storage.createStorageErrorId)("MSSQL", "DELETE_MESSAGES", "FAILED"),
3087
3220
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -3129,7 +3262,7 @@ var MemoryMSSQL = class MemoryMSSQL extends _mastra_core_storage.MemoryStorage {
3129
3262
  });
3130
3263
  return resource;
3131
3264
  }
3132
- async updateResource({ resourceId, workingMemory, metadata }) {
3265
+ async updateResource({ resourceId, workingMemory, metadata, fence }) {
3133
3266
  try {
3134
3267
  const existingResource = await this.getResourceById({ resourceId });
3135
3268
  if (!existingResource) {
@@ -3140,7 +3273,12 @@ var MemoryMSSQL = class MemoryMSSQL extends _mastra_core_storage.MemoryStorage {
3140
3273
  createdAt: /* @__PURE__ */ new Date(),
3141
3274
  updatedAt: /* @__PURE__ */ new Date()
3142
3275
  };
3143
- return this.saveResource({ resource: newResource });
3276
+ await this.#fenced(fence, "updateResource", (q) => this.db.insert({
3277
+ tableName: _mastra_core_storage.TABLE_RESOURCES,
3278
+ record: newResource,
3279
+ transaction: q
3280
+ }));
3281
+ return newResource;
3144
3282
  }
3145
3283
  const updatedResource = {
3146
3284
  ...existingResource,
@@ -3155,22 +3293,25 @@ var MemoryMSSQL = class MemoryMSSQL extends _mastra_core_storage.MemoryStorage {
3155
3293
  indexName: _mastra_core_storage.TABLE_RESOURCES,
3156
3294
  schemaName: getSchemaName(this.schema)
3157
3295
  });
3158
- const updates = [];
3159
- const req = this.pool.request();
3160
- if (workingMemory !== void 0) {
3161
- updates.push("workingMemory = @workingMemory");
3162
- req.input("workingMemory", workingMemory);
3163
- }
3164
- if (metadata) {
3165
- updates.push("metadata = @metadata");
3166
- req.input("metadata", JSON.stringify(updatedResource.metadata));
3167
- }
3168
- updates.push("updatedAt = @updatedAt");
3169
- req.input("updatedAt", updatedResource.updatedAt.toISOString());
3170
- req.input("id", resourceId);
3171
- await req.query(`UPDATE ${tableName} SET ${updates.join(", ")} WHERE id = @id`);
3296
+ await this.#fenced(fence, "updateResource", (q) => {
3297
+ const updates = [];
3298
+ const req = q.request();
3299
+ if (workingMemory !== void 0) {
3300
+ updates.push("workingMemory = @workingMemory");
3301
+ req.input("workingMemory", workingMemory);
3302
+ }
3303
+ if (metadata) {
3304
+ updates.push("metadata = @metadata");
3305
+ req.input("metadata", JSON.stringify(updatedResource.metadata));
3306
+ }
3307
+ updates.push("updatedAt = @updatedAt");
3308
+ req.input("updatedAt", updatedResource.updatedAt.toISOString());
3309
+ req.input("id", resourceId);
3310
+ return req.query(`UPDATE ${tableName} SET ${updates.join(", ")} WHERE id = @id`);
3311
+ });
3172
3312
  return updatedResource;
3173
3313
  } catch (error) {
3314
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
3174
3315
  const mastraError = new _mastra_core_error.MastraError({
3175
3316
  id: (0, _mastra_core_storage.createStorageErrorId)("MSSQL", "UPDATE_RESOURCE", "FAILED"),
3176
3317
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -4447,6 +4588,17 @@ var WorkflowDefinitionsMSSQL = class WorkflowDefinitionsMSSQL extends _mastra_co
4447
4588
  };
4448
4589
  //#endregion
4449
4590
  //#region src/storage/domains/workflows/index.ts
4591
+ const RUN_OWNER_COLUMNS = `[generation], [ownerId], [leaseExpiresAt], ${DB_NOW_MS} AS [nowMs]`;
4592
+ function toRunOwnershipRecord(runId, row) {
4593
+ const leaseExpiresAt = row.leaseExpiresAt === null ? null : Number(row.leaseExpiresAt);
4594
+ return {
4595
+ runId,
4596
+ generation: Number(row.generation),
4597
+ ownerId: String(row.ownerId),
4598
+ leaseExpiresAt: leaseExpiresAt === null ? null : new Date(leaseExpiresAt),
4599
+ live: leaseExpiresAt !== null && leaseExpiresAt > Number(row.nowMs)
4600
+ };
4601
+ }
4450
4602
  var WorkflowsMSSQL = class WorkflowsMSSQL extends _mastra_core_storage.WorkflowsStorage {
4451
4603
  pool;
4452
4604
  db;
@@ -4496,6 +4648,10 @@ var WorkflowsMSSQL = class WorkflowsMSSQL extends _mastra_core_storage.Workflows
4496
4648
  tableName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
4497
4649
  schema: _mastra_core_storage.TABLE_SCHEMAS[_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT]
4498
4650
  });
4651
+ await this.db.createTable({
4652
+ tableName: _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS,
4653
+ schema: _mastra_core_storage.RUN_FENCING_TABLE_SCHEMAS[_mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS]
4654
+ });
4499
4655
  await this.createDefaultIndexes();
4500
4656
  await this.createCustomIndexes();
4501
4657
  }
@@ -4512,6 +4668,98 @@ var WorkflowsMSSQL = class WorkflowsMSSQL extends _mastra_core_storage.Workflows
4512
4668
  }
4513
4669
  async dangerouslyClearAll() {
4514
4670
  await this.db.clearTable({ tableName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT });
4671
+ await this.db.clearTable({ tableName: _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS });
4672
+ }
4673
+ get #runOwnersTable() {
4674
+ return getTableName({
4675
+ indexName: _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS,
4676
+ schemaName: getSchemaName(this.schema)
4677
+ });
4678
+ }
4679
+ async #readRunOwner(q, runId) {
4680
+ const row = (await q.request().input("runId", runId).query(`SELECT ${RUN_OWNER_COLUMNS} FROM ${this.#runOwnersTable} WHERE [runId] = @runId`)).recordset[0];
4681
+ return row ? toRunOwnershipRecord(runId, row) : null;
4682
+ }
4683
+ supportsRunFencing() {
4684
+ return true;
4685
+ }
4686
+ #ownershipError(operation, runId, error) {
4687
+ return new _mastra_core_error.MastraError({
4688
+ id: (0, _mastra_core_storage.createStorageErrorId)("MSSQL", operation, "FAILED"),
4689
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
4690
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
4691
+ details: { runId }
4692
+ }, error);
4693
+ }
4694
+ async claimRunOwnership({ runId, ownerId, leaseMs, force, expectedGeneration }) {
4695
+ const table = this.#runOwnersTable;
4696
+ try {
4697
+ return await claimTransaction(this.pool, async (transaction) => {
4698
+ if ((await transaction.request().input("runId", runId).query(`SELECT [generation] FROM ${table} WITH (UPDLOCK, HOLDLOCK) WHERE [runId] = @runId`)).recordset.length === 0) {
4699
+ if (expectedGeneration !== void 0 && expectedGeneration !== 0) return {
4700
+ acquired: false,
4701
+ record: null
4702
+ };
4703
+ await transaction.request().input("runId", runId).input("ownerId", ownerId).input("leaseMs", mssql.default.BigInt, leaseMs).query(`INSERT INTO ${table} ([runId], [generation], [ownerId], [leaseExpiresAt], [updatedAt])
4704
+ VALUES (@runId, ${DB_NOW_MS}, @ownerId, ${DB_NOW_MS} + @leaseMs, ${DB_NOW_MS})`);
4705
+ return {
4706
+ acquired: true,
4707
+ record: await this.#readRunOwner(transaction, runId)
4708
+ };
4709
+ }
4710
+ const updated = await transaction.request().input("runId", runId).input("ownerId", ownerId).input("leaseMs", mssql.default.BigInt, leaseMs).input("expectedGeneration", mssql.default.BigInt, expectedGeneration ?? null).input("force", mssql.default.Bit, force === true).query(`UPDATE ${table}
4711
+ SET [generation] = [generation] + 1, [ownerId] = @ownerId, [leaseExpiresAt] = ${DB_NOW_MS} + @leaseMs,
4712
+ [updatedAt] = ${DB_NOW_MS}
4713
+ WHERE [runId] = @runId
4714
+ AND (@expectedGeneration IS NULL OR [generation] = @expectedGeneration)
4715
+ AND (@force = 1 OR [leaseExpiresAt] IS NULL OR [leaseExpiresAt] <= ${DB_NOW_MS})`);
4716
+ const record = await this.#readRunOwner(transaction, runId);
4717
+ return updated.rowsAffected[0] === 1 && record ? {
4718
+ acquired: true,
4719
+ record
4720
+ } : {
4721
+ acquired: false,
4722
+ record
4723
+ };
4724
+ });
4725
+ } catch (error) {
4726
+ throw this.#ownershipError("CLAIM_RUN_OWNERSHIP", runId, error);
4727
+ }
4728
+ }
4729
+ async renewRunOwnership({ leaseMs, ...fence }) {
4730
+ try {
4731
+ return await inTransaction(this.pool, async (transaction) => {
4732
+ const updated = await transaction.request().input("runId", fence.runId).input("generation", mssql.default.BigInt, fence.generation).input("ownerId", fence.ownerId).input("leaseMs", mssql.default.BigInt, leaseMs).query(`UPDATE ${this.#runOwnersTable}
4733
+ SET [leaseExpiresAt] = ${DB_NOW_MS} + @leaseMs, [updatedAt] = ${DB_NOW_MS}
4734
+ WHERE [runId] = @runId AND [generation] = @generation AND [ownerId] = @ownerId
4735
+ AND [leaseExpiresAt] IS NOT NULL`);
4736
+ const record = await this.#readRunOwner(transaction, fence.runId);
4737
+ return updated.rowsAffected[0] === 1 && record ? {
4738
+ renewed: true,
4739
+ record
4740
+ } : {
4741
+ renewed: false,
4742
+ record
4743
+ };
4744
+ });
4745
+ } catch (error) {
4746
+ throw this.#ownershipError("RENEW_RUN_OWNERSHIP", fence.runId, error);
4747
+ }
4748
+ }
4749
+ async releaseRunOwnership(fence) {
4750
+ try {
4751
+ return (await this.pool.request().input("runId", fence.runId).input("generation", mssql.default.BigInt, fence.generation).input("ownerId", fence.ownerId).query(`UPDATE ${this.#runOwnersTable} SET [leaseExpiresAt] = NULL, [updatedAt] = ${DB_NOW_MS}
4752
+ WHERE [runId] = @runId AND [generation] = @generation AND [ownerId] = @ownerId`)).rowsAffected[0] === 1;
4753
+ } catch (error) {
4754
+ throw this.#ownershipError("RELEASE_RUN_OWNERSHIP", fence.runId, error);
4755
+ }
4756
+ }
4757
+ async getRunOwnership({ runId }) {
4758
+ try {
4759
+ return await this.#readRunOwner(this.pool, runId);
4760
+ } catch (error) {
4761
+ throw this.#ownershipError("GET_RUN_OWNERSHIP", runId, error);
4762
+ }
4515
4763
  }
4516
4764
  parseWorkflowRun(row) {
4517
4765
  let parsedSnapshot = row.snapshot;
@@ -4529,14 +4777,16 @@ var WorkflowsMSSQL = class WorkflowsMSSQL extends _mastra_core_storage.Workflows
4529
4777
  resourceId: row.resourceId
4530
4778
  };
4531
4779
  }
4532
- async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state }) {
4780
+ async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state, fence: explicitFence }) {
4533
4781
  const table = getTableName({
4534
4782
  indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
4535
4783
  schemaName: getSchemaName(this.schema)
4536
4784
  });
4785
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence, runId);
4537
4786
  const transaction = this.pool.transaction();
4538
4787
  try {
4539
4788
  await transaction.begin();
4789
+ if (fence) await assertRunFence(transaction, this.#runOwnersTable, fence, "updateWorkflowResults");
4540
4790
  const selectRequest = new mssql.default.Request(transaction);
4541
4791
  selectRequest.input("workflow_name", workflowName);
4542
4792
  selectRequest.input("run_id", runId);
@@ -4584,6 +4834,7 @@ var WorkflowsMSSQL = class WorkflowsMSSQL extends _mastra_core_storage.Workflows
4584
4834
  try {
4585
4835
  await transaction.rollback();
4586
4836
  } catch {}
4837
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
4587
4838
  throw new _mastra_core_error.MastraError({
4588
4839
  id: (0, _mastra_core_storage.createStorageErrorId)("MSSQL", "UPDATE_WORKFLOW_RESULTS", "FAILED"),
4589
4840
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -4596,14 +4847,16 @@ var WorkflowsMSSQL = class WorkflowsMSSQL extends _mastra_core_storage.Workflows
4596
4847
  }, error);
4597
4848
  }
4598
4849
  }
4599
- async updateWorkflowState({ workflowName, runId, opts }) {
4850
+ async updateWorkflowState({ workflowName, runId, opts, fence: explicitFence }) {
4600
4851
  const table = getTableName({
4601
4852
  indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
4602
4853
  schemaName: getSchemaName(this.schema)
4603
4854
  });
4855
+ const fence = (0, _mastra_core_storage.resolveRunFence)(this, explicitFence, runId);
4604
4856
  const transaction = this.pool.transaction();
4605
4857
  try {
4606
4858
  await transaction.begin();
4859
+ if (fence) await assertRunFence(transaction, this.#runOwnersTable, fence, "updateWorkflowState");
4607
4860
  const selectRequest = new mssql.default.Request(transaction);
4608
4861
  selectRequest.input("workflow_name", workflowName);
4609
4862
  selectRequest.input("run_id", runId);
@@ -4659,31 +4912,34 @@ var WorkflowsMSSQL = class WorkflowsMSSQL extends _mastra_core_storage.Workflows
4659
4912
  }, error);
4660
4913
  }
4661
4914
  }
4662
- async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot }) {
4915
+ async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, fence }) {
4663
4916
  const table = getTableName({
4664
4917
  indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
4665
4918
  schemaName: getSchemaName(this.schema)
4666
4919
  });
4667
4920
  const now = (/* @__PURE__ */ new Date()).toISOString();
4668
4921
  try {
4669
- const request = this.pool.request();
4670
- request.input("workflow_name", workflowName);
4671
- request.input("run_id", runId);
4672
- request.input("resourceId", resourceId);
4673
- request.input("snapshot", JSON.stringify(snapshot));
4674
- request.input("createdAt", mssql.default.DateTime2, new Date(now));
4675
- request.input("updatedAt", mssql.default.DateTime2, new Date(now));
4676
- const mergeSql = `MERGE INTO ${table} AS target
4677
- USING (SELECT @workflow_name AS workflow_name, @run_id AS run_id) AS src
4678
- ON target.workflow_name = src.workflow_name AND target.run_id = src.run_id
4679
- WHEN MATCHED THEN UPDATE SET
4680
- resourceId = @resourceId,
4681
- snapshot = @snapshot,
4682
- [updatedAt] = @updatedAt
4683
- WHEN NOT MATCHED THEN INSERT (workflow_name, run_id, resourceId, snapshot, [createdAt], [updatedAt])
4684
- VALUES (@workflow_name, @run_id, @resourceId, @snapshot, @createdAt, @updatedAt);`;
4685
- await request.query(mergeSql);
4922
+ await withRunFence(this.pool, this.#runOwnersTable, (0, _mastra_core_storage.resolveRunFence)(this, fence, runId), "persistWorkflowSnapshot", async (q) => {
4923
+ const request = q.request();
4924
+ request.input("workflow_name", workflowName);
4925
+ request.input("run_id", runId);
4926
+ request.input("resourceId", resourceId);
4927
+ request.input("snapshot", JSON.stringify(snapshot));
4928
+ request.input("createdAt", mssql.default.DateTime2, new Date(now));
4929
+ request.input("updatedAt", mssql.default.DateTime2, new Date(now));
4930
+ const mergeSql = `MERGE INTO ${table} AS target
4931
+ USING (SELECT @workflow_name AS workflow_name, @run_id AS run_id) AS src
4932
+ ON target.workflow_name = src.workflow_name AND target.run_id = src.run_id
4933
+ WHEN MATCHED THEN UPDATE SET
4934
+ resourceId = @resourceId,
4935
+ snapshot = @snapshot,
4936
+ [updatedAt] = @updatedAt
4937
+ WHEN NOT MATCHED THEN INSERT (workflow_name, run_id, resourceId, snapshot, [createdAt], [updatedAt])
4938
+ VALUES (@workflow_name, @run_id, @resourceId, @snapshot, @createdAt, @updatedAt);`;
4939
+ await request.query(mergeSql);
4940
+ });
4686
4941
  } catch (error) {
4942
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
4687
4943
  throw new _mastra_core_error.MastraError({
4688
4944
  id: (0, _mastra_core_storage.createStorageErrorId)("MSSQL", "PERSIST_WORKFLOW_SNAPSHOT", "FAILED"),
4689
4945
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -4752,23 +5008,15 @@ var WorkflowsMSSQL = class WorkflowsMSSQL extends _mastra_core_storage.Workflows
4752
5008
  }, error);
4753
5009
  }
4754
5010
  }
4755
- async deleteWorkflowRunById({ runId, workflowName }) {
5011
+ async deleteWorkflowRunById({ runId, workflowName, fence }) {
4756
5012
  const table = getTableName({
4757
5013
  indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
4758
5014
  schemaName: getSchemaName(this.schema)
4759
5015
  });
4760
- const transaction = this.pool.transaction();
4761
5016
  try {
4762
- await transaction.begin();
4763
- const deleteRequest = new mssql.default.Request(transaction);
4764
- deleteRequest.input("workflow_name", workflowName);
4765
- deleteRequest.input("run_id", runId);
4766
- await deleteRequest.query(`DELETE FROM ${table} WHERE workflow_name = @workflow_name AND run_id = @run_id`);
4767
- await transaction.commit();
5017
+ await withRunFence(this.pool, this.#runOwnersTable, (0, _mastra_core_storage.resolveRunFence)(this, fence, runId), "deleteWorkflowRunById", (q) => q.request().input("workflow_name", workflowName).input("run_id", runId).query(`DELETE FROM ${table} WHERE workflow_name = @workflow_name AND run_id = @run_id`));
4768
5018
  } catch (error) {
4769
- try {
4770
- await transaction.rollback();
4771
- } catch {}
5019
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
4772
5020
  throw new _mastra_core_error.MastraError({
4773
5021
  id: (0, _mastra_core_storage.createStorageErrorId)("MSSQL", "DELETE_WORKFLOW_RUN_BY_ID", "FAILED"),
4774
5022
  domain: _mastra_core_error.ErrorDomain.STORAGE,