@mastra/upstash 0.11.1-alpha.0 → 0.11.1-alpha.2

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.
@@ -1,11 +1,13 @@
1
1
  import { MessageList } from '@mastra/core/agent';
2
2
  import type { MastraMessageContentV2, MastraMessageV2 } from '@mastra/core/agent';
3
+ import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
3
4
  import type { MetricResult, TestInfo } from '@mastra/core/eval';
4
5
  import type { StorageThreadType, MastraMessageV1 } from '@mastra/core/memory';
5
6
  import {
6
7
  MastraStorage,
7
8
  TABLE_MESSAGES,
8
9
  TABLE_THREADS,
10
+ TABLE_RESOURCES,
9
11
  TABLE_WORKFLOW_SNAPSHOT,
10
12
  TABLE_EVALS,
11
13
  TABLE_TRACES,
@@ -14,6 +16,7 @@ import type {
14
16
  TABLE_NAMES,
15
17
  StorageColumn,
16
18
  StorageGetMessagesArg,
19
+ StorageResourceType,
17
20
  EvalRow,
18
21
  WorkflowRuns,
19
22
  WorkflowRun,
@@ -42,9 +45,11 @@ export class UpstashStore extends MastraStorage {
42
45
 
43
46
  public get supports(): {
44
47
  selectByIncludeResourceScope: boolean;
48
+ resourceWorkingMemory: boolean;
45
49
  } {
46
50
  return {
47
51
  selectByIncludeResourceScope: true,
52
+ resourceWorkingMemory: true,
48
53
  };
49
54
  }
50
55
 
@@ -271,7 +276,17 @@ export class UpstashStore extends MastraStorage {
271
276
  // Transform to EvalRow format
272
277
  return filteredEvals.map(record => this.transformEvalRecord(record));
273
278
  } catch (error) {
274
- console.error('Failed to get evals for the specified agent:', error);
279
+ const mastraError = new MastraError(
280
+ {
281
+ id: 'STORAGE_UPSTASH_STORAGE_GET_EVALS_BY_AGENT_NAME_FAILED',
282
+ domain: ErrorDomain.STORAGE,
283
+ category: ErrorCategory.THIRD_PARTY,
284
+ details: { agentName },
285
+ },
286
+ error,
287
+ );
288
+ this.logger?.trackException(mastraError);
289
+ this.logger.error(mastraError.toString());
275
290
  return [];
276
291
  }
277
292
  }
@@ -286,8 +301,19 @@ export class UpstashStore extends MastraStorage {
286
301
  end: args.toDate,
287
302
  };
288
303
  }
289
- const { traces } = await this.getTracesPaginated(args);
290
- return traces;
304
+ try {
305
+ const { traces } = await this.getTracesPaginated(args);
306
+ return traces;
307
+ } catch (error) {
308
+ throw new MastraError(
309
+ {
310
+ id: 'STORAGE_UPSTASH_STORAGE_GET_TRACES_FAILED',
311
+ domain: ErrorDomain.STORAGE,
312
+ category: ErrorCategory.THIRD_PARTY,
313
+ },
314
+ error,
315
+ );
316
+ }
291
317
  }
292
318
 
