@mastra/upstash 1.5.2 → 1.5.4-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.
@@ -3,7 +3,7 @@ name: mastra-upstash
3
3
  description: Documentation for @mastra/upstash. Use when working with @mastra/upstash APIs, configuration, or implementation.
4
4
  metadata:
5
5
  package: "@mastra/upstash"
6
- version: "1.5.2"
6
+ version: "1.5.4-alpha.0"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.5.2",
2
+ "version": "1.5.4-alpha.0",
3
3
  "package": "@mastra/upstash",
4
4
  "exports": {},
5
5
  "modules": {}
@@ -8,6 +8,8 @@ The UpstashVector class provides vector search using [Upstash Vector](https://up
8
8
 
9
9
  ## Constructor options
10
10
 
11
+ **id** (`string`): Unique identifier for this vector store instance
12
+
11
13
  **url** (`string`): Upstash Vector database URL
12
14
 
13
15
  **token** (`string`): Upstash Vector API token
@@ -194,24 +196,28 @@ Query results are returned in this format:
194
196
  interface QueryResult {
195
197
  id: string
196
198
  score: number
197
- metadata: Record<string, any>
199
+ metadata?: Record<string, any>
198
200
  vector?: number[] // Only included if includeVector is true
199
201
  }
200
202
  ```
201
203
 
202
204
  ## Error handling
203
205
 
204
- The store throws typed errors that can be caught:
206
+ The store throws `MastraError` instances from `@mastra/core/error`:
205
207
 
206
208
  ```typescript
209
+ import { MastraError } from '@mastra/core/error'
210
+
207
211
  try {
208
212
  await store.query({
209
213
  indexName: 'index_name',
210
214
  queryVector: queryVector,
211
215
  })
212
216
  } catch (error) {
213
- if (error instanceof VectorStoreError) {
214
- console.log(error.code) // 'connection_failed' | 'invalid_dimension' | etc
217
+ if (error instanceof MastraError) {
218
+ console.log(error.id) // 'MASTRA_VECTOR_UPSTASH_QUERY_FAILED'
219
+ console.log(error.domain) // 'STORAGE'
220
+ console.log(error.category) // 'USER' | 'THIRD_PARTY'
215
221
  console.log(error.details) // Additional error context
216
222
  }
217
223
  }
package/dist/index.cjs CHANGED
@@ -351,6 +351,239 @@ var BackgroundTasksUpstash = class extends _mastra_core_storage.BackgroundTasksS
351
351
  }
352
352
  };
353
353
  //#endregion
