@mastra/mssql 1.10.2 → 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.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, MastraCompositeStore, MemoryStorage, ObservabilityStorage, SPAN_SCHEMA, ScoresStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_BACKGROUND_TASKS, TABLE_MESSAGES, TABLE_RESOURCES, TABLE_SCHEMAS, TABLE_SCORERS, TABLE_SPANS, TABLE_THREADS, TABLE_WORKFLOW_DEFINITIONS, TABLE_WORKFLOW_SNAPSHOT, TraceStatus, WORKFLOW_DEFINITIONS_SCHEMA, WorkflowDefinitionsStorage, WorkflowsStorage, calculatePagination, createStorageErrorId, executeRetentionPrune, getDefaultValue, listTracesArgsSchema, matchesExpectedWorkflowStatus, normalizePerPage, resolveRetentionTargets, retentionCutoffMs, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
2
+ import { AGENTS_SCHEMA, AGENT_VERSIONS_SCHEMA, AgentsStorage, BackgroundTasksStorage, MastraCompositeStore, MemoryStorage, ObservabilityStorage, RUN_FENCING_TABLE_SCHEMAS, RunFenceConflictError, SPAN_SCHEMA, ScoresStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_BACKGROUND_TASKS, TABLE_MEMORY_RUN_FENCES, TABLE_MESSAGES, TABLE_RESOURCES, TABLE_SCHEMAS, TABLE_SCORERS, TABLE_SPANS, TABLE_THREADS, TABLE_WORKFLOW_DEFINITIONS, TABLE_WORKFLOW_RUN_OWNERS, TABLE_WORKFLOW_SNAPSHOT, TraceStatus, WORKFLOW_DEFINITIONS_SCHEMA, WorkflowDefinitionsStorage, WorkflowsStorage, calculatePagination, createStorageErrorId, executeRetentionPrune, getDefaultValue, isRunFenceConflictError, listTracesArgsSchema, matchesExpectedWorkflowStatus, matchesRunFence, normalizePerPage, resolveRetentionTargets, resolveRunFence, retentionCutoffMs, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
3
3
  import sql from "mssql";
4
4
  import { parseSqlIdentifier } from "@mastra/core/utils";
5
5
  import { MastraBase } from "@mastra/core/base";
@@ -2103,6 +2103,73 @@ var BackgroundTasksMSSQL = class BackgroundTasksMSSQL extends BackgroundTasksSto
2103
2103
  }
2104
2104
  };
2105
2105
  //#endregion
2106
+ //#region src/storage/db/run-fencing.ts
2107
+ /**
2108
+ * Epoch milliseconds on the database clock. SQL Server evaluates
2109
+ * `SYSUTCDATETIME()` once when a statement starts, so a statement that waits
2110
+ * on a lock compares against the time it started. That error only makes an
2111
+ * existing lease look longer and a new lease shorter.
2112
+ */
2113
+ const DB_NOW_MS = `DATEDIFF_BIG(MILLISECOND, '1970-01-01', SYSUTCDATETIME())`;
2114
+ const ERR_DEADLOCK_VICTIM = 1205;
2115
+ const ERR_UNIQUE_INDEX = 2601;
2116
+ const ERR_PRIMARY_KEY = 2627;
2117
+ const MAX_CLAIM_ATTEMPTS = 5;
2118
+ /**
2119
+ * The check half of a fenced write; call it first inside the write's
2120
+ * transaction. `HOLDLOCK` keeps the shared lock on the claim row until commit,
2121
+ * even on databases with read-committed snapshot on, so a claim that updates
2122
+ * the row waits for the write to finish, and every later write sees the new
2123
+ * claim.
2124
+ */
2125
+ async function assertRunFence(q, claimsTable, fence, operation) {
2126
+ const row = (await q.request().input("runId", fence.runId).query(`SELECT [generation], [ownerId] FROM ${claimsTable} WITH (HOLDLOCK) WHERE [runId] = @runId`)).recordset[0];
2127
+ if (!matchesRunFence(row ? {
2128
+ generation: Number(row.generation),
2129
+ ownerId: String(row.ownerId)
2130
+ } : null, fence)) throw new RunFenceConflictError(fence, operation);
2131
+ }
2132
+ /** Runs `fn` in a transaction. */
2133
+ async function inTransaction(pool, fn) {
2134
+ const transaction = pool.transaction();
2135
+ await transaction.begin();
2136
+ try {
2137
+ const result = await fn(transaction);
2138
+ await transaction.commit();
2139
+ return result;
2140
+ } catch (error) {
2141
+ await transaction.rollback().catch(() => {});
2142
+ throw error;
2143
+ }
2144
+ }
2145
+ /**
2146
+ * Runs `write` directly when it carries no fence, or in a transaction behind
2147
+ * the fence check when it does.
2148
+ */
2149
+ function withRunFence(pool, claimsTable, fence, operation, write) {
2150
+ if (!fence) return write(pool);
2151
+ return inTransaction(pool, async (transaction) => {
2152
+ await assertRunFence(transaction, claimsTable, fence, operation);
2153
+ return write(transaction);
2154
+ });
2155
+ }
2156
+ /**
2157
+ * Runs a transaction that creates or updates a run's claim row, retrying when
2158
+ * two first claims race to insert it. Claims read the row with
2159
+ * `UPDLOCK, HOLDLOCK`, which locks the key range of a missing row, so racing
2160
+ * first claims normally queue; the retry covers a deadlock victim or a
2161
+ * duplicate key if they don't. On retry the row exists and the claim
2162
+ * serializes behind its row lock.
2163
+ */
2164
+ async function claimTransaction(pool, fn) {
2165
+ for (let attempt = 1;; attempt++) try {
2166
+ return await inTransaction(pool, fn);
2167
+ } catch (error) {
2168
+ const number = error.number;
2169
+ if (attempt >= MAX_CLAIM_ATTEMPTS || !(number === ERR_DEADLOCK_VICTIM || number === ERR_PRIMARY_KEY || number === ERR_UNIQUE_INDEX)) throw error;
2170
+ }
2171
+ }
2172
+ //#endregion
2106
2173
  //#region src/storage/domains/memory/index.ts
