@mastra/dynamodb 0.0.0-support-d1-client-20250701191943 → 0.0.0-taofeeq-fix-tool-call-showing-after-message-20250806184630

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.
Files changed (54) hide show
  1. package/LICENSE.md +11 -42
  2. package/dist/entities/eval.d.ts +102 -0
  3. package/dist/entities/eval.d.ts.map +1 -0
  4. package/dist/entities/index.d.ts +746 -0
  5. package/dist/entities/index.d.ts.map +1 -0
  6. package/dist/entities/message.d.ts +100 -0
  7. package/dist/entities/message.d.ts.map +1 -0
  8. package/dist/entities/resource.d.ts +54 -0
  9. package/dist/entities/resource.d.ts.map +1 -0
  10. package/dist/entities/score.d.ts +229 -0
  11. package/dist/entities/score.d.ts.map +1 -0
  12. package/dist/entities/thread.d.ts +69 -0
  13. package/dist/entities/thread.d.ts.map +1 -0
  14. package/dist/entities/trace.d.ts +127 -0
  15. package/dist/entities/trace.d.ts.map +1 -0
  16. package/dist/entities/utils.d.ts +21 -0
  17. package/dist/entities/utils.d.ts.map +1 -0
  18. package/dist/entities/workflow-snapshot.d.ts +74 -0
  19. package/dist/entities/workflow-snapshot.d.ts.map +1 -0
  20. package/dist/index.cjs +2013 -520
  21. package/dist/index.cjs.map +1 -0
  22. package/dist/index.d.ts +2 -2
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +2014 -521
  25. package/dist/index.js.map +1 -0
  26. package/dist/storage/domains/legacy-evals/index.d.ts +19 -0
  27. package/dist/storage/domains/legacy-evals/index.d.ts.map +1 -0
  28. package/dist/storage/domains/memory/index.d.ts +81 -0
  29. package/dist/storage/domains/memory/index.d.ts.map +1 -0
  30. package/dist/storage/domains/operations/index.d.ts +69 -0
  31. package/dist/storage/domains/operations/index.d.ts.map +1 -0
  32. package/dist/storage/domains/score/index.d.ts +42 -0
  33. package/dist/storage/domains/score/index.d.ts.map +1 -0
  34. package/dist/storage/domains/traces/index.d.ts +28 -0
  35. package/dist/storage/domains/traces/index.d.ts.map +1 -0
  36. package/dist/storage/domains/workflows/index.d.ts +32 -0
  37. package/dist/storage/domains/workflows/index.d.ts.map +1 -0
  38. package/dist/storage/index.d.ts +220 -0
  39. package/dist/storage/index.d.ts.map +1 -0
  40. package/package.json +13 -12
  41. package/src/entities/index.ts +5 -1
  42. package/src/entities/resource.ts +57 -0
  43. package/src/entities/score.ts +317 -0
  44. package/src/storage/domains/legacy-evals/index.ts +243 -0
  45. package/src/storage/domains/memory/index.ts +931 -0
  46. package/src/storage/domains/operations/index.ts +433 -0
  47. package/src/storage/domains/score/index.ts +288 -0
  48. package/src/storage/domains/traces/index.ts +286 -0
  49. package/src/storage/domains/workflows/index.ts +297 -0
  50. package/src/storage/index.test.ts +1346 -1292
  51. package/src/storage/index.ts +166 -1063
  52. package/dist/_tsup-dts-rollup.d.cts +0 -1160
  53. package/dist/_tsup-dts-rollup.d.ts +0 -1160
  54. package/dist/index.d.cts +0 -2