293
319
  public async getTracesPaginated(
@@ -389,7 +415,20 @@ export class UpstashStore extends MastraStorage {
389
415
  hasMore,
390
416
  };
391
417
  } catch (error) {
392
- console.error('Failed to get traces:', error);
418
+ const mastraError = new MastraError(
419
+ {
420
+ id: 'STORAGE_UPSTASH_STORAGE_GET_TRACES_PAGINATED_FAILED',
421
+ domain: ErrorDomain.STORAGE,
422
+ category: ErrorCategory.THIRD_PARTY,
423
+ details: {
424
+ name: args.name || '',
425
+ scope: args.scope || '',
426
+ },
427
+ },
428
+ error,
429
+ );
430
+ this.logger?.trackException(mastraError);
431
+ this.logger.error(mastraError.toString());
393
432
  return {
394
433
  traces: [],
395
434
  total: 0,
@@ -409,7 +448,21 @@ export class UpstashStore extends MastraStorage {
409
448
  }): Promise<void> {
410
449
  // Redis is schemaless, so we don't need to create tables
411
450
  // But we can store the schema for reference
412
- await this.redis.set(`schema:${tableName}`, schema);
451
+ try {
452
+ await this.redis.set(`schema:${tableName}`, schema);
453
+ } catch (error) {
454
+ throw new MastraError(
455
+ {
456
+ id: 'STORAGE_UPSTASH_STORAGE_CREATE_TABLE_FAILED',
457
+ domain: ErrorDomain.STORAGE,
458
+ category: ErrorCategory.THIRD_PARTY,
459
+ details: {
460
+ tableName,
461
+ },
462
+ },
463
+ error,
464
+ );
465
+ }
413
466
  }
414
467
 
415
468
  /**
@@ -428,13 +481,41 @@ export class UpstashStore extends MastraStorage {
428
481
 
429
482
  async clearTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
430
483
  const pattern = `${tableName}:*`;
431
- await this.scanAndDelete(pattern);
484
+ try {
485
+ await this.scanAndDelete(pattern);
486
+ } catch (error) {
487
+ throw new MastraError(
488
+ {
489
+ id: 'STORAGE_UPSTASH_STORAGE_CLEAR_TABLE_FAILED',
490
+ domain: ErrorDomain.STORAGE,
491
+ category: ErrorCategory.THIRD_PARTY,
492
+ details: {
493
+ tableName,
494
+ },
495
+ },
496
+ error,
497
+ );
498
+ }
432
499
  }
433
500
 
434
501
  async insert({ tableName, record }: { tableName: TABLE_NAMES; record: Record<string, any> }): Promise<void> {
435
502
  const { key, processedRecord } = this.processRecord(tableName, record);
436
503
 
437
- await this.redis.set(key, processedRecord);
504
+ try {
505
+ await this.redis.set(key, processedRecord);
506
+ } catch (error) {
507
+ throw new MastraError(
508
+ {
509
+ id: 'STORAGE_UPSTASH_STORAGE_INSERT_FAILED',
510
+ domain: ErrorDomain.STORAGE,
511
+ category: ErrorCategory.THIRD_PARTY,
512
+ details: {
513
+ tableName,
514
+ },
515
+ },
516
+ error,
517
+ );
518
+ }
438
519
  }
439
520
 
440
521
  async batchInsert(input: { tableName: TABLE_NAMES; records: Record<string, any>[] }): Promise<void> {
@@ -442,37 +523,79 @@ export class UpstashStore extends MastraStorage {
442
523
  if (!records.length) return;
443
524
 
444
525
  const batchSize = 1000;
445
- for (let i = 0; i < records.length; i += batchSize) {
446
- const batch = records.slice(i, i + batchSize);
447
- const pipeline = this.redis.pipeline();
448
- for (const record of batch) {
449
- const { key, processedRecord } = this.processRecord(tableName, record);
450
- pipeline.set(key, processedRecord);
526
+ try {
527
+ for (let i = 0; i < records.length; i += batchSize) {
528
+ const batch = records.slice(i, i + batchSize);
529
+ const pipeline = this.redis.pipeline();
530
+ for (const record of batch) {
531
+ const { key, processedRecord } = this.processRecord(tableName, record);
532
+ pipeline.set(key, processedRecord);
533
+ }
534
+ await pipeline.exec();
451
535
  }
452
- await pipeline.exec();
536
+ } catch (error) {
537
+ throw new MastraError(
538
+ {
539
+ id: 'STORAGE_UPSTASH_STORAGE_BATCH_INSERT_FAILED',
540
+ domain: ErrorDomain.STORAGE,
541
+ category: ErrorCategory.THIRD_PARTY,
542
+ details: {
543
+ tableName,
544
+ },
545
+ },
546
+ error,
547
+ );
453
548
  }
454
549
  }
455
550
 
456
551
  async load<R>({ tableName, keys }: { tableName: TABLE_NAMES; keys: Record<string, string> }): Promise<R | null> {
457
552
  const key = this.getKey(tableName, keys);
458
- const data = await this.redis.get<R>(key);
459
- return data || null;
553
+ try {
554
+ const data = await this.redis.get<R>(key);
555
+ return data || null;
556
+ } catch (error) {
557
+ throw new MastraError(
558
+ {
559
+ id: 'STORAGE_UPSTASH_STORAGE_LOAD_FAILED',
560
+ domain: ErrorDomain.STORAGE,
561
+ category: ErrorCategory.THIRD_PARTY,
562
+ details: {
563
+ tableName,
564
+ },
565
+ },
566
+ error,
567
+ );
568
+ }
460
569
  }
461
570
 
462
571
  async getThreadById({ threadId }: { threadId: string }): Promise<StorageThreadType | null> {
463
- const thread = await this.load<StorageThreadType>({
464
- tableName: TABLE_THREADS,
465
- keys: { id: threadId },
466
- });
572
+ try {
573
+ const thread = await this.load<StorageThreadType>({
574
+ tableName: TABLE_THREADS,
575
+ keys: { id: threadId },
576
+ });
467
577
 
468
- if (!thread) return null;
578
+ if (!thread) return null;
469
579
 
470
- return {
471
- ...thread,
472
- createdAt: this.ensureDate(thread.createdAt)!,
473
- updatedAt: this.ensureDate(thread.updatedAt)!,
474
- metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
475
- };
580
+ return {
581
+ ...thread,
582
+ createdAt: this.ensureDate(thread.createdAt)!,
583
+ updatedAt: this.ensureDate(thread.updatedAt)!,
584
+ metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
585
+ };
586
+ } catch (error) {
587
+ throw new MastraError(
588
+ {
589
+ id: 'STORAGE_UPSTASH_STORAGE_GET_THREAD_BY_ID_FAILED',
590
+ domain: ErrorDomain.STORAGE,
591
+ category: ErrorCategory.THIRD_PARTY,
592
+ details: {
593
+ threadId,
594
+ },
595
+ },
596
+ error,
597
+ );
598
+ }
476
599
  }
477
600
 
478
601
  /**
@@ -507,7 +630,19 @@ export class UpstashStore extends MastraStorage {
507
630
  allThreads.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
508
631
  return allThreads;
509
632
  } catch (error) {
510
- console.error('Error in getThreadsByResourceId:', error);
633
+ const mastraError = new MastraError(
634
+ {
635
+ id: 'STORAGE_UPSTASH_STORAGE_GET_THREADS_BY_RESOURCE_ID_FAILED',
636
+ domain: ErrorDomain.STORAGE,
637
+ category: ErrorCategory.THIRD_PARTY,
638
+ details: {
639
+ resourceId,
640
+ },
641
+ },
642
+ error,
643
+ );
644
+ this.logger?.trackException(mastraError);
645
+ this.logger.error(mastraError.toString());
511
646
  return [];
512
647
  }
513
648
  }
@@ -536,7 +671,21 @@ export class UpstashStore extends MastraStorage {
536
671
  hasMore,
537
672
  };
538
673
  } catch (error) {
539
- console.error('Error in getThreadsByResourceIdPaginated:', error);
674
+ const mastraError = new MastraError(
675
+ {
676
+ id: 'STORAGE_UPSTASH_STORAGE_GET_THREADS_BY_RESOURCE_ID_PAGINATED_FAILED',
677
+ domain: ErrorDomain.STORAGE,
678
+ category: ErrorCategory.THIRD_PARTY,
679
+ details: {
680
+ resourceId,
681
+ page,
682
+ perPage,
683
+ },
684
+ },
685
+ error,
686
+ );
687
+ this.logger?.trackException(mastraError);
688
+ this.logger.error(mastraError.toString());
540
689
  return {
541
690
  threads: [],
542
691
  total: 0,
@@ -548,11 +697,28 @@ export class UpstashStore extends MastraStorage {
548
697
  }
549
698
 
550
699
  async saveThread({ thread }: { thread: StorageThreadType }): Promise<StorageThreadType> {
551
- await this.insert({
552
- tableName: TABLE_THREADS,
553
- record: thread,
554
- });
555
- return thread;
700
+ try {
701
+ await this.insert({
702
+ tableName: TABLE_THREADS,
703
+ record: thread,
704
+ });
705
+ return thread;
706
+ } catch (error) {
707
+ const mastraError = new MastraError(
708
+ {
709
+ id: 'STORAGE_UPSTASH_STORAGE_SAVE_THREAD_FAILED',
710
+ domain: ErrorDomain.STORAGE,
711
+ category: ErrorCategory.THIRD_PARTY,
712
+ details: {
713
+ threadId: thread.id,
714
+ },
715
+ },
716
+ error,
717
+ );
718
+ this.logger?.trackException(mastraError);
719
+ this.logger.error(mastraError.toString());
720
+ throw mastraError;
721
+ }
556
722
  }
557
723
 
558
724
  async updateThread({
@@ -566,7 +732,15 @@ export class UpstashStore extends MastraStorage {
566
732
  }): Promise<StorageThreadType> {
567
733
  const thread = await this.getThreadById({ threadId: id });
568
734
  if (!thread) {
569
- throw new Error(`Thread ${id} not found`);
735
+ throw new MastraError({
736
+ id: 'STORAGE_UPSTASH_STORAGE_UPDATE_THREAD_FAILED',
737
+ domain: ErrorDomain.STORAGE,
738
+ category: ErrorCategory.USER,
739
+ text: `Thread ${id} not found`,
740
+ details: {
741
+ threadId: id,
742
+ },
743
+ });
570
744
  }
571
745
 
572
746
  const updatedThread = {
@@ -578,30 +752,58 @@ export class UpstashStore extends MastraStorage {
578
752
  },
579
753
  };
580
754
 
581
- await this.saveThread({ thread: updatedThread });
582
- return updatedThread;
755
+ try {
756
+ await this.saveThread({ thread: updatedThread });
757
+ return updatedThread;
758
+ } catch (error) {
759
+ throw new MastraError(
760
+ {
761
+ id: 'STORAGE_UPSTASH_STORAGE_UPDATE_THREAD_FAILED',
762
+ domain: ErrorDomain.STORAGE,
763
+ category: ErrorCategory.THIRD_PARTY,
764
+ details: {
765
+ threadId: id,
766
+ },
767
+ },
768
+ error,
769
+ );
770
+ }
583
771
  }
584
772
 
585
773
  async deleteThread({ threadId }: { threadId: string }): Promise<void> {
586
774
  // Delete thread metadata and sorted set
587
775
  const threadKey = this.getKey(TABLE_THREADS, { id: threadId });
588
776
  const threadMessagesKey = this.getThreadMessagesKey(threadId);
589
- const messageIds: string[] = await this.redis.zrange(threadMessagesKey, 0, -1);
777
+ try {
778
+ const messageIds: string[] = await this.redis.zrange(threadMessagesKey, 0, -1);
590
779
 
591
- const pipeline = this.redis.pipeline();
592
- pipeline.del(threadKey);
593
- pipeline.del(threadMessagesKey);
780
+ const pipeline = this.redis.pipeline();
781
+ pipeline.del(threadKey);
782
+ pipeline.del(threadMessagesKey);
594
783
 
595
- for (let i = 0; i < messageIds.length; i++) {
596
- const messageId = messageIds[i];
597
- const messageKey = this.getMessageKey(threadId, messageId as string);
598
- pipeline.del(messageKey);
599
- }
784
+ for (let i = 0; i < messageIds.length; i++) {
785
+ const messageId = messageIds[i];
786
+ const messageKey = this.getMessageKey(threadId, messageId as string);
787
+ pipeline.del(messageKey);
788
+ }
600
789
 
601
- await pipeline.exec();
790
+ await pipeline.exec();
602
791
 
603
- // Bulk delete all message keys for this thread if any remain
604
- await this.scanAndDelete(this.getMessageKey(threadId, '*'));
792
+ // Bulk delete all message keys for this thread if any remain
793
+ await this.scanAndDelete(this.getMessageKey(threadId, '*'));
794
+ } catch (error) {
795
+ throw new MastraError(
796
+ {
797
+ id: 'STORAGE_UPSTASH_STORAGE_DELETE_THREAD_FAILED',
798
+ domain: ErrorDomain.STORAGE,
799
+ category: ErrorCategory.THIRD_PARTY,
800
+ details: {
801
+ threadId,
802
+ },
803
+ },
804
+ error,
805
+ );
806
+ }
605
807
  }
606
808
 
607
809
  async saveMessages(args: { messages: MastraMessageV1[]; format?: undefined | 'v1' }): Promise<MastraMessageV1[]>;
@@ -613,14 +815,25 @@ export class UpstashStore extends MastraStorage {
613
815
  if (messages.length === 0) return [];
614
816
 
615
817
  const threadId = messages[0]?.threadId;
616
- if (!threadId) {
617
- throw new Error('Thread ID is required');
618
- }
818
+ try {
819
+ if (!threadId) {
820
+ throw new Error('Thread ID is required');
821
+ }
619
822
 
620
- // Check if thread exists
621
- const thread = await this.getThreadById({ threadId });
622
- if (!thread) {
623
- throw new Error(`Thread ${threadId} not found`);
823
+ // Check if thread exists
824
+ const thread = await this.getThreadById({ threadId });
825
+ if (!thread) {
826
+ throw new Error(`Thread ${threadId} not found`);
827
+ }
828
+ } catch (error) {
829
+ throw new MastraError(
830
+ {
831
+ id: 'STORAGE_UPSTASH_STORAGE_SAVE_MESSAGES_INVALID_ARGS',
832
+ domain: ErrorDomain.STORAGE,
833
+ category: ErrorCategory.USER,
834
+ },
835
+ error,
836
+ );
624
837
  }
625
838
 
626
839
  // Add an index to each message to maintain order
@@ -633,41 +846,76 @@ export class UpstashStore extends MastraStorage {
633
846
  const threadKey = this.getKey(TABLE_THREADS, { id: threadId });
634
847
  const existingThread = await this.redis.get<StorageThreadType>(threadKey);
635
848
 
636
- const batchSize = 1000;
637
- for (let i = 0; i < messagesWithIndex.length; i += batchSize) {
638
- const batch = messagesWithIndex.slice(i, i + batchSize);
639
- const pipeline = this.redis.pipeline();
849
+ try {
850
+ const batchSize = 1000;
851
+ for (let i = 0; i < messagesWithIndex.length; i += batchSize) {
852
+ const batch = messagesWithIndex.slice(i, i + batchSize);
853
+ const pipeline = this.redis.pipeline();
854
+
855
+ for (const message of batch) {
856
+ const key = this.getMessageKey(message.threadId!, message.id);
857
+ const createdAtScore = new Date(message.createdAt).getTime();
858
+ const score = message._index !== undefined ? message._index : createdAtScore;
859
+
860
+ // Check if this message id exists in another thread
861
+ const existingKeyPattern = this.getMessageKey('*', message.id);
862
+ const keys = await this.scanKeys(existingKeyPattern);
863
+
864
+ if (keys.length > 0) {
865
+ const pipeline2 = this.redis.pipeline();
866
+ keys.forEach(key => pipeline2.get(key));
867
+ const results = await pipeline2.exec();
868
+ const existingMessages = results.filter(
869
+ (msg): msg is MastraMessageV2 | MastraMessageV1 => msg !== null,
870
+ ) as (MastraMessageV2 | MastraMessageV1)[];
871
+ for (const existingMessage of existingMessages) {
872
+ const existingMessageKey = this.getMessageKey(existingMessage.threadId!, existingMessage.id);
873
+ if (existingMessage && existingMessage.threadId !== message.threadId) {
874
+ pipeline.del(existingMessageKey);
875
+ // Remove from old thread's sorted set
876
+ pipeline.zrem(this.getThreadMessagesKey(existingMessage.threadId!), existingMessage.id);
877
+ }
878
+ }
879
+ }
640
880
 
641
- for (const message of batch) {
642
- const key = this.getMessageKey(message.threadId!, message.id);
643
- const createdAtScore = new Date(message.createdAt).getTime();
644
- const score = message._index !== undefined ? message._index : createdAtScore;
881
+ // Store the message data
882
+ pipeline.set(key, message);
645
883
 
646
- // Store the message data
647
- pipeline.set(key, message);
884
+ // Add to sorted set for this thread
885
+ pipeline.zadd(this.getThreadMessagesKey(message.threadId!), {
886
+ score,
887
+ member: message.id,
888
+ });
889
+ }
648
890
 
649
- // Add to sorted set for this thread
650
- pipeline.zadd(this.getThreadMessagesKey(message.threadId!), {
651
- score,
652
- member: message.id,
653
- });
654
- }
891
+ // Update the thread's updatedAt field (only in the first batch)
892
+ if (i === 0 && existingThread) {
893
+ const updatedThread = {
894
+ ...existingThread,
895
+ updatedAt: new Date(),
896
+ };
897
+ pipeline.set(threadKey, this.processRecord(TABLE_THREADS, updatedThread).processedRecord);
898
+ }
655
899
 
656
- // Update the thread's updatedAt field (only in the first batch)
657
- if (i === 0 && existingThread) {
658
- const updatedThread = {
659
- ...existingThread,
660
- updatedAt: new Date(),
661
- };
662
- pipeline.set(threadKey, this.processRecord(TABLE_THREADS, updatedThread).processedRecord);
900
+ await pipeline.exec();
663
901
  }
664
902
 
665
- await pipeline.exec();
903
+ const list = new MessageList().add(messages, 'memory');
904
+ if (format === `v2`) return list.get.all.v2();
905
+ return list.get.all.v1();
906
+ } catch (error) {
907
+ throw new MastraError(
908
+ {
909
+ id: 'STORAGE_UPSTASH_STORAGE_SAVE_MESSAGES_FAILED',
910
+ domain: ErrorDomain.STORAGE,
911
+ category: ErrorCategory.THIRD_PARTY,
912
+ details: {
913
+ threadId,
914
+ },
915
+ },
916
+ error,
917
+ );
666
918
  }
667
-
668
- const list = new MessageList().add(messages, 'memory');
669
- if (format === `v2`) return list.get.all.v2();
670
- return list.get.all.v1();
671
919
  }
672
920
 
673
921
  private async _getIncludedMessages(
@@ -734,89 +982,96 @@ export class UpstashStore extends MastraStorage {
734
982
  format,
735
983
  }: StorageGetMessagesArg & { format?: 'v1' | 'v2' }): Promise<MastraMessageV1[] | MastraMessageV2[]> {
736
984
  const threadMessagesKey = this.getThreadMessagesKey(threadId);
737
- const allMessageIds = await this.redis.zrange(threadMessagesKey, 0, -1);
738
- // When selectBy is undefined or selectBy.last is undefined, get ALL messages (not just 40)
739
- let limit: number;
740
- if (typeof selectBy?.last === 'number') {
741
- limit = Math.max(0, selectBy.last);
742
- } else if (selectBy?.last === false) {
743
- limit = 0;
744
- } else {
745
- // No limit specified - get all messages
746
- limit = Number.MAX_SAFE_INTEGER;
747
- }
985
+ try {
986
+ const allMessageIds = await this.redis.zrange(threadMessagesKey, 0, -1);
987
+ const limit = this.resolveMessageLimit({ last: selectBy?.last, defaultLimit: Number.MAX_SAFE_INTEGER });
748
988
 
749
- const messageIds = new Set<string>();
750
- const messageIdToThreadIds: Record<string, string> = {};
989
+ const messageIds = new Set<string>();
990
+ const messageIdToThreadIds: Record<string, string> = {};
751
991
 
752
- if (limit === 0 && !selectBy?.include) {
753
- return [];
754
- }
755
-
756
- // Then get the most recent messages (or all if no limit)
757
- if (limit === Number.MAX_SAFE_INTEGER) {
758
- // Get all messages
759
- const allIds = await this.redis.zrange(threadMessagesKey, 0, -1);
760
- allIds.forEach(id => {
761
- messageIds.add(id as string);
762
- messageIdToThreadIds[id as string] = threadId;
763
- });
764
- } else if (limit > 0) {
765
- // Get limited number of recent messages
766
- const latestIds = await this.redis.zrange(threadMessagesKey, -limit, -1);
767
- latestIds.forEach(id => {
768
- messageIds.add(id as string);
769
- messageIdToThreadIds[id as string] = threadId;
770
- });
771
- }
992
+ if (limit === 0 && !selectBy?.include) {
993
+ return [];
994
+ }
772
995
 
773
- const includedMessages = await this._getIncludedMessages(threadId, selectBy);
996
+ // Then get the most recent messages (or all if no limit)
997
+ if (limit === Number.MAX_SAFE_INTEGER) {
998
+ // Get all messages
999
+ const allIds = await this.redis.zrange(threadMessagesKey, 0, -1);
1000
+ allIds.forEach(id => {
1001
+ messageIds.add(id as string);
1002
+ messageIdToThreadIds[id as string] = threadId;
1003
+ });
1004
+ } else if (limit > 0) {
1005
+ // Get limited number of recent messages
1006
+ const latestIds = await this.redis.zrange(threadMessagesKey, -limit, -1);
1007
+ latestIds.forEach(id => {
1008
+ messageIds.add(id as string);
1009
+ messageIdToThreadIds[id as string] = threadId;
1010
+ });
1011
+ }
774
1012
 
775
- // Fetch all needed messages in parallel
776
- const messages = [
777
- ...includedMessages,
778
- ...((
779
- await Promise.all(
780
- Array.from(messageIds).map(async id => {
781
- const tId = messageIdToThreadIds[id] || threadId;
782
- const byThreadId = await this.redis.get<MastraMessageV2 & { _index?: number }>(this.getMessageKey(tId, id));
783
- if (byThreadId) return byThreadId;
1013
+ const includedMessages = await this._getIncludedMessages(threadId, selectBy);
1014
+
1015
+ // Fetch all needed messages in parallel
1016
+ const messages = [
1017
+ ...includedMessages,
1018
+ ...((
1019
+ await Promise.all(
1020
+ Array.from(messageIds).map(async id => {
1021
+ const tId = messageIdToThreadIds[id] || threadId;
1022
+ const byThreadId = await this.redis.get<MastraMessageV2 & { _index?: number }>(
1023
+ this.getMessageKey(tId, id),
1024
+ );
1025
+ if (byThreadId) return byThreadId;
1026
+
1027
+ return null;
1028
+ }),
1029
+ )
1030
+ ).filter(msg => msg !== null) as (MastraMessageV2 & { _index?: number })[]),
1031
+ ];
784
1032
 
785
- return null;
786
- }),
787
- )
788
- ).filter(msg => msg !== null) as (MastraMessageV2 & { _index?: number })[]),
789
- ];
1033
+ // Sort messages by their position in the sorted set
1034
+ messages.sort((a, b) => allMessageIds.indexOf(a!.id) - allMessageIds.indexOf(b!.id));
790
1035
 
791
- // Sort messages by their position in the sorted set
792
- messages.sort((a, b) => allMessageIds.indexOf(a!.id) - allMessageIds.indexOf(b!.id));
1036
+ const seen = new Set<string>();
1037
+ const dedupedMessages = messages.filter(row => {
1038
+ if (seen.has(row.id)) return false;
1039
+ seen.add(row.id);
1040
+ return true;
1041
+ });
793
1042
 
794
- const seen = new Set<string>();
795
- const dedupedMessages = messages.filter(row => {
796
- if (seen.has(row.id)) return false;
797
- seen.add(row.id);
798
- return true;
799
- });
1043
+ // Remove _index before returning and handle format conversion properly
1044
+ const prepared = dedupedMessages
1045
+ .filter(message => message !== null && message !== undefined)
1046
+ .map(message => {
1047
+ const { _index, ...messageWithoutIndex } = message as MastraMessageV2 & { _index?: number };
1048
+ return messageWithoutIndex as unknown as MastraMessageV1;
1049
+ });
800
1050
 
801
- // Remove _index before returning and handle format conversion properly
802
- const prepared = dedupedMessages
803
- .filter(message => message !== null && message !== undefined)
804
- .map(message => {
805
- const { _index, ...messageWithoutIndex } = message as MastraMessageV2 & { _index?: number };
806
- return messageWithoutIndex as unknown as MastraMessageV1;
807
- });
1051
+ // For backward compatibility, return messages directly without using MessageList
1052
+ // since MessageList has deduplication logic that can cause issues
1053
+ if (format === 'v2') {
1054
+ // Convert V1 format back to V2 format
1055
+ return prepared.map(msg => ({
1056
+ ...msg,
1057
+ content: msg.content || { format: 2, parts: [{ type: 'text', text: '' }] },
1058
+ })) as MastraMessageV2[];
1059
+ }
808
1060
 
809
- // For backward compatibility, return messages directly without using MessageList
810
- // since MessageList has deduplication logic that can cause issues
811
- if (format === 'v2') {
812
- // Convert V1 format back to V2 format
813
- return prepared.map(msg => ({
814
- ...msg,
815
- content: msg.content || { format: 2, parts: [{ type: 'text', text: '' }] },
816
- })) as MastraMessageV2[];
1061
+ return prepared;
1062
+ } catch (error) {
1063
+ throw new MastraError(
1064
+ {
1065
+ id: 'STORAGE_UPSTASH_STORAGE_GET_MESSAGES_FAILED',
1066
+ domain: ErrorDomain.STORAGE,
1067
+ category: ErrorCategory.THIRD_PARTY,
1068
+ details: {
1069
+ threadId,
1070
+ },
1071
+ },
1072
+ error,
1073
+ );
817
1074
  }
818
-
819
- return prepared;
820
1075
  }
821
1076
 
822
1077
  public async getMessagesPaginated(
@@ -831,10 +1086,10 @@ export class UpstashStore extends MastraStorage {
831
1086
  const threadMessagesKey = this.getThreadMessagesKey(threadId);
832
1087
  const messages: (MastraMessageV2 | MastraMessageV1)[] = [];
833
1088
 
834
- const includedMessages = await this._getIncludedMessages(threadId, selectBy);
835
- messages.push(...includedMessages);
836
-
837
1089
  try {
1090
+ const includedMessages = await this._getIncludedMessages(threadId, selectBy);
1091
+ messages.push(...includedMessages);
1092
+
838
1093
  const allMessageIds = await this.redis.zrange(threadMessagesKey, 0, -1);
839
1094
  if (allMessageIds.length === 0) {
840
1095
  return {
@@ -891,7 +1146,19 @@ export class UpstashStore extends MastraStorage {
891
1146
  hasMore,
892
1147
  };
893
1148
  } catch (error) {
894
- console.error('Failed to get paginated messages:', error);
1149
+ const mastraError = new MastraError(
1150
+ {
1151
+ id: 'STORAGE_UPSTASH_STORAGE_GET_MESSAGES_PAGINATED_FAILED',
1152
+ domain: ErrorDomain.STORAGE,
1153
+ category: ErrorCategory.THIRD_PARTY,
1154
+ details: {
1155
+ threadId,
1156
+ },
1157
+ },
1158
+ error,
1159
+ );
1160
+ this.logger.error(mastraError.toString());
1161
+ this.logger?.trackException(mastraError);
895
1162
  return {
896
1163
  messages: [],
897
1164
  total: 0,
@@ -909,17 +1176,33 @@ export class UpstashStore extends MastraStorage {
909
1176
  snapshot: WorkflowRunState;
910
1177
  }): Promise<void> {
911
1178
  const { namespace = 'workflows', workflowName, runId, snapshot } = params;
912
- await this.insert({
913
- tableName: TABLE_WORKFLOW_SNAPSHOT,
914
- record: {
915
- namespace,
916
- workflow_name: workflowName,
917
- run_id: runId,
918
- snapshot,
919
- createdAt: new Date(),
920
- updatedAt: new Date(),
921
- },
922
- });
1179
+ try {
1180
+ await this.insert({
1181
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
1182
+ record: {
1183
+ namespace,
1184
+ workflow_name: workflowName,
1185
+ run_id: runId,
1186
+ snapshot,
1187
+ createdAt: new Date(),
1188
+ updatedAt: new Date(),
1189
+ },
1190
+ });
1191
+ } catch (error) {
1192
+ throw new MastraError(
1193
+ {
1194
+ id: 'STORAGE_UPSTASH_STORAGE_PERSIST_WORKFLOW_SNAPSHOT_FAILED',
1195
+ domain: ErrorDomain.STORAGE,
1196
+ category: ErrorCategory.THIRD_PARTY,
1197
+ details: {
1198
+ namespace,
1199
+ workflowName,
1200
+ runId,
1201
+ },
1202
+ },
1203
+ error,
1204
+ );
1205
+ }
923
1206
  }
924
1207
 
925
1208
  async loadWorkflowSnapshot(params: {
@@ -933,14 +1216,30 @@ export class UpstashStore extends MastraStorage {
933
1216
  workflow_name: workflowName,
934
1217
  run_id: runId,
935
1218
  });
936
- const data = await this.redis.get<{
937
- namespace: string;
938
- workflow_name: string;
939
- run_id: string;
940
- snapshot: WorkflowRunState;
941
- }>(key);
942
- if (!data) return null;
943
- return data.snapshot;
1219
+ try {
1220
+ const data = await this.redis.get<{
1221
+ namespace: string;
1222
+ workflow_name: string;
1223
+ run_id: string;
1224
+ snapshot: WorkflowRunState;
1225
+ }>(key);
1226
+ if (!data) return null;
1227
+ return data.snapshot;
1228
+ } catch (error) {
1229
+ throw new MastraError(
1230
+ {
1231
+ id: 'STORAGE_UPSTASH_STORAGE_LOAD_WORKFLOW_SNAPSHOT_FAILED',
1232
+ domain: ErrorDomain.STORAGE,
1233
+ category: ErrorCategory.THIRD_PARTY,
1234
+ details: {
1235
+ namespace,
1236
+ workflowName,
1237
+ runId,
1238
+ },
1239
+ },
1240
+ error,
1241
+ );
1242
+ }
944
1243
  }
945
1244
 
946
1245
  /**
@@ -1063,7 +1362,20 @@ export class UpstashStore extends MastraStorage {
1063
1362
  };
1064
1363
  } catch (error) {
1065
1364
  const { page = 0, perPage = 100 } = options || {};
1066
- console.error('Failed to get evals:', error);
1365
+ const mastraError = new MastraError(
1366
+ {
1367
+ id: 'STORAGE_UPSTASH_STORAGE_GET_EVALS_FAILED',
1368
+ domain: ErrorDomain.STORAGE,
1369
+ category: ErrorCategory.THIRD_PARTY,
1370
+ details: {
1371
+ page,
1372
+ perPage,
1373
+ },
1374
+ },
1375
+ error,
1376
+ );
1377
+ this.logger.error(mastraError.toString());
1378
+ this.logger?.trackException(mastraError);
1067
1379
  return {
1068
1380
  evals: [],
1069
1381
  total: 0,
@@ -1146,8 +1458,19 @@ export class UpstashStore extends MastraStorage {
1146
1458
 
1147
1459
  return { runs, total };
1148
1460
  } catch (error) {
1149
- console.error('Error getting workflow runs:', error);
1150
- throw error;
1461
+ throw new MastraError(
1462
+ {
1463
+ id: 'STORAGE_UPSTASH_STORAGE_GET_WORKFLOW_RUNS_FAILED',
1464
+ domain: ErrorDomain.STORAGE,
1465
+ category: ErrorCategory.THIRD_PARTY,
1466
+ details: {
1467
+ namespace,
1468
+ workflowName: workflowName || '',
1469
+ resourceId: resourceId || '',
1470
+ },
1471
+ },
1472
+ error,
1473
+ );
1151
1474
  }
1152
1475
  }
1153
1476
 
@@ -1180,8 +1503,19 @@ export class UpstashStore extends MastraStorage {
1180
1503
  if (!data) return null;
1181
1504
  return this.parseWorkflowRun(data);
1182
1505
  } catch (error) {
1183
- console.error('Error getting workflow run by ID:', error);
1184
- throw error;
1506
+ throw new MastraError(
1507
+ {
1508
+ id: 'STORAGE_UPSTASH_STORAGE_GET_WORKFLOW_RUN_BY_ID_FAILED',
1509
+ domain: ErrorDomain.STORAGE,
1510
+ category: ErrorCategory.THIRD_PARTY,
1511
+ details: {
1512
+ namespace,
1513
+ runId,
1514
+ workflowName: workflowName || '',
1515
+ },
1516
+ },
1517
+ error,
1518
+ );
1185
1519
  }
1186
1520
  }
1187
1521
 
@@ -1199,4 +1533,88 @@ export class UpstashStore extends MastraStorage {
1199
1533
  this.logger.error('updateMessages is not yet implemented in UpstashStore');
1200
1534
  throw new Error('Method not implemented');
1201
1535
  }
1536
+
1537
+ async getResourceById({ resourceId }: { resourceId: string }): Promise<StorageResourceType | null> {
1538
+ try {
1539
+ const key = `${TABLE_RESOURCES}:${resourceId}`;
1540
+ const data = await this.redis.get<StorageResourceType>(key);
1541
+
1542
+ if (!data) {
1543
+ return null;
1544
+ }
1545
+
1546
+ return {
1547
+ ...data,
1548
+ createdAt: new Date(data.createdAt),
1549
+ updatedAt: new Date(data.updatedAt),
1550
+ // Ensure workingMemory is always returned as a string, regardless of automatic parsing
1551
+ workingMemory: typeof data.workingMemory === 'object' ? JSON.stringify(data.workingMemory) : data.workingMemory,
1552
+ metadata: typeof data.metadata === 'string' ? JSON.parse(data.metadata) : data.metadata,
1553
+ };
1554
+ } catch (error) {
1555
+ this.logger.error('Error getting resource by ID:', error);
1556
+ throw error;
1557
+ }
1558
+ }
1559
+
1560
+ async saveResource({ resource }: { resource: StorageResourceType }): Promise<StorageResourceType> {
1561
+ try {
1562
+ const key = `${TABLE_RESOURCES}:${resource.id}`;
1563
+ const serializedResource = {
1564
+ ...resource,
1565
+ metadata: JSON.stringify(resource.metadata),
1566
+ createdAt: resource.createdAt.toISOString(),
1567
+ updatedAt: resource.updatedAt.toISOString(),
1568
+ };
1569
+
1570
+ await this.redis.set(key, serializedResource);
1571
+
1572
+ return resource;
1573
+ } catch (error) {
1574
+ this.logger.error('Error saving resource:', error);
1575
+ throw error;
1576
+ }
1577
+ }
1578
+
1579
+ async updateResource({
1580
+ resourceId,
1581
+ workingMemory,
1582
+ metadata,
1583
+ }: {
1584
+ resourceId: string;
1585
+ workingMemory?: string;
1586
+ metadata?: Record<string, unknown>;
1587
+ }): Promise<StorageResourceType> {
1588
+ try {
1589
+ const existingResource = await this.getResourceById({ resourceId });
1590
+
1591
+ if (!existingResource) {
1592
+ // Create new resource if it doesn't exist
1593
+ const newResource: StorageResourceType = {
1594
+ id: resourceId,
1595
+ workingMemory,
1596
+ metadata: metadata || {},
1597
+ createdAt: new Date(),
1598
+ updatedAt: new Date(),
1599
+ };
1600
+ return this.saveResource({ resource: newResource });
1601
+ }
1602
+
1603
+ const updatedResource = {
1604
+ ...existingResource,
1605
+ workingMemory: workingMemory !== undefined ? workingMemory : existingResource.workingMemory,
1606
+ metadata: {
1607
+ ...existingResource.metadata,
1608
+ ...metadata,
1609
+ },
1610
+ updatedAt: new Date(),
1611
+ };
1612
+
1613
+ await this.saveResource({ resource: updatedResource });
1614
+ return updatedResource;
1615
+ } catch (error) {
1616
+ this.logger.error('Error updating resource:', error);
1617
+ throw error;
1618
+ }
1619
+ }
1202
1620
  }