2107
2174
  function bindMssqlMetadataParams(request, params) {
2108
2175
  for (const [paramName, paramValue] of Object.entries(params)) request.input(paramName, paramValue);
@@ -2192,9 +2259,69 @@ var MemoryMSSQL = class MemoryMSSQL extends MemoryStorage {
2192
2259
  tableName: TABLE_RESOURCES,
2193
2260
  schema: TABLE_SCHEMAS[TABLE_RESOURCES]
2194
2261
  });
2262
+ await this.db.createTable({
2263
+ tableName: TABLE_MEMORY_RUN_FENCES,
2264
+ schema: RUN_FENCING_TABLE_SCHEMAS[TABLE_MEMORY_RUN_FENCES]
2265
+ });
2195
2266
  await this.createDefaultIndexes();
2196
2267
  await this.createCustomIndexes();
2197
2268
  }
2269
+ supportsRunFencing() {
2270
+ return true;
2271
+ }
2272
+ get #runFencesTable() {
2273
+ return getTableName({
2274
+ indexName: TABLE_MEMORY_RUN_FENCES,
2275
+ schemaName: getSchemaName(this.schema)
2276
+ });
2277
+ }
2278
+ async raiseRunFence(fence) {
2279
+ const table = this.#runFencesTable;
2280
+ try {
2281
+ return await claimTransaction(this.pool, async (transaction) => {
2282
+ const row = (await transaction.request().input("runId", fence.runId).query(`SELECT [generation], [ownerId] FROM ${table} WITH (UPDLOCK, HOLDLOCK) WHERE [runId] = @runId`)).recordset[0];
2283
+ if (!row) {
2284
+ await transaction.request().input("runId", fence.runId).input("generation", sql.BigInt, fence.generation).input("ownerId", fence.ownerId).query(`INSERT INTO ${table} ([runId], [generation], [ownerId]) VALUES (@runId, @generation, @ownerId)`);
2285
+ return true;
2286
+ }
2287
+ const generation = Number(row.generation);
2288
+ const raises = generation < fence.generation;
2289
+ const reaffirms = generation === fence.generation && String(row.ownerId) === fence.ownerId;
2290
+ if (!raises && !reaffirms) return false;
2291
+ await transaction.request().input("runId", fence.runId).input("generation", sql.BigInt, fence.generation).input("ownerId", fence.ownerId).query(`UPDATE ${table} SET [generation] = @generation, [ownerId] = @ownerId, [retiredAt] = NULL WHERE [runId] = @runId`);
2292
+ return true;
2293
+ });
2294
+ } catch (error) {
2295
+ throw new MastraError({
2296
+ id: createStorageErrorId("MSSQL", "RAISE_RUN_FENCE", "FAILED"),
2297
+ domain: ErrorDomain.STORAGE,
2298
+ category: ErrorCategory.THIRD_PARTY,
2299
+ details: { runId: fence.runId }
2300
+ }, error);
2301
+ }
2302
+ }
2303
+ async retireRunFence(fence) {
2304
+ try {
2305
+ return (await this.pool.request().input("runId", fence.runId).input("generation", sql.BigInt, fence.generation).input("ownerId", fence.ownerId).query(`UPDATE ${this.#runFencesTable} SET [retiredAt] = ${DB_NOW_MS}
2306
+ WHERE [runId] = @runId AND [generation] = @generation AND [ownerId] = @ownerId`)).rowsAffected[0] === 1;
2307
+ } catch (error) {
2308
+ throw new MastraError({
2309
+ id: createStorageErrorId("MSSQL", "RETIRE_RUN_FENCE", "FAILED"),
2310
+ domain: ErrorDomain.STORAGE,
2311
+ category: ErrorCategory.THIRD_PARTY,
2312
+ details: { runId: fence.runId }
2313
+ }, error);
2314
+ }
2315
+ }
2316
+ /** Runs `write` behind its own fence, otherwise the one in scope for this store. */
2317
+ #fenced(fence, operation, write) {
2318
+ return withRunFence(this.pool, this.#runFencesTable, resolveRunFence(this, fence), operation, write);
2319
+ }
2320
+ /** The fence check for a write that already runs in its own transaction. */
2321
+ async #assertFence(transaction, fence, operation) {
2322
+ const resolved = resolveRunFence(this, fence);
2323
+ if (resolved) await assertRunFence(transaction, this.#runFencesTable, resolved, operation);
2324
+ }
2198
2325
  /**
2199
2326
  * Returns default index definitions for the memory domain tables.
2200
2327
  * IMPORTANT: Uses seq_id DESC instead of createdAt DESC for MSSQL due to millisecond accuracy limitations
@@ -2237,6 +2364,7 @@ var MemoryMSSQL = class MemoryMSSQL extends MemoryStorage {
2237
2364
  await this.db.clearTable({ tableName: TABLE_MESSAGES });
2238
2365
  await this.db.clearTable({ tableName: TABLE_THREADS });
2239
2366
  await this.db.clearTable({ tableName: TABLE_RESOURCES });
2367
+ await this.db.clearTable({ tableName: TABLE_MEMORY_RUN_FENCES });
2240
2368
  }
2241
2369
  async getThreadById({ threadId, resourceId }) {
2242
2370
  try {
@@ -2387,7 +2515,7 @@ var MemoryMSSQL = class MemoryMSSQL extends MemoryStorage {
2387
2515
  throw mastraError;
2388
2516
  }
2389
2517
  }
2390
- async saveThread({ thread }) {
2518
+ async saveThread({ thread, fence }) {
2391
2519
  try {
2392
2520
  const mergeSql = `MERGE INTO ${getTableName({
2393
2521
  indexName: TABLE_THREADS,
@@ -2404,18 +2532,21 @@ var MemoryMSSQL = class MemoryMSSQL extends MemoryStorage {
2404
2532
  WHEN NOT MATCHED THEN
2405
2533
  INSERT (id, [resourceId], title, metadata, [createdAt], [updatedAt])
2406
2534
  VALUES (@id, @resourceId, @title, @metadata, @createdAt, @updatedAt);`;
2407
- const req = this.pool.request();
2408
- req.input("id", thread.id);
2409
- req.input("resourceId", thread.resourceId);
2410
- req.input("title", thread.title);
2411
2535
  const metadata = thread.metadata ? JSON.stringify(thread.metadata) : null;
2412
- if (metadata === null) req.input("metadata", sql.NVarChar, null);
2413
- else req.input("metadata", metadata);
2414
- req.input("createdAt", sql.DateTime2, thread.createdAt);
2415
- req.input("updatedAt", sql.DateTime2, thread.updatedAt);
2416
- await req.query(mergeSql);
2536
+ await this.#fenced(fence, "saveThread", (q) => {
2537
+ const req = q.request();
2538
+ req.input("id", thread.id);
2539
+ req.input("resourceId", thread.resourceId);
2540
+ req.input("title", thread.title);
2541
+ if (metadata === null) req.input("metadata", sql.NVarChar, null);
2542
+ else req.input("metadata", metadata);
2543
+ req.input("createdAt", sql.DateTime2, thread.createdAt);
2544
+ req.input("updatedAt", sql.DateTime2, thread.updatedAt);
2545
+ return req.query(mergeSql);
2546
+ });
2417
2547
  return thread;
2418
2548
  } catch (error) {
2549
+ if (isRunFenceConflictError(error)) throw error;
2419
2550
  throw new MastraError({
2420
2551
  id: createStorageErrorId("MSSQL", "SAVE_THREAD", "FAILED"),
2421
2552
  domain: ErrorDomain.STORAGE,
@@ -2427,7 +2558,7 @@ var MemoryMSSQL = class MemoryMSSQL extends MemoryStorage {
2427
2558
  /**
2428
2559
  * Updates a thread's title and metadata, merging with existing metadata. Returns the updated thread.
2429
2560
  */