354
+ //#region src/storage/domains/run-fencing.ts
355
+ function runClaimKey(claims, runId) {
356
+ return `${claims}:run_id:${runId}`;
357
+ }
358
+ const RUN_FENCE_CONFLICT = "MASTRA_RUN_FENCE_CONFLICT";
359
+ const RUN_CLAIM_RETENTION_MS = 10080 * 60 * 1e3;
360
+ const FENCE_GUARD = `
361
+ local fenceClaim = redis.call('HMGET', KEYS[#KEYS], 'generation', 'ownerId')
362
+ if tonumber(fenceClaim[1]) ~= tonumber(ARGV[#ARGV - 1]) or fenceClaim[2] ~= ARGV[#ARGV] then
363
+ return '${RUN_FENCE_CONFLICT}'
364
+ end
365
+ `;
366
+ const OWNER = `
367
+ local function owner(flag)
368
+ local r = redis.call('HMGET', KEYS[1], 'generation', 'ownerId', 'leaseExpiresAt')
369
+ return cjson.encode({ flag, r[1], r[2], r[3] })
370
+ end
371
+ `;
372
+ const CLAIM = `${OWNER}
373
+ local generation = tonumber(redis.call('HGET', KEYS[1], 'generation'))
374
+ local expected = ARGV[3]
375
+ if not generation then
376
+ if expected ~= '' and expected ~= '0' then return owner(0) end
377
+ -- The first generation comes from the clock, so a claim after this hash is
378
+ -- removed still outranks a surviving fence.
379
+ redis.call('HSET', KEYS[1], 'generation', string.format('%d', math.max(1, tonumber(ARGV[4]))), 'ownerId', ARGV[1], 'leaseExpiresAt', ARGV[5], 'updatedAt', ARGV[4])
380
+ redis.call('PEXPIRE', KEYS[1], ARGV[6])
381
+ return owner(1)
382
+ end
383
+ if expected ~= '' and generation ~= tonumber(expected) then return owner(0) end
384
+ if ARGV[2] ~= '1' then
385
+ local expires = tonumber(redis.call('HGET', KEYS[1], 'leaseExpiresAt'))
386
+ if expires and expires > tonumber(ARGV[4]) then return owner(0) end
387
+ end
388
+ redis.call('HSET', KEYS[1], 'generation', string.format('%d', generation + 1), 'ownerId', ARGV[1], 'leaseExpiresAt', ARGV[5], 'updatedAt', ARGV[4])
389
+ redis.call('PEXPIRE', KEYS[1], ARGV[6])
390
+ return owner(1)
391
+ `;
392
+ const RENEW = `${OWNER}
393
+ local r = redis.call('HMGET', KEYS[1], 'generation', 'ownerId', 'leaseExpiresAt')
394
+ if tonumber(r[1]) ~= tonumber(ARGV[1]) or r[2] ~= ARGV[2] or not r[3] then return owner(0) end
395
+ redis.call('HSET', KEYS[1], 'leaseExpiresAt', ARGV[3], 'updatedAt', ARGV[4])
396
+ redis.call('PEXPIRE', KEYS[1], ARGV[5])
397
+ return owner(1)
398
+ `;
399
+ const RELEASE = `
400
+ local r = redis.call('HMGET', KEYS[1], 'generation', 'ownerId')
401
+ if tonumber(r[1]) ~= tonumber(ARGV[1]) or r[2] ~= ARGV[2] then return 0 end
402
+ redis.call('HDEL', KEYS[1], 'leaseExpiresAt')
403
+ redis.call('HSET', KEYS[1], 'updatedAt', ARGV[3])
404
+ redis.call('PEXPIRE', KEYS[1], ARGV[4])
405
+ return 1
406
+ `;
407
+ const READ = `${OWNER}
408
+ return owner(1)
409
+ `;
410
+ const RAISE = `
411
+ local r = redis.call('HMGET', KEYS[1], 'generation', 'ownerId')
412
+ local current = tonumber(r[1])
413
+ if not current or current < tonumber(ARGV[1]) then
414
+ redis.call('HSET', KEYS[1], 'generation', ARGV[1], 'ownerId', ARGV[2])
415
+ elseif current ~= tonumber(ARGV[1]) or r[2] ~= ARGV[2] then
416
+ return 0
417
+ end
418
+ -- Every write un-retires the fence.
419
+ redis.call('HDEL', KEYS[1], 'retiredAt')
420
+ redis.call('PERSIST', KEYS[1])
421
+ return 1
422
+ `;
423
+ const RETIRE = `
424
+ local r = redis.call('HMGET', KEYS[1], 'generation', 'ownerId')
425
+ if tonumber(r[1]) ~= tonumber(ARGV[1]) or r[2] ~= ARGV[2] then return 0 end
426
+ redis.call('HSET', KEYS[1], 'retiredAt', ARGV[3])
427
+ redis.call('PEXPIRE', KEYS[1], ARGV[4])
428
+ return 1
429
+ `;
430
+ const FENCED_WRITE = `
431
+ local i = 1
432
+ while i <= #ARGV - 2 do
433
+ local n = tonumber(ARGV[i])
434
+ redis.call(unpack(ARGV, i + 1, i + n))
435
+ i = i + n + 1
436
+ end
437
+ return 1
438
+ `;
439
+ /**
440
+ * Runs `script`, first checking the run's claim against `check`'s fence in
441
+ * the same script when `check` is set. A stale fence throws
442
+ * `RunFenceConflictError` before the script does anything.
443
+ */
444
+ async function evalFenced(client, script, keys, args, check) {
445
+ if (!check) return client.eval(script, keys, args);
446
+ const { claims, fence, operation } = check;
447
+ const result = await client.eval(FENCE_GUARD + script, [...keys, runClaimKey(claims, fence.runId)], [
448
+ ...args,
449
+ String(fence.generation),
450
+ fence.ownerId
451
+ ]);
452
+ if (result === RUN_FENCE_CONFLICT) throw new _mastra_core_storage.RunFenceConflictError(fence, operation);
453
+ return result;
454
+ }
455
+ function toRunOwnershipRecord(runId, reply, now) {
456
+ const [, generation, ownerId, leaseExpiresAt] = reply;
457
+ if (!generation) return null;
458
+ const expiresAt = leaseExpiresAt ? Number(leaseExpiresAt) : null;
459
+ return {
460
+ runId,
461
+ generation: Number(generation),
462
+ ownerId: String(ownerId),
463
+ leaseExpiresAt: expiresAt === null ? null : new Date(expiresAt),
464
+ live: expiresAt !== null && expiresAt > now
465
+ };
466
+ }
467
+ async function evalOwner(client, script, runId, args, now) {
468
+ const raw = await client.eval(script, [runClaimKey(_mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS, runId)], args);
469
+ const reply = typeof raw === "string" ? JSON.parse(raw) : raw;
470
+ return {
471
+ applied: Number(reply[0]) === 1,
472
+ record: toRunOwnershipRecord(runId, reply, now)
473
+ };
474
+ }
475
+ async function claimRunOwnership(client, { runId, ownerId, leaseMs, force, expectedGeneration }) {
476
+ const now = Date.now();
477
+ const { applied, record } = await evalOwner(client, CLAIM, runId, [
478
+ ownerId,
479
+ force ? "1" : "0",
480
+ expectedGeneration === void 0 ? "" : String(expectedGeneration),
481
+ String(now),
482
+ String(now + leaseMs),
483
+ String(RUN_CLAIM_RETENTION_MS)
484
+ ], now);
485
+ return applied ? {
486
+ acquired: true,
487
+ record
488
+ } : {
489
+ acquired: false,
490
+ record
491
+ };
492
+ }
493
+ async function renewRunOwnership(client, { leaseMs, ...fence }) {
494
+ const now = Date.now();
495
+ const { applied, record } = await evalOwner(client, RENEW, fence.runId, [
496
+ String(fence.generation),
497
+ fence.ownerId,
498
+ String(now + leaseMs),
499
+ String(now),
500
+ String(RUN_CLAIM_RETENTION_MS)
501
+ ], now);
502
+ return applied ? {
503
+ renewed: true,
504
+ record
505
+ } : {
506
+ renewed: false,
507
+ record
508
+ };
509
+ }
510
+ async function releaseRunOwnership(client, fence) {
511
+ const released = await client.eval(RELEASE, [runClaimKey(_mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS, fence.runId)], [
512
+ String(fence.generation),
513
+ fence.ownerId,
514
+ String(Date.now()),
515
+ String(RUN_CLAIM_RETENTION_MS)
516
+ ]);
517
+ return Number(released) === 1;
518
+ }
519
+ async function getRunOwnership(client, runId) {
520
+ return (await evalOwner(client, READ, runId, [], Date.now())).record;
521
+ }
522
+ async function raiseRunFence(client, fence) {
523
+ const raised = await client.eval(RAISE, [runClaimKey(_mastra_core_storage.TABLE_MEMORY_RUN_FENCES, fence.runId)], [String(fence.generation), fence.ownerId]);
524
+ return Number(raised) === 1;
525
+ }
526
+ async function retireRunFence(client, fence) {
527
+ const retired = await client.eval(RETIRE, [runClaimKey(_mastra_core_storage.TABLE_MEMORY_RUN_FENCES, fence.runId)], [
528
+ String(fence.generation),
529
+ fence.ownerId,
530
+ String(Date.now()),
531
+ String(RUN_CLAIM_RETENTION_MS)
532
+ ]);
533
+ return Number(retired) === 1;
534
+ }
535
+ function serialize(value) {
536
+ return typeof value === "string" ? value : JSON.stringify(value);
537
+ }
538
+ /**
539
+ * Collects writes and applies them in one script that first checks the run's
540
+ * claim against the fence. A fence that is no longer current makes `exec()`
541
+ * throw `RunFenceConflictError` without writing anything. Scripts run
542
+ * atomically, so a takeover lands either before the batch (failing its check)
543
+ * or after it.
544
+ */
545
+ var FencedWriteBatch = class {
546
+ client;
547
+ check;
548
+ #keys = /* @__PURE__ */ new Set();
549
+ #args = [];
550
+ constructor(client, check) {
551
+ this.client = client;
552
+ this.check = check;
553
+ }
554
+ #add(key, ...command) {
555
+ this.#keys.add(key);
556
+ this.#args.push(String(command.length), ...command);
557
+ return this;
558
+ }
559
+ set(key, value) {
560
+ return this.#add(key, "SET", key, serialize(value));
561
+ }
562
+ del(key) {
563
+ return this.#add(key, "DEL", key);
564
+ }
565
+ zadd(key, { score, member }) {
566
+ return this.#add(key, "ZADD", key, String(score), member);
567
+ }
568
+ zrem(key, member) {
569
+ return this.#add(key, "ZREM", key, member);
570
+ }
571
+ async exec() {
572
+ await evalFenced(this.client, FENCED_WRITE, [...this.#keys], this.#args, this.check);
573
+ }
574
+ };
575
+ /** A pipeline, or a fenced batch when `check` is set. */
576
+ function writeBatch(client, check) {
577
+ return check ? new FencedWriteBatch(client, check) : client.pipeline();
578
+ }
579
+ /**
580
+ * Rejects a fenced write that turns out to have nothing to write when its
581
+ * fence is no longer current, as the write itself would have been rejected.
582
+ */
583
+ async function assertRunFence(client, check) {
584
+ if (check) await new FencedWriteBatch(client, check).exec();
585
+ }
586
+ //#endregion
354
587
  //#region src/storage/domains/memory/index.ts
355
588
  function getThreadMessagesKey(threadId) {
356
589
  return `thread:${threadId}:messages`;
@@ -374,10 +607,46 @@ var StoreMemoryUpstash = class extends _mastra_core_storage.MemoryStorage {
374
607
  this.client = client;
375
608
  this.#db = new UpstashDB({ client });
376
609
  }
610
+ supportsRunFencing() {
611
+ return true;
612
+ }
613
+ async raiseRunFence(fence) {
614
+ try {
615
+ return await raiseRunFence(this.client, fence);
616
+ } catch (error) {
617
+ throw new _mastra_core_error.MastraError({
618
+ id: (0, _mastra_core_storage.createStorageErrorId)("UPSTASH", "RAISE_RUN_FENCE", "FAILED"),
619
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
620
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
621
+ details: { runId: fence.runId }
622
+ }, error);
623
+ }
624
+ }
625
+ async retireRunFence(fence) {
626
+ try {
627
+ return await retireRunFence(this.client, fence);
628
+ } catch (error) {
629
+ throw new _mastra_core_error.MastraError({
630
+ id: (0, _mastra_core_storage.createStorageErrorId)("UPSTASH", "RETIRE_RUN_FENCE", "FAILED"),
631
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
632
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
633
+ details: { runId: fence.runId }
634
+ }, error);
635
+ }
636
+ }
637
+ #runFenceCheck(fence, operation) {
638
+ const resolved = (0, _mastra_core_storage.resolveRunFence)(this, fence);
639
+ return resolved && {
640
+ claims: _mastra_core_storage.TABLE_MEMORY_RUN_FENCES,
641
+ fence: resolved,
642
+ operation
643
+ };
644
+ }
377
645
  async dangerouslyClearAll() {
378
646
  await this.#db.deleteData({ tableName: _mastra_core_storage.TABLE_THREADS });
379
647
  await this.#db.deleteData({ tableName: _mastra_core_storage.TABLE_MESSAGES });
380
648
  await this.#db.deleteData({ tableName: _mastra_core_storage.TABLE_RESOURCES });
649
+ await this.#db.scanAndDelete(`${runClaimKey(_mastra_core_storage.TABLE_MEMORY_RUN_FENCES, "")}*`);
381
650
  }
382
651
  async getThreadById({ threadId, resourceId }) {
383
652
  try {
@@ -486,14 +755,16 @@ var StoreMemoryUpstash = class extends _mastra_core_storage.MemoryStorage {
486
755
  throw mastraError;
487
756
  }
488
757
  }
489
- async saveThread({ thread }) {
758
+ async saveThread({ thread, fence }) {
759
+ const check = this.#runFenceCheck(fence, "saveThread");
490
760
  try {
491
- await this.#db.insert({
492
- tableName: _mastra_core_storage.TABLE_THREADS,
493
- record: thread
494
- });
761
+ const { key, processedRecord } = processRecord(_mastra_core_storage.TABLE_THREADS, thread);
762
+ const batch = writeBatch(this.client, check);
763
+ batch.set(key, processedRecord);
764
+ await batch.exec();
495
765
  return thread;
496
766
  } catch (error) {
767
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
497
768
  const mastraError = new _mastra_core_error.MastraError({
498
769
  id: (0, _mastra_core_storage.createStorageErrorId)("UPSTASH", "SAVE_THREAD", "FAILED"),
499
770
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -505,7 +776,7 @@ var StoreMemoryUpstash = class extends _mastra_core_storage.MemoryStorage {
505
776
  throw mastraError;
506
777
  }
507
778
  }
508
- async updateThread({ id, title, metadata }) {
779
+ async updateThread({ id, title, metadata, fence }) {
509
780
  const thread = await this.getThreadById({ threadId: id });
510
781
  if (!thread) throw new _mastra_core_error.MastraError({
511
782
  id: (0, _mastra_core_storage.createStorageErrorId)("UPSTASH", "UPDATE_THREAD", "FAILED"),
@@ -525,9 +796,13 @@ var StoreMemoryUpstash = class extends _mastra_core_storage.MemoryStorage {
525
796
  updatedAt: now
526
797
  };
527
798
  try {
528
- await this.saveThread({ thread: updatedThread });
799
+ await this.saveThread({
800
+ thread: updatedThread,
801
+ fence
802
+ });
529
803
  return updatedThread;
530
804
  } catch (error) {
805
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
531
806
  throw new _mastra_core_error.MastraError({
532
807
  id: (0, _mastra_core_storage.createStorageErrorId)("UPSTASH", "UPDATE_THREAD", "FAILED"),
533
808
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -562,6 +837,7 @@ var StoreMemoryUpstash = class extends _mastra_core_storage.MemoryStorage {
562
837
  }
563
838
  async saveMessages(args) {
564
839
  const { messages } = args;
840
+ const check = this.#runFenceCheck(args.fence, "saveMessages");
565
841
  if (messages.length === 0) return { messages: [] };
566
842
  try {
567
843
  for (const message of messages) {
@@ -616,7 +892,7 @@ var StoreMemoryUpstash = class extends _mastra_core_storage.MemoryStorage {
616
892
  }, /* @__PURE__ */ new Error(`Thread ${targetThreadId} not found`));
617
893
  for (let i = 0; i < messagesWithIndex.length; i += batchSize) {
618
894
  const batch = messagesWithIndex.slice(i, i + batchSize);
619
- const pipeline = this.client.pipeline();
895
+ const pipeline = writeBatch(this.client, check);
620
896
  const batchTouchedThreadIds = /* @__PURE__ */ new Set();
621
897
  const batchExistingThreadIds = existingThreadIds.slice(i, i + batch.length);
622
898
  for (const [batchIndex, message] of batch.entries()) {
@@ -931,6 +1207,9 @@ var StoreMemoryUpstash = class extends _mastra_core_storage.MemoryStorage {
931
1207
  }
932
1208
  }
933
1209
  async saveResource({ resource }) {
1210
+ return this.#saveResource(resource, void 0);
1211
+ }
1212
+ async #saveResource(resource, check) {
934
1213
  try {
935
1214
  const key = `${_mastra_core_storage.TABLE_RESOURCES}:${resource.id}`;
936
1215
  const serializedResource = {
@@ -939,14 +1218,17 @@ var StoreMemoryUpstash = class extends _mastra_core_storage.MemoryStorage {
939
1218
  createdAt: resource.createdAt.toISOString(),
940
1219
  updatedAt: resource.updatedAt.toISOString()
941
1220
  };
942
- await this.client.set(key, serializedResource);
1221
+ const batch = writeBatch(this.client, check);
1222
+ batch.set(key, serializedResource);
1223
+ await batch.exec();
943
1224
  return resource;
944
1225
  } catch (error) {
945
- this.logger.error("Error saving resource:", error);
1226
+ if (!(0, _mastra_core_storage.isRunFenceConflictError)(error)) this.logger.error("Error saving resource:", error);
946
1227
  throw error;
947
1228
  }
948
1229
  }
949
- async updateResource({ resourceId, workingMemory, metadata }) {
1230
+ async updateResource({ resourceId, workingMemory, metadata, fence }) {
1231
+ const check = this.#runFenceCheck(fence, "updateResource");
950
1232
  try {
951
1233
  const existingResource = await this.getResourceById({ resourceId });
952
1234
  if (!existingResource) {
@@ -957,7 +1239,7 @@ var StoreMemoryUpstash = class extends _mastra_core_storage.MemoryStorage {
957
1239
  createdAt: /* @__PURE__ */ new Date(),
958
1240
  updatedAt: /* @__PURE__ */ new Date()
959
1241
  };
960
- return this.saveResource({ resource: newResource });
1242
+ return await this.#saveResource(newResource, check);
961
1243
  }
962
1244
  const updatedResource = {
963
1245
  ...existingResource,
@@ -968,16 +1250,17 @@ var StoreMemoryUpstash = class extends _mastra_core_storage.MemoryStorage {
968
1250
  },
969
1251
  updatedAt: /* @__PURE__ */ new Date()
970
1252
  };
971
- await this.saveResource({ resource: updatedResource });
1253
+ await this.#saveResource(updatedResource, check);
972
1254
  return updatedResource;
973
1255
  } catch (error) {
974
- this.logger.error("Error updating resource:", error);
1256
+ if (!(0, _mastra_core_storage.isRunFenceConflictError)(error)) this.logger.error("Error updating resource:", error);
975
1257
  throw error;
976
1258
  }
977
1259
  }
978
1260
  async updateMessages(args) {
979
1261
  const { messages } = args;
980
1262
  if (messages.length === 0) return [];
1263
+ const check = this.#runFenceCheck(args.fence, "updateMessages");
981
1264
  try {
982
1265
  const messageIds = messages.map((m) => m.id);
983
1266
  const updatesById = new Map(messages.map((message) => [message.id, message]));
@@ -1030,7 +1313,10 @@ var StoreMemoryUpstash = class extends _mastra_core_storage.MemoryStorage {
1030
1313
  for (const [messageId, threadId] of Object.entries(backfillIndexValues)) backfillPipeline.set(getMessageIndexKey(messageId), threadId);
1031
1314
  await backfillPipeline.exec();
1032
1315
  }
1033
- if (existingMessages.length === 0) return [];
1316
+ if (existingMessages.length === 0) {
1317
+ await assertRunFence(this.client, check);
1318
+ return [];
1319
+ }
1034
1320
  const threadIdsToUpdate = /* @__PURE__ */ new Set();
1035
1321
  const destinationThreadIds = /* @__PURE__ */ new Set();
1036
1322
  for (const existingMessage of existingMessages) {
@@ -1062,7 +1348,7 @@ var StoreMemoryUpstash = class extends _mastra_core_storage.MemoryStorage {
1062
1348
  domain: _mastra_core_error.ErrorDomain.STORAGE,
1063
1349
  category: _mastra_core_error.ErrorCategory.USER
1064
1350
  }, /* @__PURE__ */ new Error(`Thread ${destinationThreadId} not found`));
1065
- const pipeline = this.client.pipeline();
1351
+ const pipeline = writeBatch(this.client, check);
1066
1352
  for (const existingMessage of existingMessages) {
1067
1353
  const updatePayload = updatesById.get(existingMessage.id);
1068
1354
  if (!updatePayload) continue;
@@ -1132,8 +1418,9 @@ var StoreMemoryUpstash = class extends _mastra_core_storage.MemoryStorage {
1132
1418
  }, error);
1133
1419
  }
1134
1420
  }
1135
- async deleteMessages(messageIds) {
1421
+ async deleteMessages(messageIds, options) {
1136
1422
  if (!messageIds || messageIds.length === 0) return;
1423
+ const check = this.#runFenceCheck(options?.fence, "deleteMessages");
1137
1424
  try {
1138
1425
  const threadIds = /* @__PURE__ */ new Set();
1139
1426
  const messageKeys = [];
@@ -1169,8 +1456,11 @@ var StoreMemoryUpstash = class extends _mastra_core_storage.MemoryStorage {
1169
1456
  }
1170
1457
  }
1171
1458
  }
1172
- if (messageKeys.length === 0) return;
1173
- const pipeline = this.client.pipeline();
1459
+ if (messageKeys.length === 0) {
1460
+ await assertRunFence(this.client, check);
1461
+ return;
1462
+ }
1463
+ const pipeline = writeBatch(this.client, check);
1174
1464
  for (const key of messageKeys) pipeline.del(key);
1175
1465
  for (const messageId of foundMessageIds) pipeline.del(getMessageIndexKey(messageId));
1176
1466
  if (threadIds.size > 0) for (const threadId of threadIds) {
@@ -1186,6 +1476,7 @@ var StoreMemoryUpstash = class extends _mastra_core_storage.MemoryStorage {
1186
1476
  }
1187
1477
  await pipeline.exec();
1188
1478
  } catch (error) {
1479
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
1189
1480
  throw new _mastra_core_error.MastraError({
1190
1481
  id: (0, _mastra_core_storage.createStorageErrorId)("UPSTASH", "DELETE_MESSAGES", "FAILED"),
1191
1482
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -1613,8 +1904,56 @@ var WorkflowsUpstash = class extends _mastra_core_storage.WorkflowsStorage {
1613
1904
  resourceId: row.resourceId
1614
1905
  };
1615
1906
  }
1907
+ supportsRunFencing() {
1908
+ return true;
1909
+ }
1616
1910
  async dangerouslyClearAll() {
1617
1911
  await this.#db.deleteData({ tableName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT });
1912
+ await this.#db.scanAndDelete(`${runClaimKey(_mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS, "")}*`);
1913
+ }
1914
+ #runFenceCheck(fence, runId, operation) {
1915
+ const resolved = (0, _mastra_core_storage.resolveRunFence)(this, fence, runId);
1916
+ return resolved && {
1917
+ claims: _mastra_core_storage.TABLE_WORKFLOW_RUN_OWNERS,
1918
+ fence: resolved,
1919
+ operation
1920
+ };
1921
+ }
1922
+ #ownershipError(operation, runId, error) {
1923
+ return new _mastra_core_error.MastraError({
1924
+ id: (0, _mastra_core_storage.createStorageErrorId)("UPSTASH", operation, "FAILED"),
1925
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
1926
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
1927
+ details: { runId }
1928
+ }, error);
1929
+ }
1930
+ async claimRunOwnership(args) {
1931
+ try {
1932
+ return await claimRunOwnership(this.client, args);
1933
+ } catch (error) {
1934
+ throw this.#ownershipError("CLAIM_RUN_OWNERSHIP", args.runId, error);
1935
+ }
1936
+ }
1937
+ async renewRunOwnership(args) {
1938
+ try {
1939
+ return await renewRunOwnership(this.client, args);
1940
+ } catch (error) {
1941
+ throw this.#ownershipError("RENEW_RUN_OWNERSHIP", args.runId, error);
1942
+ }
1943
+ }
1944
+ async releaseRunOwnership(fence) {
1945
+ try {
1946
+ return await releaseRunOwnership(this.client, fence);
1947
+ } catch (error) {
1948
+ throw this.#ownershipError("RELEASE_RUN_OWNERSHIP", fence.runId, error);
1949
+ }
1950
+ }
1951
+ async getRunOwnership({ runId }) {
1952
+ try {
1953
+ return await getRunOwnership(this.client, runId);
1954
+ } catch (error) {
1955
+ throw this.#ownershipError("GET_RUN_OWNERSHIP", runId, error);
1956
+ }
1618
1957
  }
1619
1958
  async getWorkflowRunRecord({ namespace, runId, resourceId, workflowName }) {
1620
1959
  if (workflowName) {
@@ -1642,7 +1981,8 @@ var WorkflowsUpstash = class extends _mastra_core_storage.WorkflowsStorage {
1642
1981
  return this.client.get(key);
1643
1982
  }))).find((w) => w?.run_id === runId && (!workflowName || w.workflow_name === workflowName) && (!resourceId || w.resourceId === resourceId)) ?? null;
1644
1983
  }
1645
- async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state }) {
1984
+ async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state, fence }) {
1985
+ const check = this.#runFenceCheck(fence, runId, "updateWorkflowResults");
1646
1986
  try {
1647
1987
  const key = getKey(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT, {
1648
1988
  namespace: "workflows",
@@ -1650,7 +1990,7 @@ var WorkflowsUpstash = class extends _mastra_core_storage.WorkflowsStorage {
1650
1990
  run_id: runId
1651
1991
  });
1652
1992
  const now = (/* @__PURE__ */ new Date()).toISOString();
1653
- const resultJson = await this.client.eval(`
1993
+ const resultJson = await evalFenced(this.client, `
1654
1994
  local key = KEYS[1]
1655
1995
  local stepId = ARGV[1]
1656
1996
  local resultJson = ARGV[2]
@@ -1740,7 +2080,7 @@ var WorkflowsUpstash = class extends _mastra_core_storage.WorkflowsStorage {
1740
2080
  runId,
1741
2081
  String(Date.now()),
1742
2082
  state === void 0 ? "" : JSON.stringify(state)
1743
- ]);
2083
+ ], check);
1744
2084
  let data;
1745
2085
  if (typeof resultJson === "string") data = JSON.parse(resultJson);
1746
2086
  else data = resultJson;
@@ -1759,7 +2099,8 @@ var WorkflowsUpstash = class extends _mastra_core_storage.WorkflowsStorage {
1759
2099
  }, error);
1760
2100
  }
1761
2101
  }
1762
- async updateWorkflowState({ workflowName, runId, opts }) {
2102
+ async updateWorkflowState({ workflowName, runId, opts, fence }) {
2103
+ const check = this.#runFenceCheck(fence, runId, "updateWorkflowState");
1763
2104
  try {
1764
2105
  const key = getKey(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT, {
1765
2106
  namespace: "workflows",
@@ -1824,11 +2165,11 @@ var WorkflowsUpstash = class extends _mastra_core_storage.WorkflowsStorage {
1824
2165
  `;
1825
2166
  const { expectedStatus, ...state } = opts;
1826
2167
  const expectedStatusJson = expectedStatus === void 0 ? "" : JSON.stringify(Array.isArray(expectedStatus) ? expectedStatus : [expectedStatus]);
1827
- const resultJson = await this.client.eval(luaScript, [key], [
2168
+ const resultJson = await evalFenced(this.client, luaScript, [key], [
1828
2169
  JSON.stringify(state),
1829
2170
  now,
1830
2171
  expectedStatusJson
1831
- ]);
2172
+ ], check);
1832
2173
  if (!resultJson) return;
1833
2174
  let data;
1834
2175
  if (typeof resultJson === "string") data = JSON.parse(resultJson);
@@ -1849,6 +2190,7 @@ var WorkflowsUpstash = class extends _mastra_core_storage.WorkflowsStorage {
1849
2190
  }
1850
2191
  async persistWorkflowSnapshot(params) {
1851
2192
  const { namespace = "workflows", workflowName, runId, resourceId, snapshot, createdAt, updatedAt } = params;
2193
+ const check = this.#runFenceCheck(params.fence, runId, "persistWorkflowSnapshot");
1852
2194
  try {
1853
2195
  const now = /* @__PURE__ */ new Date();
1854
2196
  const key = getKey(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT, {
@@ -1866,7 +2208,7 @@ var WorkflowsUpstash = class extends _mastra_core_storage.WorkflowsStorage {
1866
2208
  createdAt: (createdAt ?? now).toISOString(),
1867
2209
  updatedAt: (updatedAt ?? now).toISOString()
1868
2210
  };
1869
- await this.client.eval(`
2211
+ await evalFenced(this.client, `
1870
2212
  local existing = redis.call("GET", KEYS[1])
1871
2213
  local next = cjson.decode(ARGV[1])
1872
2214
  if existing then
@@ -1877,8 +2219,9 @@ var WorkflowsUpstash = class extends _mastra_core_storage.WorkflowsStorage {
1877
2219
  end
1878
2220
  redis.call("SET", KEYS[1], cjson.encode(next))
1879
2221
  return 1
1880
- `, [key], [JSON.stringify(record)]);
2222
+ `, [key], [JSON.stringify(record)], check);
1881
2223
  } catch (error) {
2224
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
1882
2225
  throw new _mastra_core_error.MastraError({
1883
2226
  id: (0, _mastra_core_storage.createStorageErrorId)("UPSTASH", "PERSIST_WORKFLOW_SNAPSHOT", "FAILED"),
1884
2227
  domain: _mastra_core_error.ErrorDomain.STORAGE,
@@ -1936,7 +2279,8 @@ var WorkflowsUpstash = class extends _mastra_core_storage.WorkflowsStorage {
1936
2279
  }, error);
1937
2280
  }
1938
2281
  }
1939
- async deleteWorkflowRunById({ runId, workflowName }) {
2282
+ async deleteWorkflowRunById({ runId, workflowName, fence }) {
2283
+ const check = this.#runFenceCheck(fence, runId, "deleteWorkflowRunById");
1940
2284
  try {
1941
2285
  const record = await this.getWorkflowRunRecord({
1942
2286
  namespace: "workflows",
@@ -1949,8 +2293,11 @@ var WorkflowsUpstash = class extends _mastra_core_storage.WorkflowsStorage {
1949
2293
  run_id: runId,
1950
2294
  ...record?.resourceId ? { resourceId: record.resourceId } : {}
1951
2295
  });
1952
- await this.client.del(key);
2296
+ const batch = writeBatch(this.client, check);
2297
+ batch.del(key);
2298
+ await batch.exec();
1953
2299
  } catch (error) {
2300
+ if ((0, _mastra_core_storage.isRunFenceConflictError)(error)) throw error;
1954
2301
  throw new _mastra_core_error.MastraError({
1955
2302
  id: (0, _mastra_core_storage.createStorageErrorId)("UPSTASH", "DELETE_WORKFLOW_RUN_BY_ID", "FAILED"),
1956
2303
  domain: _mastra_core_error.ErrorDomain.STORAGE,