@@ -0,0 +1,931 @@
1
+ import { MessageList } from '@mastra/core/agent';
2
+ import type { MastraMessageContentV2 } from '@mastra/core/agent';
3
+ import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
4
+ import type { StorageThreadType, MastraMessageV1, MastraMessageV2 } from '@mastra/core/memory';
5
+ import { MemoryStorage, resolveMessageLimit } from '@mastra/core/storage';
6
+ import type {
7
+ PaginationInfo,
8
+ StorageGetMessagesArg,
9
+ StorageResourceType,
10
+ ThreadSortOptions,
11
+ } from '@mastra/core/storage';
12
+ import type { Service } from 'electrodb';
13
+
14
+ export class MemoryStorageDynamoDB extends MemoryStorage {
15
+ private service: Service<Record<string, any>>;
16
+ constructor({ service }: { service: Service<Record<string, any>> }) {
17
+ super();
18
+ this.service = service;
19
+ }
20
+
21
+ // Helper function to parse message data (handle JSON fields)
22
+ private parseMessageData(data: any): MastraMessageV2 | MastraMessageV1 {
23
+ // Removed try/catch and JSON.parse logic - now handled by entity 'get' attributes
24
+ // This function now primarily ensures correct typing and Date conversion.
25
+ return {
26
+ ...data,
27
+ // Ensure dates are Date objects if needed (ElectroDB might return strings)
28
+ createdAt: data.createdAt ? new Date(data.createdAt) : undefined,
29
+ updatedAt: data.updatedAt ? new Date(data.updatedAt) : undefined,
30
+ // Other fields like content, toolCallArgs etc. are assumed to be correctly
31
+ // transformed by the ElectroDB entity getters.
32
+ };
33
+ }
34
+
35
+ // Helper function to transform and sort threads
36
+ private transformAndSortThreads(rawThreads: any[], orderBy: string, sortDirection: string): StorageThreadType[] {
37
+ return rawThreads
38
+ .map((data: any) => ({
39
+ ...data,
40
+ // Convert date strings back to Date objects for consistency
41
+ createdAt: typeof data.createdAt === 'string' ? new Date(data.createdAt) : data.createdAt,
42
+ updatedAt: typeof data.updatedAt === 'string' ? new Date(data.updatedAt) : data.updatedAt,
43
+ }))
44
+ .sort((a: StorageThreadType, b: StorageThreadType) => {
45
+ const fieldA = orderBy === 'createdAt' ? a.createdAt : a.updatedAt;
46
+ const fieldB = orderBy === 'createdAt' ? b.createdAt : b.updatedAt;
47
+
48
+ const comparison = fieldA.getTime() - fieldB.getTime();
49
+ return sortDirection === 'DESC' ? -comparison : comparison;
50
+ }) as StorageThreadType[];
51
+ }
52
+
53
+ async getThreadById({ threadId }: { threadId: string }): Promise<StorageThreadType | null> {
54
+ this.logger.debug('Getting thread by ID', { threadId });
55
+ try {
56
+ const result = await this.service.entities.thread.get({ entity: 'thread', id: threadId }).go();
57
+
58
+ if (!result.data) {
59
+ return null;
60
+ }
61
+
62
+ // ElectroDB handles the transformation with attribute getters
63
+ const data = result.data;
64
+ return {
65
+ ...data,
66
+ // Convert date strings back to Date objects for consistency
67
+ createdAt: typeof data.createdAt === 'string' ? new Date(data.createdAt) : data.createdAt,
68
+ updatedAt: typeof data.updatedAt === 'string' ? new Date(data.updatedAt) : data.updatedAt,
69
+ // metadata: data.metadata ? JSON.parse(data.metadata) : undefined, // REMOVED by AI
70
+ // metadata is already transformed by the entity's getter
71
+ } as StorageThreadType;
72
+ } catch (error) {
73
+ throw new MastraError(
74
+ {
75
+ id: 'STORAGE_DYNAMODB_STORE_GET_THREAD_BY_ID_FAILED',
76
+ domain: ErrorDomain.STORAGE,
77
+ category: ErrorCategory.THIRD_PARTY,
78
+ details: { threadId },
79
+ },
80
+ error,
81
+ );
82
+ }
83
+ }
84
+
85
+ /**
86
+ * @deprecated use getThreadsByResourceIdPaginated instead for paginated results.
87
+ */
88
+ public async getThreadsByResourceId(args: { resourceId: string } & ThreadSortOptions): Promise<StorageThreadType[]> {
89
+ const resourceId = args.resourceId;
90
+ const orderBy = this.castThreadOrderBy(args.orderBy);
91
+ const sortDirection = this.castThreadSortDirection(args.sortDirection);
92
+
93
+ this.logger.debug('Getting threads by resource ID', { resourceId, orderBy, sortDirection });
94
+
95
+ try {
96
+ const result = await this.service.entities.thread.query.byResource({ entity: 'thread', resourceId }).go();
97
+
98
+ if (!result.data.length) {
99
+ return [];
100
+ }
101
+
102
+ // Use shared helper method for transformation and sorting
103
+ return this.transformAndSortThreads(result.data, orderBy, sortDirection);
104
+ } catch (error) {
105
+ throw new MastraError(
106
+ {
107
+ id: 'STORAGE_DYNAMODB_STORE_GET_THREADS_BY_RESOURCE_ID_FAILED',
108
+ domain: ErrorDomain.STORAGE,
109
+ category: ErrorCategory.THIRD_PARTY,
110
+ details: { resourceId },
111
+ },
112
+ error,
113
+ );
114
+ }
115
+ }
116
+
117
+ async saveThread({ thread }: { thread: StorageThreadType }): Promise<StorageThreadType> {
118
+ this.logger.debug('Saving thread', { threadId: thread.id });
119
+
120
+ const now = new Date();
121
+
122
+ const threadData = {
123
+ entity: 'thread',
124
+ id: thread.id,
125
+ resourceId: thread.resourceId,
126
+ title: thread.title || `Thread ${thread.id}`,
127
+ createdAt: thread.createdAt?.toISOString() || now.toISOString(),
128
+ updatedAt: now.toISOString(),
129
+ metadata: thread.metadata ? JSON.stringify(thread.metadata) : undefined,
130
+ };
131
+
132
+ try {
133
+ await this.service.entities.thread.upsert(threadData).go();
134
+
135
+ return {
136
+ id: thread.id,
137
+ resourceId: thread.resourceId,
138
+ title: threadData.title,
139
+ createdAt: thread.createdAt || now,
140
+ updatedAt: now,
141
+ metadata: thread.metadata,
142
+ };
143
+ } catch (error) {
144
+ throw new MastraError(
145
+ {
146
+ id: 'STORAGE_DYNAMODB_STORE_SAVE_THREAD_FAILED',
147
+ domain: ErrorDomain.STORAGE,
148
+ category: ErrorCategory.THIRD_PARTY,
149
+ details: { threadId: thread.id },
150
+ },
151
+ error,
152
+ );
153
+ }
154
+ }
155
+
156
+ async updateThread({
157
+ id,
158
+ title,
159
+ metadata,
160
+ }: {
161
+ id: string;
162
+ title: string;
163
+ metadata: Record<string, unknown>;
164
+ }): Promise<StorageThreadType> {
165
+ this.logger.debug('Updating thread', { threadId: id });
166
+
167
+ try {
168
+ // First, get the existing thread to merge with updates
169
+ const existingThread = await this.getThreadById({ threadId: id });
170
+
171
+ if (!existingThread) {
172
+ throw new Error(`Thread not found: ${id}`);
173
+ }
174
+
175
+ const now = new Date();
176
+
177
+ // Prepare the update
178
+ // Define type for only the fields we are actually updating
179
+ type ThreadUpdatePayload = {
180
+ updatedAt: string; // ISO String for DDB
181
+ title?: string;
182
+ metadata?: string; // Stringified JSON for DDB
183
+ };
184
+ const updateData: ThreadUpdatePayload = {
185
+ updatedAt: now.toISOString(),
186
+ };
187
+
188
+ if (title) {
189
+ updateData.title = title;
190
+ }
191
+
192
+ if (metadata) {
193
+ // Merge with existing metadata instead of overwriting
194
+ const existingMetadata = existingThread.metadata
195
+ ? typeof existingThread.metadata === 'string'
196
+ ? JSON.parse(existingThread.metadata)
197
+ : existingThread.metadata
198
+ : {};
199
+ const mergedMetadata = { ...existingMetadata, ...metadata };
200
+ updateData.metadata = JSON.stringify(mergedMetadata); // Stringify merged metadata for update
201
+ }
202
+
203
+ // Update the thread using the primary key
204
+ await this.service.entities.thread.update({ entity: 'thread', id }).set(updateData).go();
205
+
206
+ // Return the potentially updated thread object
207
+ return {
208
+ ...existingThread,
209
+ title: title || existingThread.title,
210
+ metadata: metadata ? { ...existingThread.metadata, ...metadata } : existingThread.metadata,
211
+ updatedAt: now,
212
+ };
213
+ } catch (error) {
214
+ throw new MastraError(
215
+ {
216
+ id: 'STORAGE_DYNAMODB_STORE_UPDATE_THREAD_FAILED',
217
+ domain: ErrorDomain.STORAGE,
218
+ category: ErrorCategory.THIRD_PARTY,
219
+ details: { threadId: id },
220
+ },
221
+ error,
222
+ );
223
+ }
224
+ }
225
+
226
+ async deleteThread({ threadId }: { threadId: string }): Promise<void> {
227
+ this.logger.debug('Deleting thread', { threadId });
228
+
229
+ try {
230
+ // First, delete all messages associated with this thread
231
+ const messages = await this.getMessages({ threadId });
232
+ if (messages.length > 0) {
233
+ // Delete messages in batches
234
+ const batchSize = 25; // DynamoDB batch limits
235
+ for (let i = 0; i < messages.length; i += batchSize) {
236
+ const batch = messages.slice(i, i + batchSize);
237
+ await Promise.all(
238
+ batch.map(message =>
239
+ this.service.entities.message
240
+ .delete({
241
+ entity: 'message',
242
+ id: message.id,
243
+ threadId: message.threadId,
244
+ })
245
+ .go(),
246
+ ),
247
+ );
248
+ }
249
+ }
250
+
251
+ // Then delete the thread using the primary key
252
+ await this.service.entities.thread.delete({ entity: 'thread', id: threadId }).go();
253
+ } catch (error) {
254
+ throw new MastraError(
255
+ {
256
+ id: 'STORAGE_DYNAMODB_STORE_DELETE_THREAD_FAILED',
257
+ domain: ErrorDomain.STORAGE,
258
+ category: ErrorCategory.THIRD_PARTY,
259
+ details: { threadId },
260
+ },
261
+ error,
262
+ );
263
+ }
264
+ }
265
+
266
+ public async getMessages(args: StorageGetMessagesArg & { format?: 'v1' }): Promise<MastraMessageV1[]>;
267
+ public async getMessages(args: StorageGetMessagesArg & { format: 'v2' }): Promise<MastraMessageV2[]>;
268
+ public async getMessages({
269
+ threadId,
270
+ resourceId,
271
+ selectBy,
272
+ format,
273
+ }: StorageGetMessagesArg & { format?: 'v1' | 'v2' }): Promise<MastraMessageV1[] | MastraMessageV2[]> {
274
+ this.logger.debug('Getting messages', { threadId, selectBy });
275
+
276
+ try {
277
+ const messages: MastraMessageV2[] = [];
278
+ const limit = resolveMessageLimit({ last: selectBy?.last, defaultLimit: Number.MAX_SAFE_INTEGER });
279
+
280
+ // Handle included messages first (like libsql)
281
+ if (selectBy?.include?.length) {
282
+ const includeMessages = await this._getIncludedMessages(threadId, selectBy);
283
+ if (includeMessages) {
284
+ messages.push(...includeMessages);
285
+ }
286
+ }
287
+
288
+ // Get remaining messages only if limit is not 0
289
+ if (limit !== 0) {
290
+ // Query messages by thread ID using the GSI
291
+ const query = this.service.entities.message.query.byThread({ entity: 'message', threadId });
292
+
293
+ // Get messages from the main thread
294
+ let results;
295
+ if (limit !== Number.MAX_SAFE_INTEGER && limit > 0) {
296
+ // Use limit in query to get only the last N messages
297
+ results = await query.go({ limit, order: 'desc' });
298
+ // Reverse the results since we want ascending order
299
+ results.data = results.data.reverse();
300
+ } else {
301
+ // Get all messages
302
+ results = await query.go();
303
+ }
304
+
305
+ let allThreadMessages = results.data
306
+ .map((data: any) => this.parseMessageData(data))
307
+ .filter((msg: any): msg is MastraMessageV2 => 'content' in msg);
308
+
309
+ // Sort by createdAt ASC to get proper order
310
+ allThreadMessages.sort((a: MastraMessageV2, b: MastraMessageV2) => {
311
+ const timeA = a.createdAt.getTime();
312
+ const timeB = b.createdAt.getTime();
313
+ if (timeA === timeB) {
314
+ return a.id.localeCompare(b.id);
315
+ }
316
+ return timeA - timeB;
317
+ });
318
+
319
+ messages.push(...allThreadMessages);
320
+ }
321
+
322
+ // Sort by createdAt ASC to match libsql behavior, with ID tiebreaker for stable ordering
323
+ messages.sort((a, b) => {
324
+ const timeA = a.createdAt.getTime();
325
+ const timeB = b.createdAt.getTime();
326
+ if (timeA === timeB) {
327
+ return a.id.localeCompare(b.id);
328
+ }
329
+ return timeA - timeB;
330
+ });
331
+
332
+ // Deduplicate messages by ID (like libsql)
333
+ const uniqueMessages = messages.filter(
334
+ (message, index, self) => index === self.findIndex(m => m.id === message.id),
335
+ );
336
+
337
+ const list = new MessageList({ threadId, resourceId }).add(uniqueMessages, 'memory');
338
+ if (format === `v2`) return list.get.all.v2();
339
+ return list.get.all.v1();
340
+ } catch (error) {
341
+ throw new MastraError(
342
+ {
343
+ id: 'STORAGE_DYNAMODB_STORE_GET_MESSAGES_FAILED',
344
+ domain: ErrorDomain.STORAGE,
345
+ category: ErrorCategory.THIRD_PARTY,
346
+ details: { threadId },
347
+ },
348
+ error,
349
+ );
350
+ }
351
+ }
352
+
353
+ async saveMessages(args: { messages: MastraMessageV1[]; format?: undefined | 'v1' }): Promise<MastraMessageV1[]>;
354
+ async saveMessages(args: { messages: MastraMessageV2[]; format: 'v2' }): Promise<MastraMessageV2[]>;
355
+ async saveMessages(
356
+ args: { messages: MastraMessageV1[]; format?: undefined | 'v1' } | { messages: MastraMessageV2[]; format: 'v2' },
357
+ ): Promise<MastraMessageV2[] | MastraMessageV1[]> {
358
+ const { messages, format = 'v1' } = args;
359
+ this.logger.debug('Saving messages', { count: messages.length });
360
+
361
+ if (!messages.length) {
362
+ return [];
363
+ }
364
+
365
+ const threadId = messages[0]?.threadId;
366
+ if (!threadId) {
367
+ throw new Error('Thread ID is required');
368
+ }
369
+
370
+ // Ensure 'entity' is added and complex fields are handled
371
+ const messagesToSave = messages.map(msg => {
372
+ const now = new Date().toISOString();
373
+ return {
374
+ entity: 'message', // Add entity type
375
+ id: msg.id,
376
+ threadId: msg.threadId,
377
+ role: msg.role,
378
+ type: msg.type,
379
+ resourceId: msg.resourceId,
380
+ // Ensure complex fields are stringified if not handled by attribute setters
381
+ content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
382
+ toolCallArgs: `toolCallArgs` in msg && msg.toolCallArgs ? JSON.stringify(msg.toolCallArgs) : undefined,
383
+ toolCallIds: `toolCallIds` in msg && msg.toolCallIds ? JSON.stringify(msg.toolCallIds) : undefined,
384
+ toolNames: `toolNames` in msg && msg.toolNames ? JSON.stringify(msg.toolNames) : undefined,
385
+ createdAt: msg.createdAt instanceof Date ? msg.createdAt.toISOString() : msg.createdAt || now,
386
+ updatedAt: now, // Add updatedAt
387
+ };
388
+ });
389
+
390
+ try {
391
+ // Process messages sequentially to enable rollback on error
392
+ const savedMessageIds: string[] = [];
393
+
394
+ for (const messageData of messagesToSave) {
395
+ // Ensure each item has the entity property before sending
396
+ if (!messageData.entity) {
397
+ this.logger.error('Missing entity property in message data for create', { messageData });
398
+ throw new Error('Internal error: Missing entity property during saveMessages');
399
+ }
400
+
401
+ try {
402
+ await this.service.entities.message.put(messageData).go();
403
+ savedMessageIds.push(messageData.id);
404
+ } catch (error) {
405
+ // Rollback: delete all previously saved messages
406
+ for (const savedId of savedMessageIds) {
407
+ try {
408
+ await this.service.entities.message.delete({ entity: 'message', id: savedId }).go();
409
+ } catch (rollbackError) {
410
+ this.logger.error('Failed to rollback message during save error', {
411
+ messageId: savedId,
412
+ error: rollbackError,
413
+ });
414
+ }
415
+ }
416
+ throw error;
417
+ }
418
+ }
419
+
420
+ // Update thread's updatedAt timestamp
421
+ await this.service.entities.thread
422
+ .update({ entity: 'thread', id: threadId })
423
+ .set({
424
+ updatedAt: new Date().toISOString(),
425
+ })
426
+ .go();
427
+
428
+ const list = new MessageList().add(messages, 'memory');
429
+ if (format === `v1`) return list.get.all.v1();
430
+ return list.get.all.v2();
431
+ } catch (error) {
432
+ throw new MastraError(
433
+ {
434
+ id: 'STORAGE_DYNAMODB_STORE_SAVE_MESSAGES_FAILED',
435
+ domain: ErrorDomain.STORAGE,
436
+ category: ErrorCategory.THIRD_PARTY,
437
+ details: { count: messages.length },
438
+ },
439
+ error,
440
+ );
441
+ }
442
+ }
443
+
444
+ async getThreadsByResourceIdPaginated(
445
+ args: {
446
+ resourceId: string;
447
+ page?: number;
448
+ perPage?: number;
449
+ } & ThreadSortOptions,
450
+ ): Promise<PaginationInfo & { threads: StorageThreadType[] }> {
451
+ const { resourceId, page = 0, perPage = 100 } = args;
452
+ const orderBy = this.castThreadOrderBy(args.orderBy);
453
+ const sortDirection = this.castThreadSortDirection(args.sortDirection);
454
+
455
+ this.logger.debug('Getting threads by resource ID with pagination', {
456
+ resourceId,
457
+ page,
458
+ perPage,
459
+ orderBy,
460
+ sortDirection,
461
+ });
462
+
463
+ try {
464
+ // Query threads by resource ID using the GSI
465
+ const query = this.service.entities.thread.query.byResource({ entity: 'thread', resourceId });
466
+
467
+ // Get all threads for this resource ID (DynamoDB doesn't support OFFSET/LIMIT)
468
+ const results = await query.go();
469
+
470
+ // Use shared helper method for transformation and sorting
471
+ const allThreads = this.transformAndSortThreads(results.data, orderBy, sortDirection);
472
+
473
+ // Apply pagination in memory
474
+ const startIndex = page * perPage;
475
+ const endIndex = startIndex + perPage;
476
+ const paginatedThreads = allThreads.slice(startIndex, endIndex);
477
+
478
+ // Calculate pagination info
479
+ const total = allThreads.length;
480
+ const hasMore = endIndex < total;
481
+
482
+ return {
483
+ threads: paginatedThreads,
484
+ total,
485
+ page,
486
+ perPage,
487
+ hasMore,
488
+ };
489
+ } catch (error) {
490
+ throw new MastraError(
491
+ {
492
+ id: 'STORAGE_DYNAMODB_STORE_GET_THREADS_BY_RESOURCE_ID_PAGINATED_FAILED',
493
+ domain: ErrorDomain.STORAGE,
494
+ category: ErrorCategory.THIRD_PARTY,
495
+ details: { resourceId, page, perPage },
496
+ },
497
+ error,
498
+ );
499
+ }
500
+ }
501
+
502
+ async getMessagesPaginated(
503
+ args: StorageGetMessagesArg & { format?: 'v1' | 'v2' },
504
+ ): Promise<PaginationInfo & { messages: MastraMessageV1[] | MastraMessageV2[] }> {
505
+ const { threadId, resourceId, selectBy, format = 'v1' } = args;
506
+ const { page = 0, perPage = 40, dateRange } = selectBy?.pagination || {};
507
+ const fromDate = dateRange?.start;
508
+ const toDate = dateRange?.end;
509
+ const limit = resolveMessageLimit({ last: selectBy?.last, defaultLimit: Number.MAX_SAFE_INTEGER });
510
+
511
+ this.logger.debug('Getting messages with pagination', { threadId, page, perPage, fromDate, toDate, limit });
512
+
513
+ try {
514
+ let messages: MastraMessageV2[] = [];
515
+
516
+ // Handle include messages first
517
+ if (selectBy?.include?.length) {
518
+ const includeMessages = await this._getIncludedMessages(threadId, selectBy);
519
+ if (includeMessages) {
520
+ messages.push(...includeMessages);
521
+ }
522
+ }
523
+
524
+ // Get remaining messages only if limit is not 0
525
+ if (limit !== 0) {
526
+ // Query messages by thread ID using the GSI
527
+ const query = this.service.entities.message.query.byThread({ entity: 'message', threadId });
528
+
529
+ // Get messages from the main thread
530
+ let results;
531
+ if (limit !== Number.MAX_SAFE_INTEGER && limit > 0) {
532
+ // Use limit in query to get only the last N messages
533
+ results = await query.go({ limit, order: 'desc' });
534
+ // Reverse the results since we want ascending order
535
+ results.data = results.data.reverse();
536
+ } else {
537
+ // Get all messages
538
+ results = await query.go();
539
+ }
540
+
541
+ let allThreadMessages = results.data
542
+ .map((data: any) => this.parseMessageData(data))
543
+ .filter((msg: any): msg is MastraMessageV2 => 'content' in msg);
544
+
545
+ // Sort by createdAt ASC to get proper order
546
+ allThreadMessages.sort((a: MastraMessageV2, b: MastraMessageV2) => {
547
+ const timeA = a.createdAt.getTime();
548
+ const timeB = b.createdAt.getTime();
549
+ if (timeA === timeB) {
550
+ return a.id.localeCompare(b.id);
551
+ }
552
+ return timeA - timeB;
553
+ });
554
+
555
+ // Exclude already included messages
556
+ const excludeIds = messages.map(m => m.id);
557
+ if (excludeIds.length > 0) {
558
+ allThreadMessages = allThreadMessages.filter((msg: MastraMessageV2) => !excludeIds.includes(msg.id));
559
+ }
560
+
561
+ messages.push(...allThreadMessages);
562
+ }
563
+
564
+ // Sort all messages by createdAt (oldest first for final result)
565
+ messages.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
566
+
567
+ // Apply date filtering if needed
568
+ if (fromDate || toDate) {
569
+ messages = messages.filter(msg => {
570
+ const createdAt = new Date(msg.createdAt).getTime();
571
+ if (fromDate && createdAt < new Date(fromDate).getTime()) return false;
572
+ if (toDate && createdAt > new Date(toDate).getTime()) return false;
573
+ return true;
574
+ });
575
+ }
576
+
577
+ // Save total before pagination
578
+ const total = messages.length;
579
+
580
+ // Apply offset-based pagination in memory
581
+ const start = page * perPage;
582
+ const end = start + perPage;
583
+ const paginatedMessages = messages.slice(start, end);
584
+ const hasMore = end < total;
585
+
586
+ const list = new MessageList({ threadId, resourceId }).add(paginatedMessages as MastraMessageV2[], 'memory');
587
+ const finalMessages = format === 'v2' ? list.get.all.v2() : list.get.all.v1();
588
+
589
+ return {
590
+ messages: finalMessages,
591
+ total,
592
+ page,
593
+ perPage,
594
+ hasMore,
595
+ };
596
+ } catch (error) {
597
+ throw new MastraError(
598
+ {
599
+ id: 'STORAGE_DYNAMODB_STORE_GET_MESSAGES_PAGINATED_FAILED',
600
+ domain: ErrorDomain.STORAGE,
601
+ category: ErrorCategory.THIRD_PARTY,
602
+ details: { threadId },
603
+ },
604
+ error,
605
+ );
606
+ }
607
+ }
608
+
609
+ // Helper method to get included messages with context
610
+ private async _getIncludedMessages(threadId: string, selectBy: any): Promise<MastraMessageV2[]> {
611
+ if (!selectBy?.include?.length) {
612
+ return [];
613
+ }
614
+
615
+ const includeMessages: MastraMessageV2[] = [];
616
+
617
+ for (const includeItem of selectBy.include) {
618
+ try {
619
+ const { id, threadId: targetThreadId, withPreviousMessages = 0, withNextMessages = 0 } = includeItem;
620
+ const searchThreadId = targetThreadId || threadId;
621
+
622
+ this.logger.debug('Getting included messages for', {
623
+ id,
624
+ targetThreadId,
625
+ searchThreadId,
626
+ withPreviousMessages,
627
+ withNextMessages,
628
+ });
629
+
630
+ // Get all messages for the target thread
631
+ const query = this.service.entities.message.query.byThread({ entity: 'message', threadId: searchThreadId });
632
+ const results = await query.go();
633
+ const allMessages = results.data
634
+ .map((data: any) => this.parseMessageData(data))
635
+ .filter((msg: any): msg is MastraMessageV2 => 'content' in msg && typeof msg.content === 'object');
636
+
637
+ this.logger.debug('Found messages in thread', {
638
+ threadId: searchThreadId,
639
+ messageCount: allMessages.length,
640
+ messageIds: allMessages.map((m: MastraMessageV2) => m.id),
641
+ });
642
+
643
+ // Sort by createdAt ASC to get proper order, with ID tiebreaker for stable ordering
644
+ allMessages.sort((a: MastraMessageV2, b: MastraMessageV2) => {
645
+ const timeA = a.createdAt.getTime();
646
+ const timeB = b.createdAt.getTime();
647
+ if (timeA === timeB) {
648
+ return a.id.localeCompare(b.id);
649
+ }
650
+ return timeA - timeB;
651
+ });
652
+
653
+ // Find the target message
654
+ const targetIndex = allMessages.findIndex((msg: MastraMessageV2) => msg.id === id);
655
+ if (targetIndex === -1) {
656
+ this.logger.warn('Target message not found', { id, threadId: searchThreadId });
657
+ continue;
658
+ }
659
+
660
+ this.logger.debug('Found target message at index', { id, targetIndex, totalMessages: allMessages.length });
661
+
662
+ // Get context messages (previous and next)
663
+ const startIndex = Math.max(0, targetIndex - withPreviousMessages);
664
+ const endIndex = Math.min(allMessages.length, targetIndex + withNextMessages + 1);
665
+ const contextMessages = allMessages.slice(startIndex, endIndex);
666
+
667
+ this.logger.debug('Context messages', {
668
+ startIndex,
669
+ endIndex,
670
+ contextCount: contextMessages.length,
671
+ contextIds: contextMessages.map((m: MastraMessageV2) => m.id),
672
+ });
673
+
674
+ includeMessages.push(...contextMessages);
675
+ } catch (error) {
676
+ this.logger.warn('Failed to get included message', { messageId: includeItem.id, error });
677
+ }
678
+ }
679
+
680
+ this.logger.debug('Total included messages', {
681
+ count: includeMessages.length,
682
+ ids: includeMessages.map((m: MastraMessageV2) => m.id),
683
+ });
684
+
685
+ return includeMessages;
686
+ }
687
+
688
+ async updateMessages(args: {
689
+ messages: Partial<Omit<MastraMessageV2, 'createdAt'>> &
690
+ {
691
+ id: string;
692
+ content?: { metadata?: MastraMessageContentV2['metadata']; content?: MastraMessageContentV2['content'] };
693
+ }[];
694
+ }): Promise<MastraMessageV2[]> {
695
+ const { messages } = args;
696
+ this.logger.debug('Updating messages', { count: messages.length });
697
+
698
+ if (!messages.length) {
699
+ return [];
700
+ }
701
+
702
+ const updatedMessages: MastraMessageV2[] = [];
703
+ const affectedThreadIds = new Set<string>();
704
+
705
+ try {
706
+ for (const updateData of messages) {
707
+ const { id, ...updates } = updateData;
708
+
709
+ // Get the existing message
710
+ const existingMessage = await this.service.entities.message.get({ entity: 'message', id }).go();
711
+ if (!existingMessage.data) {
712
+ this.logger.warn('Message not found for update', { id });
713
+ continue;
714
+ }
715
+
716
+ const existingMsg = this.parseMessageData(existingMessage.data) as MastraMessageV2;
717
+ const originalThreadId = existingMsg.threadId;
718
+ affectedThreadIds.add(originalThreadId!);
719
+
720
+ // Prepare the update payload
721
+ const updatePayload: any = {
722
+ updatedAt: new Date().toISOString(),
723
+ };
724
+
725
+ // Handle basic field updates
726
+ if ('role' in updates && updates.role !== undefined) updatePayload.role = updates.role;
727
+ if ('type' in updates && updates.type !== undefined) updatePayload.type = updates.type;
728
+ if ('resourceId' in updates && updates.resourceId !== undefined) updatePayload.resourceId = updates.resourceId;
729
+ if ('threadId' in updates && updates.threadId !== undefined && updates.threadId !== null) {
730
+ updatePayload.threadId = updates.threadId;
731
+ affectedThreadIds.add(updates.threadId as string);
732
+ }
733
+
734
+ // Handle content updates
735
+ if (updates.content) {
736
+ const existingContent = existingMsg.content;
737
+ let newContent = { ...existingContent };
738
+
739
+ // Deep merge metadata if provided
740
+ if (updates.content.metadata !== undefined) {
741
+ newContent.metadata = {
742
+ ...(existingContent.metadata || {}),
743
+ ...(updates.content.metadata || {}),
744
+ };
745
+ }
746
+
747
+ // Update content string if provided
748
+ if (updates.content.content !== undefined) {
749
+ newContent.content = updates.content.content;
750
+ }
751
+
752
+ // Update parts if provided (only if it exists in the content type)
753
+ if ('parts' in updates.content && updates.content.parts !== undefined) {
754
+ (newContent as any).parts = updates.content.parts;
755
+ }
756
+
757
+ updatePayload.content = JSON.stringify(newContent);
758
+ }
759
+
760
+ // Update the message
761
+ await this.service.entities.message.update({ entity: 'message', id }).set(updatePayload).go();
762
+
763
+ // Get the updated message
764
+ const updatedMessage = await this.service.entities.message.get({ entity: 'message', id }).go();
765
+ if (updatedMessage.data) {
766
+ updatedMessages.push(this.parseMessageData(updatedMessage.data) as MastraMessageV2);
767
+ }
768
+ }
769
+
770
+ // Update timestamps for all affected threads
771
+ for (const threadId of affectedThreadIds) {
772
+ await this.service.entities.thread
773
+ .update({ entity: 'thread', id: threadId })
774
+ .set({
775
+ updatedAt: new Date().toISOString(),
776
+ })
777
+ .go();
778
+ }
779
+
780
+ return updatedMessages;
781
+ } catch (error) {
782
+ throw new MastraError(
783
+ {
784
+ id: 'STORAGE_DYNAMODB_STORE_UPDATE_MESSAGES_FAILED',
785
+ domain: ErrorDomain.STORAGE,
786
+ category: ErrorCategory.THIRD_PARTY,
787
+ details: { count: messages.length },
788
+ },
789
+ error,
790
+ );
791
+ }
792
+ }
793
+
794
+ async getResourceById({ resourceId }: { resourceId: string }): Promise<StorageResourceType | null> {
795
+ this.logger.debug('Getting resource by ID', { resourceId });
796
+ try {
797
+ const result = await this.service.entities.resource.get({ entity: 'resource', id: resourceId }).go();
798
+
799
+ if (!result.data) {
800
+ return null;
801
+ }
802
+
803
+ // ElectroDB handles the transformation with attribute getters
804
+ const data = result.data;
805
+ return {
806
+ ...data,
807
+ // Convert date strings back to Date objects for consistency
808
+ createdAt: typeof data.createdAt === 'string' ? new Date(data.createdAt) : data.createdAt,
809
+ updatedAt: typeof data.updatedAt === 'string' ? new Date(data.updatedAt) : data.updatedAt,
810
+ // Ensure workingMemory is always returned as a string, regardless of automatic parsing
811
+ workingMemory: typeof data.workingMemory === 'object' ? JSON.stringify(data.workingMemory) : data.workingMemory,
812
+ // metadata is already transformed by the entity's getter
813
+ } as StorageResourceType;
814
+ } catch (error) {
815
+ throw new MastraError(
816
+ {
817
+ id: 'STORAGE_DYNAMODB_STORE_GET_RESOURCE_BY_ID_FAILED',
818
+ domain: ErrorDomain.STORAGE,
819
+ category: ErrorCategory.THIRD_PARTY,
820
+ details: { resourceId },
821
+ },
822
+ error,
823
+ );
824
+ }
825
+ }
826
+
827
+ async saveResource({ resource }: { resource: StorageResourceType }): Promise<StorageResourceType> {
828
+ this.logger.debug('Saving resource', { resourceId: resource.id });
829
+
830
+ const now = new Date();
831
+
832
+ const resourceData = {
833
+ entity: 'resource',
834
+ id: resource.id,
835
+ workingMemory: resource.workingMemory,
836
+ metadata: resource.metadata ? JSON.stringify(resource.metadata) : undefined,
837
+ createdAt: resource.createdAt?.toISOString() || now.toISOString(),
838
+ updatedAt: now.toISOString(),
839
+ };
840
+
841
+ try {
842
+ await this.service.entities.resource.upsert(resourceData).go();
843
+
844
+ return {
845
+ id: resource.id,
846
+ workingMemory: resource.workingMemory,
847
+ metadata: resource.metadata,
848
+ createdAt: resource.createdAt || now,
849
+ updatedAt: now,
850
+ };
851
+ } catch (error) {
852
+ throw new MastraError(
853
+ {
854
+ id: 'STORAGE_DYNAMODB_STORE_SAVE_RESOURCE_FAILED',
855
+ domain: ErrorDomain.STORAGE,
856
+ category: ErrorCategory.THIRD_PARTY,
857
+ details: { resourceId: resource.id },
858
+ },
859
+ error,
860
+ );
861
+ }
862
+ }
863
+
864
+ async updateResource({
865
+ resourceId,
866
+ workingMemory,
867
+ metadata,
868
+ }: {
869
+ resourceId: string;
870
+ workingMemory?: string;
871
+ metadata?: Record<string, unknown>;
872
+ }): Promise<StorageResourceType> {
873
+ this.logger.debug('Updating resource', { resourceId });
874
+
875
+ try {
876
+ // First, get the existing resource to merge with updates
877
+ const existingResource = await this.getResourceById({ resourceId });
878
+
879
+ if (!existingResource) {
880
+ // Create new resource if it doesn't exist
881
+ const newResource: StorageResourceType = {
882
+ id: resourceId,
883
+ workingMemory,
884
+ metadata: metadata || {},
885
+ createdAt: new Date(),
886
+ updatedAt: new Date(),
887
+ };
888
+ return this.saveResource({ resource: newResource });
889
+ }
890
+
891
+ const now = new Date();
892
+
893
+ // Prepare the update
894
+ const updateData: any = {
895
+ updatedAt: now.toISOString(),
896
+ };
897
+
898
+ if (workingMemory !== undefined) {
899
+ updateData.workingMemory = workingMemory;
900
+ }
901
+
902
+ if (metadata) {
903
+ // Merge with existing metadata instead of overwriting
904
+ const existingMetadata = existingResource.metadata || {};
905
+ const mergedMetadata = { ...existingMetadata, ...metadata };
906
+ updateData.metadata = JSON.stringify(mergedMetadata);
907
+ }
908
+
909
+ // Update the resource using the primary key
910
+ await this.service.entities.resource.update({ entity: 'resource', id: resourceId }).set(updateData).go();
911
+
912
+ // Return the updated resource object
913
+ return {
914
+ ...existingResource,
915
+ workingMemory: workingMemory !== undefined ? workingMemory : existingResource.workingMemory,
916
+ metadata: metadata ? { ...existingResource.metadata, ...metadata } : existingResource.metadata,
917
+ updatedAt: now,
918
+ };
919
+ } catch (error) {
920
+ throw new MastraError(
921
+ {
922
+ id: 'STORAGE_DYNAMODB_STORE_UPDATE_RESOURCE_FAILED',
923
+ domain: ErrorDomain.STORAGE,
924
+ category: ErrorCategory.THIRD_PARTY,
925
+ details: { resourceId },
926
+ },
927
+ error,
928
+ );
929
+ }
930
+ }
931
+ }