2430
- async updateThread({ id, title, metadata }) {
2561
+ async updateThread({ id, title, metadata, fence }) {
2431
2562
  const existingThread = await this.getThreadById({ threadId: id });
2432
2563
  if (!existingThread) throw new MastraError({
2433
2564
  id: createStorageErrorId("MSSQL", "UPDATE_THREAD", "NOT_FOUND"),
@@ -2453,12 +2584,7 @@ var MemoryMSSQL = class MemoryMSSQL extends MemoryStorage {
2453
2584
  [updatedAt] = @updatedAt
2454
2585
  OUTPUT INSERTED.*
2455
2586
  WHERE id = @id`;
2456
- const req = this.pool.request();
2457
- req.input("id", id);
2458
- req.input("title", title ?? existingThread.title);
2459
- req.input("metadata", JSON.stringify(mergedMetadata));
2460
- req.input("updatedAt", /* @__PURE__ */ new Date());
2461
- const result = await req.query(sql);
2587
+ 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));
2462
2588
  let thread = result.recordset && result.recordset[0];
2463
2589
  if (thread && "seq_id" in thread) {
2464
2590
  const { seq_id, ...rest } = thread;
@@ -2481,6 +2607,7 @@ var MemoryMSSQL = class MemoryMSSQL extends MemoryStorage {
2481
2607
  updatedAt: thread.updatedAt
2482
2608
  };
2483
2609
  } catch (error) {
2610
+ if (isRunFenceConflictError(error)) throw error;
2484
2611
  throw new MastraError({
2485
2612
  id: createStorageErrorId("MSSQL", "UPDATE_THREAD", "FAILED"),
2486
2613
  domain: ErrorDomain.STORAGE,
@@ -2846,7 +2973,7 @@ var MemoryMSSQL = class MemoryMSSQL extends MemoryStorage {
2846
2973
  throw mastraError;
2847
2974
  }
2848
2975
  }
2849
- async saveMessages({ messages }) {
2976
+ async saveMessages({ messages, fence }) {
2850
2977
  if (messages.length === 0) return { messages: [] };
2851
2978
  const threadId = messages[0]?.threadId;
2852
2979
  if (!threadId) throw new MastraError({
@@ -2874,6 +3001,7 @@ var MemoryMSSQL = class MemoryMSSQL extends MemoryStorage {
2874
3001
  const transaction = this.pool.transaction();
2875
3002
  await transaction.begin();
2876
3003
  try {
3004
+ await this.#assertFence(transaction, fence, "saveMessages");
2877
3005
  for (const message of messages) {
2878
3006
  if (!message.threadId) throw new Error(`Expected to find a threadId for message, but couldn't find one. An unexpected error has occurred.`);
2879
3007
  if (!message.resourceId) throw new Error(`Expected to find a resourceId for message, but couldn't find one. An unexpected error has occurred.`);
@@ -2921,6 +3049,7 @@ var MemoryMSSQL = class MemoryMSSQL extends MemoryStorage {
2921
3049
  });
2922
3050
  return { messages: new MessageList().add(messagesWithParsedContent, "memory").get.all.db() };
2923
3051
  } catch (error) {
3052
+ if (isRunFenceConflictError(error)) throw error;
2924
3053
  throw new MastraError({
2925
3054
  id: createStorageErrorId("MSSQL", "SAVE_MESSAGES", "FAILED"),
2926
3055
  domain: ErrorDomain.STORAGE,
@@ -2929,7 +3058,7 @@ var MemoryMSSQL = class MemoryMSSQL extends MemoryStorage {
2929
3058
  }, error);
2930
3059
  }
2931
3060
  }
2932
- async updateMessages({ messages }) {
3061
+ async updateMessages({ messages, fence }) {
2933
3062
  if (!messages || messages.length === 0) return [];
2934
3063
  const messageIds = messages.map((m) => m.id);
2935
3064
  const idParams = messageIds.map((_, i) => `@id${i}`).join(", ");
@@ -2953,6 +3082,7 @@ var MemoryMSSQL = class MemoryMSSQL extends MemoryStorage {
2953
3082
  const transaction = this.pool.transaction();
2954
3083
  try {
2955
3084
  await transaction.begin();
3085
+ await this.#assertFence(transaction, fence, "updateMessages");
2956
3086
  for (const existingMessage of existingMessages) {
2957
3087
  const updatePayload = messages.find((m) => m.id === existingMessage.id);
2958
3088
  if (!updatePayload) continue;
@@ -3005,6 +3135,7 @@ var MemoryMSSQL = class MemoryMSSQL extends MemoryStorage {
3005
3135
  await transaction.commit();
3006
3136
  } catch (error) {
3007
3137
  await transaction.rollback();
3138
+ if (isRunFenceConflictError(error)) throw error;
3008
3139
  throw new MastraError({
3009
3140
  id: createStorageErrorId("MSSQL", "UPDATE_MESSAGES", "FAILED"),
3010
3141
  domain: ErrorDomain.STORAGE,
@@ -3020,7 +3151,7 @@ var MemoryMSSQL = class MemoryMSSQL extends MemoryStorage {
3020
3151
  return message;
3021
3152
  });
3022
3153
  }
3023
- async deleteMessages(messageIds) {
3154
+ async deleteMessages(messageIds, options) {
3024
3155
  if (!messageIds || messageIds.length === 0) return;
3025
3156
  try {
3026
3157
  const messageTableName = getTableName({
@@ -3040,6 +3171,7 @@ var MemoryMSSQL = class MemoryMSSQL extends MemoryStorage {
3040
3171
  const transaction = this.pool.transaction();
3041
3172
  await transaction.begin();
3042
3173
  try {
3174
+ await this.#assertFence(transaction, options?.fence, "deleteMessages");
3043
3175
  const deleteRequest = transaction.request();
3044
3176
  messageIds.forEach((id, idx) => {
3045
3177
  deleteRequest.input(`p${idx + 1}`, id);
@@ -3058,6 +3190,7 @@ var MemoryMSSQL = class MemoryMSSQL extends MemoryStorage {
3058
3190
  throw error;
3059
3191
  }
3060
3192
  } catch (error) {
3193
+ if (isRunFenceConflictError(error)) throw error;
3061
3194
  throw new MastraError({
3062
3195
  id: createStorageErrorId("MSSQL", "DELETE_MESSAGES", "FAILED"),
3063
3196
  domain: ErrorDomain.STORAGE,
@@ -3105,7 +3238,7 @@ var MemoryMSSQL = class MemoryMSSQL extends MemoryStorage {
3105
3238
  });
3106
3239
  return resource;
3107
3240
  }
3108
- async updateResource({ resourceId, workingMemory, metadata }) {
3241
+ async updateResource({ resourceId, workingMemory, metadata, fence }) {
3109
3242
  try {
3110
3243
  const existingResource = await this.getResourceById({ resourceId });
3111
3244
  if (!existingResource) {
@@ -3116,7 +3249,12 @@ var MemoryMSSQL = class MemoryMSSQL extends MemoryStorage {
3116
3249
  createdAt: /* @__PURE__ */ new Date(),
3117
3250
  updatedAt: /* @__PURE__ */ new Date()
3118
3251
  };
3119
- return this.saveResource({ resource: newResource });
3252
+ await this.#fenced(fence, "updateResource", (q) => this.db.insert({
3253
+ tableName: TABLE_RESOURCES,
3254
+ record: newResource,
3255
+ transaction: q
3256
+ }));
3257
+ return newResource;
3120
3258
  }
3121
3259
  const updatedResource = {
3122
3260
  ...existingResource,
@@ -3131,22 +3269,25 @@ var MemoryMSSQL = class MemoryMSSQL extends MemoryStorage {
3131
3269
  indexName: TABLE_RESOURCES,
3132
3270
  schemaName: getSchemaName(this.schema)
3133
3271
  });
3134
- const updates = [];
3135
- const req = this.pool.request();
3136
- if (workingMemory !== void 0) {
3137
- updates.push("workingMemory = @workingMemory");
3138
- req.input("workingMemory", workingMemory);
3139
- }
3140
- if (metadata) {
3141
- updates.push("metadata = @metadata");
3142
- req.input("metadata", JSON.stringify(updatedResource.metadata));
3143
- }
3144
- updates.push("updatedAt = @updatedAt");
3145
- req.input("updatedAt", updatedResource.updatedAt.toISOString());
3146
- req.input("id", resourceId);
3147
- await req.query(`UPDATE ${tableName} SET ${updates.join(", ")} WHERE id = @id`);
3272
+ await this.#fenced(fence, "updateResource", (q) => {
3273
+ const updates = [];
3274
+ const req = q.request();
3275
+ if (workingMemory !== void 0) {
3276
+ updates.push("workingMemory = @workingMemory");
3277
+ req.input("workingMemory", workingMemory);
3278
+ }
3279
+ if (metadata) {
3280
+ updates.push("metadata = @metadata");
3281
+ req.input("metadata", JSON.stringify(updatedResource.metadata));
3282
+ }
3283
+ updates.push("updatedAt = @updatedAt");
3284
+ req.input("updatedAt", updatedResource.updatedAt.toISOString());
3285
+ req.input("id", resourceId);
3286
+ return req.query(`UPDATE ${tableName} SET ${updates.join(", ")} WHERE id = @id`);
3287
+ });
3148
3288
  return updatedResource;
3149
3289
  } catch (error) {
3290
+ if (isRunFenceConflictError(error)) throw error;
3150
3291
  const mastraError = new MastraError({
3151
3292
  id: createStorageErrorId("MSSQL", "UPDATE_RESOURCE", "FAILED"),
3152
3293
  domain: ErrorDomain.STORAGE,
@@ -4423,6 +4564,17 @@ var WorkflowDefinitionsMSSQL = class WorkflowDefinitionsMSSQL extends WorkflowDe
4423
4564
  };
4424
4565
  //#endregion
4425
4566
  //#region src/storage/domains/workflows/index.ts
4567
+ const RUN_OWNER_COLUMNS = `[generation], [ownerId], [leaseExpiresAt], ${DB_NOW_MS} AS [nowMs]`;
4568
+ function toRunOwnershipRecord(runId, row) {
4569
+ const leaseExpiresAt = row.leaseExpiresAt === null ? null : Number(row.leaseExpiresAt);
4570
+ return {
4571
+ runId,
4572
+ generation: Number(row.generation),
4573
+ ownerId: String(row.ownerId),
4574
+ leaseExpiresAt: leaseExpiresAt === null ? null : new Date(leaseExpiresAt),
4575
+ live: leaseExpiresAt !== null && leaseExpiresAt > Number(row.nowMs)
4576
+ };
4577
+ }
4426
4578
  var WorkflowsMSSQL = class WorkflowsMSSQL extends WorkflowsStorage {
4427
4579
  pool;
4428
4580
  db;
@@ -4472,6 +4624,10 @@ var WorkflowsMSSQL = class WorkflowsMSSQL extends WorkflowsStorage {
4472
4624
  tableName: TABLE_WORKFLOW_SNAPSHOT,
4473
4625
  schema: TABLE_SCHEMAS[TABLE_WORKFLOW_SNAPSHOT]
4474
4626
  });
4627
+ await this.db.createTable({
4628
+ tableName: TABLE_WORKFLOW_RUN_OWNERS,
4629
+ schema: RUN_FENCING_TABLE_SCHEMAS[TABLE_WORKFLOW_RUN_OWNERS]
4630
+ });
4475
4631
  await this.createDefaultIndexes();
4476
4632
  await this.createCustomIndexes();
4477
4633
  }
@@ -4488,6 +4644,98 @@ var WorkflowsMSSQL = class WorkflowsMSSQL extends WorkflowsStorage {
4488
4644
  }
4489
4645
  async dangerouslyClearAll() {
4490
4646
  await this.db.clearTable({ tableName: TABLE_WORKFLOW_SNAPSHOT });
4647
+ await this.db.clearTable({ tableName: TABLE_WORKFLOW_RUN_OWNERS });
4648
+ }
4649
+ get #runOwnersTable() {
4650
+ return getTableName({
4651
+ indexName: TABLE_WORKFLOW_RUN_OWNERS,
4652
+ schemaName: getSchemaName(this.schema)
4653
+ });
4654
+ }
4655
+ async #readRunOwner(q, runId) {
4656
+ const row = (await q.request().input("runId", runId).query(`SELECT ${RUN_OWNER_COLUMNS} FROM ${this.#runOwnersTable} WHERE [runId] = @runId`)).recordset[0];
4657
+ return row ? toRunOwnershipRecord(runId, row) : null;
4658
+ }
4659
+ supportsRunFencing() {
4660
+ return true;
4661
+ }
4662
+ #ownershipError(operation, runId, error) {
4663
+ return new MastraError({
4664
+ id: createStorageErrorId("MSSQL", operation, "FAILED"),
4665
+ domain: ErrorDomain.STORAGE,
4666
+ category: ErrorCategory.THIRD_PARTY,
4667
+ details: { runId }
4668
+ }, error);
4669
+ }
4670
+ async claimRunOwnership({ runId, ownerId, leaseMs, force, expectedGeneration }) {
4671
+ const table = this.#runOwnersTable;
4672
+ try {
4673
+ return await claimTransaction(this.pool, async (transaction) => {
4674
+ if ((await transaction.request().input("runId", runId).query(`SELECT [generation] FROM ${table} WITH (UPDLOCK, HOLDLOCK) WHERE [runId] = @runId`)).recordset.length === 0) {
4675
+ if (expectedGeneration !== void 0 && expectedGeneration !== 0) return {
4676
+ acquired: false,
4677
+ record: null
4678
+ };
4679
+ await transaction.request().input("runId", runId).input("ownerId", ownerId).input("leaseMs", sql.BigInt, leaseMs).query(`INSERT INTO ${table} ([runId], [generation], [ownerId], [leaseExpiresAt], [updatedAt])
4680
+ VALUES (@runId, ${DB_NOW_MS}, @ownerId, ${DB_NOW_MS} + @leaseMs, ${DB_NOW_MS})`);
4681
+ return {
4682
+ acquired: true,
4683
+ record: await this.#readRunOwner(transaction, runId)
4684
+ };
4685
+ }
4686
+ const updated = await transaction.request().input("runId", runId).input("ownerId", ownerId).input("leaseMs", sql.BigInt, leaseMs).input("expectedGeneration", sql.BigInt, expectedGeneration ?? null).input("force", sql.Bit, force === true).query(`UPDATE ${table}
4687
+ SET [generation] = [generation] + 1, [ownerId] = @ownerId, [leaseExpiresAt] = ${DB_NOW_MS} + @leaseMs,
4688
+ [updatedAt] = ${DB_NOW_MS}
4689
+ WHERE [runId] = @runId
4690
+ AND (@expectedGeneration IS NULL OR [generation] = @expectedGeneration)
4691
+ AND (@force = 1 OR [leaseExpiresAt] IS NULL OR [leaseExpiresAt] <= ${DB_NOW_MS})`);
4692
+ const record = await this.#readRunOwner(transaction, runId);
4693
+ return updated.rowsAffected[0] === 1 && record ? {
4694
+ acquired: true,
4695
+ record
4696
+ } : {
4697
+ acquired: false,
4698
+ record
4699
+ };
4700
+ });
4701
+ } catch (error) {
4702
+ throw this.#ownershipError("CLAIM_RUN_OWNERSHIP", runId, error);
4703
+ }
4704
+ }
4705
+ async renewRunOwnership({ leaseMs, ...fence }) {
4706
+ try {
4707
+ return await inTransaction(this.pool, async (transaction) => {
4708
+ const updated = await transaction.request().input("runId", fence.runId).input("generation", sql.BigInt, fence.generation).input("ownerId", fence.ownerId).input("leaseMs", sql.BigInt, leaseMs).query(`UPDATE ${this.#runOwnersTable}
4709
+ SET [leaseExpiresAt] = ${DB_NOW_MS} + @leaseMs, [updatedAt] = ${DB_NOW_MS}
4710
+ WHERE [runId] = @runId AND [generation] = @generation AND [ownerId] = @ownerId
4711
+ AND [leaseExpiresAt] IS NOT NULL`);
4712
+ const record = await this.#readRunOwner(transaction, fence.runId);
4713
+ return updated.rowsAffected[0] === 1 && record ? {
4714
+ renewed: true,
4715
+ record
4716
+ } : {
4717
+ renewed: false,
4718
+ record
4719
+ };
4720
+ });
4721
+ } catch (error) {
4722
+ throw this.#ownershipError("RENEW_RUN_OWNERSHIP", fence.runId, error);
4723
+ }
4724
+ }
4725
+ async releaseRunOwnership(fence) {
4726
+ try {
4727
+ return (await this.pool.request().input("runId", fence.runId).input("generation", sql.BigInt, fence.generation).input("ownerId", fence.ownerId).query(`UPDATE ${this.#runOwnersTable} SET [leaseExpiresAt] = NULL, [updatedAt] = ${DB_NOW_MS}
4728
+ WHERE [runId] = @runId AND [generation] = @generation AND [ownerId] = @ownerId`)).rowsAffected[0] === 1;
4729
+ } catch (error) {
4730
+ throw this.#ownershipError("RELEASE_RUN_OWNERSHIP", fence.runId, error);
4731
+ }
4732
+ }
4733
+ async getRunOwnership({ runId }) {
4734
+ try {
4735
+ return await this.#readRunOwner(this.pool, runId);
4736
+ } catch (error) {
4737
+ throw this.#ownershipError("GET_RUN_OWNERSHIP", runId, error);
4738
+ }
4491
4739
  }
4492
4740
  parseWorkflowRun(row) {
4493
4741
  let parsedSnapshot = row.snapshot;
@@ -4505,14 +4753,16 @@ var WorkflowsMSSQL = class WorkflowsMSSQL extends WorkflowsStorage {
4505
4753
  resourceId: row.resourceId
4506
4754
  };
4507
4755
  }
4508
- async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state }) {
4756
+ async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state, fence: explicitFence }) {
4509
4757
  const table = getTableName({
4510
4758
  indexName: TABLE_WORKFLOW_SNAPSHOT,
4511
4759
  schemaName: getSchemaName(this.schema)
4512
4760
  });
4761
+ const fence = resolveRunFence(this, explicitFence, runId);
4513
4762
  const transaction = this.pool.transaction();
4514
4763
  try {
4515
4764
  await transaction.begin();
4765
+ if (fence) await assertRunFence(transaction, this.#runOwnersTable, fence, "updateWorkflowResults");
4516
4766
  const selectRequest = new sql.Request(transaction);
4517
4767
  selectRequest.input("workflow_name", workflowName);
4518
4768
  selectRequest.input("run_id", runId);
@@ -4560,6 +4810,7 @@ var WorkflowsMSSQL = class WorkflowsMSSQL extends WorkflowsStorage {
4560
4810
  try {
4561
4811
  await transaction.rollback();
4562
4812
  } catch {}
4813
+ if (isRunFenceConflictError(error)) throw error;
4563
4814
  throw new MastraError({
4564
4815
  id: createStorageErrorId("MSSQL", "UPDATE_WORKFLOW_RESULTS", "FAILED"),
4565
4816
  domain: ErrorDomain.STORAGE,
@@ -4572,14 +4823,16 @@ var WorkflowsMSSQL = class WorkflowsMSSQL extends WorkflowsStorage {
4572
4823
  }, error);
4573
4824
  }
4574
4825
  }
4575
- async updateWorkflowState({ workflowName, runId, opts }) {
4826
+ async updateWorkflowState({ workflowName, runId, opts, fence: explicitFence }) {
4576
4827
  const table = getTableName({
4577
4828
  indexName: TABLE_WORKFLOW_SNAPSHOT,
4578
4829
  schemaName: getSchemaName(this.schema)
4579
4830
  });
4831
+ const fence = resolveRunFence(this, explicitFence, runId);
4580
4832
  const transaction = this.pool.transaction();
4581
4833
  try {
4582
4834
  await transaction.begin();
4835
+ if (fence) await assertRunFence(transaction, this.#runOwnersTable, fence, "updateWorkflowState");
4583
4836
  const selectRequest = new sql.Request(transaction);
4584
4837
  selectRequest.input("workflow_name", workflowName);
4585
4838
  selectRequest.input("run_id", runId);
@@ -4635,31 +4888,34 @@ var WorkflowsMSSQL = class WorkflowsMSSQL extends WorkflowsStorage {
4635
4888
  }, error);
4636
4889
  }
4637
4890
  }
4638
- async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot }) {
4891
+ async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, fence }) {
4639
4892
  const table = getTableName({
4640
4893
  indexName: TABLE_WORKFLOW_SNAPSHOT,
4641
4894
  schemaName: getSchemaName(this.schema)
4642
4895
  });
4643
4896
  const now = (/* @__PURE__ */ new Date()).toISOString();
4644
4897
  try {
4645
- const request = this.pool.request();
4646
- request.input("workflow_name", workflowName);
4647
- request.input("run_id", runId);
4648
- request.input("resourceId", resourceId);
4649
- request.input("snapshot", JSON.stringify(snapshot));
4650
- request.input("createdAt", sql.DateTime2, new Date(now));
4651
- request.input("updatedAt", sql.DateTime2, new Date(now));
4652
- const mergeSql = `MERGE INTO ${table} AS target
4653
- USING (SELECT @workflow_name AS workflow_name, @run_id AS run_id) AS src
4654
- ON target.workflow_name = src.workflow_name AND target.run_id = src.run_id
4655
- WHEN MATCHED THEN UPDATE SET
4656
- resourceId = @resourceId,
4657
- snapshot = @snapshot,
4658
- [updatedAt] = @updatedAt
4659
- WHEN NOT MATCHED THEN INSERT (workflow_name, run_id, resourceId, snapshot, [createdAt], [updatedAt])
4660
- VALUES (@workflow_name, @run_id, @resourceId, @snapshot, @createdAt, @updatedAt);`;
4661
- await request.query(mergeSql);
4898
+ await withRunFence(this.pool, this.#runOwnersTable, resolveRunFence(this, fence, runId), "persistWorkflowSnapshot", async (q) => {
4899
+ const request = q.request();
4900
+ request.input("workflow_name", workflowName);
4901
+ request.input("run_id", runId);
4902
+ request.input("resourceId", resourceId);
4903
+ request.input("snapshot", JSON.stringify(snapshot));
4904
+ request.input("createdAt", sql.DateTime2, new Date(now));
4905
+ request.input("updatedAt", sql.DateTime2, new Date(now));
4906
+ const mergeSql = `MERGE INTO ${table} AS target
4907
+ USING (SELECT @workflow_name AS workflow_name, @run_id AS run_id) AS src
4908
+ ON target.workflow_name = src.workflow_name AND target.run_id = src.run_id
4909
+ WHEN MATCHED THEN UPDATE SET
4910
+ resourceId = @resourceId,
4911
+ snapshot = @snapshot,
4912
+ [updatedAt] = @updatedAt
4913
+ WHEN NOT MATCHED THEN INSERT (workflow_name, run_id, resourceId, snapshot, [createdAt], [updatedAt])
4914
+ VALUES (@workflow_name, @run_id, @resourceId, @snapshot, @createdAt, @updatedAt);`;
4915
+ await request.query(mergeSql);
4916
+ });
4662
4917
  } catch (error) {
4918
+ if (isRunFenceConflictError(error)) throw error;
4663
4919
  throw new MastraError({
4664
4920
  id: createStorageErrorId("MSSQL", "PERSIST_WORKFLOW_SNAPSHOT", "FAILED"),
4665
4921
  domain: ErrorDomain.STORAGE,
@@ -4728,23 +4984,15 @@ var WorkflowsMSSQL = class WorkflowsMSSQL extends WorkflowsStorage {
4728
4984
  }, error);
4729
4985
  }
4730
4986
  }
4731
- async deleteWorkflowRunById({ runId, workflowName }) {
4987
+ async deleteWorkflowRunById({ runId, workflowName, fence }) {
4732
4988
  const table = getTableName({
4733
4989
  indexName: TABLE_WORKFLOW_SNAPSHOT,
4734
4990
  schemaName: getSchemaName(this.schema)
4735
4991
  });
4736
- const transaction = this.pool.transaction();
4737
4992
  try {
4738
- await transaction.begin();
4739
- const deleteRequest = new sql.Request(transaction);
4740
- deleteRequest.input("workflow_name", workflowName);
4741
- deleteRequest.input("run_id", runId);
4742
- await deleteRequest.query(`DELETE FROM ${table} WHERE workflow_name = @workflow_name AND run_id = @run_id`);
4743
- await transaction.commit();
4993
+ await withRunFence(this.pool, this.#runOwnersTable, 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`));
4744
4994
  } catch (error) {
4745
- try {
4746
- await transaction.rollback();
4747
- } catch {}
4995
+ if (isRunFenceConflictError(error)) throw error;
4748
4996
  throw new MastraError({
4749
4997
  id: createStorageErrorId("MSSQL", "DELETE_WORKFLOW_RUN_BY_ID", "FAILED"),
4750
4998
  domain: ErrorDomain.STORAGE,