@mastra/lance 0.2.0 → 0.2.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +26 -0
- package/LICENSE.md +11 -42
- package/dist/_tsup-dts-rollup.d.cts +360 -89
- package/dist/_tsup-dts-rollup.d.ts +360 -89
- package/dist/index.cjs +1628 -646
- package/dist/index.js +1563 -581
- package/package.json +6 -6
- package/src/storage/domains/legacy-evals/index.ts +156 -0
- package/src/storage/domains/memory/index.ts +947 -0
- package/src/storage/domains/operations/index.ts +489 -0
- package/src/storage/domains/scores/index.ts +221 -0
- package/src/storage/domains/traces/index.ts +212 -0
- package/src/storage/domains/utils.ts +158 -0
- package/src/storage/domains/workflows/index.ts +207 -0
- package/src/storage/index.test.ts +6 -1332
- package/src/storage/index.ts +156 -1162
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
import type { Connection } from '@lancedb/lancedb';
|
|
2
|
+
import { MessageList } from '@mastra/core/agent';
|
|
3
|
+
import type { MastraMessageContentV2 } from '@mastra/core/agent';
|
|
4
|
+
import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
|
|
5
|
+
import type { MastraMessageV1, MastraMessageV2, StorageThreadType } from '@mastra/core/memory';
|
|
6
|
+
import {
|
|
7
|
+
MemoryStorage,
|
|
8
|
+
resolveMessageLimit,
|
|
9
|
+
TABLE_MESSAGES,
|
|
10
|
+
TABLE_RESOURCES,
|
|
11
|
+
TABLE_THREADS,
|
|
12
|
+
} from '@mastra/core/storage';
|
|
13
|
+
import type { PaginationInfo, StorageGetMessagesArg, StorageResourceType } from '@mastra/core/storage';
|
|
14
|
+
import type { StoreOperationsLance } from '../operations';
|
|
15
|
+
import { getTableSchema, processResultWithTypeConversion } from '../utils';
|
|
16
|
+
|
|
17
|
+
export class StoreMemoryLance extends MemoryStorage {
|
|
18
|
+
private client: Connection;
|
|
19
|
+
private operations: StoreOperationsLance;
|
|
20
|
+
constructor({ client, operations }: { client: Connection; operations: StoreOperationsLance }) {
|
|
21
|
+
super();
|
|
22
|
+
this.client = client;
|
|
23
|
+
this.operations = operations;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async getThreadById({ threadId }: { threadId: string }): Promise<StorageThreadType | null> {
|
|
27
|
+
try {
|
|
28
|
+
const thread = await this.operations.load({ tableName: TABLE_THREADS, keys: { id: threadId } });
|
|
29
|
+
|
|
30
|
+
if (!thread) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
...thread,
|
|
36
|
+
createdAt: new Date(thread.createdAt),
|
|
37
|
+
updatedAt: new Date(thread.updatedAt),
|
|
38
|
+
};
|
|
39
|
+
} catch (error: any) {
|
|
40
|
+
throw new MastraError(
|
|
41
|
+
{
|
|
42
|
+
id: 'LANCE_STORE_GET_THREAD_BY_ID_FAILED',
|
|
43
|
+
domain: ErrorDomain.STORAGE,
|
|
44
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
45
|
+
},
|
|
46
|
+
error,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async getThreadsByResourceId({ resourceId }: { resourceId: string }): Promise<StorageThreadType[]> {
|
|
52
|
+
try {
|
|
53
|
+
const table = await this.client.openTable(TABLE_THREADS);
|
|
54
|
+
// fetches all threads with the given resourceId
|
|
55
|
+
const query = table.query().where(`\`resourceId\` = '${resourceId}'`);
|
|
56
|
+
|
|
57
|
+
const records = await query.toArray();
|
|
58
|
+
return processResultWithTypeConversion(
|
|
59
|
+
records,
|
|
60
|
+
await getTableSchema({ tableName: TABLE_THREADS, client: this.client }),
|
|
61
|
+
) as StorageThreadType[];
|
|
62
|
+
} catch (error: any) {
|
|
63
|
+
throw new MastraError(
|
|
64
|
+
{
|
|
65
|
+
id: 'LANCE_STORE_GET_THREADS_BY_RESOURCE_ID_FAILED',
|
|
66
|
+
domain: ErrorDomain.STORAGE,
|
|
67
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
68
|
+
},
|
|
69
|
+
error,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Saves a thread to the database. This function doesn't overwrite existing threads.
|
|
76
|
+
* @param thread - The thread to save
|
|
77
|
+
* @returns The saved thread
|
|
78
|
+
*/
|
|
79
|
+
async saveThread({ thread }: { thread: StorageThreadType }): Promise<StorageThreadType> {
|
|
80
|
+
try {
|
|
81
|
+
const record = { ...thread, metadata: JSON.stringify(thread.metadata) };
|
|
82
|
+
const table = await this.client.openTable(TABLE_THREADS);
|
|
83
|
+
await table.add([record], { mode: 'append' });
|
|
84
|
+
|
|
85
|
+
return thread;
|
|
86
|
+
} catch (error: any) {
|
|
87
|
+
throw new MastraError(
|
|
88
|
+
{
|
|
89
|
+
id: 'LANCE_STORE_SAVE_THREAD_FAILED',
|
|
90
|
+
domain: ErrorDomain.STORAGE,
|
|
91
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
92
|
+
},
|
|
93
|
+
error,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async updateThread({
|
|
99
|
+
id,
|
|
100
|
+
title,
|
|
101
|
+
metadata,
|
|
102
|
+
}: {
|
|
103
|
+
id: string;
|
|
104
|
+
title: string;
|
|
105
|
+
metadata: Record<string, unknown>;
|
|
106
|
+
}): Promise<StorageThreadType> {
|
|
107
|
+
const maxRetries = 5;
|
|
108
|
+
|
|
109
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
110
|
+
try {
|
|
111
|
+
// Get current state atomically
|
|
112
|
+
const current = await this.getThreadById({ threadId: id });
|
|
113
|
+
if (!current) {
|
|
114
|
+
throw new Error(`Thread with id ${id} not found`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Merge metadata
|
|
118
|
+
const mergedMetadata = { ...current.metadata, ...metadata };
|
|
119
|
+
|
|
120
|
+
// Update atomically
|
|
121
|
+
const record = {
|
|
122
|
+
id,
|
|
123
|
+
title,
|
|
124
|
+
metadata: JSON.stringify(mergedMetadata),
|
|
125
|
+
updatedAt: new Date().getTime(),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const table = await this.client.openTable(TABLE_THREADS);
|
|
129
|
+
await table.mergeInsert('id').whenMatchedUpdateAll().whenNotMatchedInsertAll().execute([record]);
|
|
130
|
+
|
|
131
|
+
const updatedThread = await this.getThreadById({ threadId: id });
|
|
132
|
+
if (!updatedThread) {
|
|
133
|
+
throw new Error(`Failed to retrieve updated thread ${id}`);
|
|
134
|
+
}
|
|
135
|
+
return updatedThread;
|
|
136
|
+
} catch (error: any) {
|
|
137
|
+
if (error.message?.includes('Commit conflict') && attempt < maxRetries - 1) {
|
|
138
|
+
// Wait with exponential backoff before retrying
|
|
139
|
+
const delay = Math.pow(2, attempt) * 10; // 10ms, 20ms, 40ms
|
|
140
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// If it's not a commit conflict or we've exhausted retries, throw the error
|
|
145
|
+
throw new MastraError(
|
|
146
|
+
{
|
|
147
|
+
id: 'LANCE_STORE_UPDATE_THREAD_FAILED',
|
|
148
|
+
domain: ErrorDomain.STORAGE,
|
|
149
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
150
|
+
},
|
|
151
|
+
error,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// This should never be reached, but just in case
|
|
157
|
+
throw new MastraError(
|
|
158
|
+
{
|
|
159
|
+
id: 'LANCE_STORE_UPDATE_THREAD_FAILED',
|
|
160
|
+
domain: ErrorDomain.STORAGE,
|
|
161
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
162
|
+
},
|
|
163
|
+
new Error('All retries exhausted'),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async deleteThread({ threadId }: { threadId: string }): Promise<void> {
|
|
168
|
+
try {
|
|
169
|
+
// Delete the thread
|
|
170
|
+
const table = await this.client.openTable(TABLE_THREADS);
|
|
171
|
+
await table.delete(`id = '${threadId}'`);
|
|
172
|
+
|
|
173
|
+
// Delete all messages with the matching thread_id
|
|
174
|
+
const messagesTable = await this.client.openTable(TABLE_MESSAGES);
|
|
175
|
+
await messagesTable.delete(`thread_id = '${threadId}'`);
|
|
176
|
+
} catch (error: any) {
|
|
177
|
+
throw new MastraError(
|
|
178
|
+
{
|
|
179
|
+
id: 'LANCE_STORE_DELETE_THREAD_FAILED',
|
|
180
|
+
domain: ErrorDomain.STORAGE,
|
|
181
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
182
|
+
},
|
|
183
|
+
error,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public async getMessages(args: StorageGetMessagesArg & { format?: 'v1' }): Promise<MastraMessageV1[]>;
|
|
189
|
+
public async getMessages(args: StorageGetMessagesArg & { format: 'v2' }): Promise<MastraMessageV2[]>;
|
|
190
|
+
public async getMessages({
|
|
191
|
+
threadId,
|
|
192
|
+
resourceId,
|
|
193
|
+
selectBy,
|
|
194
|
+
format,
|
|
195
|
+
threadConfig,
|
|
196
|
+
}: StorageGetMessagesArg & { format?: 'v1' | 'v2' }): Promise<MastraMessageV1[] | MastraMessageV2[]> {
|
|
197
|
+
try {
|
|
198
|
+
if (threadConfig) {
|
|
199
|
+
throw new Error('ThreadConfig is not supported by LanceDB storage');
|
|
200
|
+
}
|
|
201
|
+
const limit = resolveMessageLimit({ last: selectBy?.last, defaultLimit: Number.MAX_SAFE_INTEGER });
|
|
202
|
+
const table = await this.client.openTable(TABLE_MESSAGES);
|
|
203
|
+
|
|
204
|
+
let allRecords: any[] = [];
|
|
205
|
+
|
|
206
|
+
// Handle selectBy.include for cross-thread context retrieval
|
|
207
|
+
if (selectBy?.include && selectBy.include.length > 0) {
|
|
208
|
+
// Get all unique thread IDs from include items
|
|
209
|
+
const threadIds = [...new Set(selectBy.include.map(item => item.threadId))];
|
|
210
|
+
|
|
211
|
+
// Fetch all messages from all relevant threads
|
|
212
|
+
for (const threadId of threadIds) {
|
|
213
|
+
const threadQuery = table.query().where(`thread_id = '${threadId}'`);
|
|
214
|
+
let threadRecords = await threadQuery.toArray();
|
|
215
|
+
allRecords.push(...threadRecords);
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
// Regular single-thread query
|
|
219
|
+
let query = table.query().where(`\`thread_id\` = '${threadId}'`);
|
|
220
|
+
allRecords = await query.toArray();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Sort the records chronologically
|
|
224
|
+
allRecords.sort((a, b) => {
|
|
225
|
+
const dateA = new Date(a.createdAt).getTime();
|
|
226
|
+
const dateB = new Date(b.createdAt).getTime();
|
|
227
|
+
return dateA - dateB; // Ascending order
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Process the include.withPreviousMessages and include.withNextMessages if specified
|
|
231
|
+
if (selectBy?.include && selectBy.include.length > 0) {
|
|
232
|
+
allRecords = this.processMessagesWithContext(allRecords, selectBy.include);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// If we're fetching the last N messages, take only the last N after sorting
|
|
236
|
+
if (limit !== Number.MAX_SAFE_INTEGER) {
|
|
237
|
+
allRecords = allRecords.slice(-limit);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const messages = processResultWithTypeConversion(
|
|
241
|
+
allRecords,
|
|
242
|
+
await getTableSchema({ tableName: TABLE_MESSAGES, client: this.client }),
|
|
243
|
+
);
|
|
244
|
+
const normalized = messages.map((msg: any) => {
|
|
245
|
+
const { thread_id, ...rest } = msg;
|
|
246
|
+
return {
|
|
247
|
+
...rest,
|
|
248
|
+
threadId: thread_id,
|
|
249
|
+
content:
|
|
250
|
+
typeof msg.content === 'string'
|
|
251
|
+
? (() => {
|
|
252
|
+
try {
|
|
253
|
+
return JSON.parse(msg.content);
|
|
254
|
+
} catch {
|
|
255
|
+
return msg.content;
|
|
256
|
+
}
|
|
257
|
+
})()
|
|
258
|
+
: msg.content,
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
const list = new MessageList({ threadId, resourceId }).add(normalized, 'memory');
|
|
262
|
+
if (format === 'v2') return list.get.all.v2();
|
|
263
|
+
return list.get.all.v1();
|
|
264
|
+
} catch (error: any) {
|
|
265
|
+
throw new MastraError(
|
|
266
|
+
{
|
|
267
|
+
id: 'LANCE_STORE_GET_MESSAGES_FAILED',
|
|
268
|
+
domain: ErrorDomain.STORAGE,
|
|
269
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
270
|
+
},
|
|
271
|
+
error,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async saveMessages(args: { messages: MastraMessageV1[]; format?: undefined | 'v1' }): Promise<MastraMessageV1[]>;
|
|
277
|
+
async saveMessages(args: { messages: MastraMessageV2[]; format: 'v2' }): Promise<MastraMessageV2[]>;
|
|
278
|
+
async saveMessages(
|
|
279
|
+
args: { messages: MastraMessageV1[]; format?: undefined | 'v1' } | { messages: MastraMessageV2[]; format: 'v2' },
|
|
280
|
+
): Promise<MastraMessageV2[] | MastraMessageV1[]> {
|
|
281
|
+
try {
|
|
282
|
+
const { messages, format = 'v1' } = args;
|
|
283
|
+
if (messages.length === 0) {
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const threadId = messages[0]?.threadId;
|
|
288
|
+
|
|
289
|
+
if (!threadId) {
|
|
290
|
+
throw new Error('Thread ID is required');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Validate all messages before saving
|
|
294
|
+
for (const message of messages) {
|
|
295
|
+
if (!message.id) {
|
|
296
|
+
throw new Error('Message ID is required');
|
|
297
|
+
}
|
|
298
|
+
if (!message.threadId) {
|
|
299
|
+
throw new Error('Thread ID is required for all messages');
|
|
300
|
+
}
|
|
301
|
+
if (message.resourceId === null || message.resourceId === undefined) {
|
|
302
|
+
throw new Error('Resource ID cannot be null or undefined');
|
|
303
|
+
}
|
|
304
|
+
if (!message.content) {
|
|
305
|
+
throw new Error('Message content is required');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const transformedMessages = messages.map((message: MastraMessageV2 | MastraMessageV1) => {
|
|
310
|
+
const { threadId, type, ...rest } = message;
|
|
311
|
+
return {
|
|
312
|
+
...rest,
|
|
313
|
+
thread_id: threadId,
|
|
314
|
+
type: type ?? 'v2',
|
|
315
|
+
content: JSON.stringify(message.content),
|
|
316
|
+
};
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const table = await this.client.openTable(TABLE_MESSAGES);
|
|
320
|
+
await table.mergeInsert('id').whenMatchedUpdateAll().whenNotMatchedInsertAll().execute(transformedMessages);
|
|
321
|
+
|
|
322
|
+
// Update the thread's updatedAt timestamp
|
|
323
|
+
const threadsTable = await this.client.openTable(TABLE_THREADS);
|
|
324
|
+
const currentTime = new Date().getTime();
|
|
325
|
+
const updateRecord = { id: threadId, updatedAt: currentTime };
|
|
326
|
+
await threadsTable.mergeInsert('id').whenMatchedUpdateAll().whenNotMatchedInsertAll().execute([updateRecord]);
|
|
327
|
+
|
|
328
|
+
const list = new MessageList().add(messages, 'memory');
|
|
329
|
+
if (format === `v2`) return list.get.all.v2();
|
|
330
|
+
return list.get.all.v1();
|
|
331
|
+
} catch (error: any) {
|
|
332
|
+
throw new MastraError(
|
|
333
|
+
{
|
|
334
|
+
id: 'LANCE_STORE_SAVE_MESSAGES_FAILED',
|
|
335
|
+
domain: ErrorDomain.STORAGE,
|
|
336
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
337
|
+
},
|
|
338
|
+
error,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async getThreadsByResourceIdPaginated(args: {
|
|
344
|
+
resourceId: string;
|
|
345
|
+
page?: number;
|
|
346
|
+
perPage?: number;
|
|
347
|
+
}): Promise<PaginationInfo & { threads: StorageThreadType[] }> {
|
|
348
|
+
try {
|
|
349
|
+
const { resourceId, page = 0, perPage = 10 } = args;
|
|
350
|
+
const table = await this.client.openTable(TABLE_THREADS);
|
|
351
|
+
|
|
352
|
+
// Get total count
|
|
353
|
+
const total = await table.countRows(`\`resourceId\` = '${resourceId}'`);
|
|
354
|
+
|
|
355
|
+
// Get paginated results
|
|
356
|
+
const query = table.query().where(`\`resourceId\` = '${resourceId}'`);
|
|
357
|
+
const offset = page * perPage;
|
|
358
|
+
query.limit(perPage);
|
|
359
|
+
if (offset > 0) {
|
|
360
|
+
query.offset(offset);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const records = await query.toArray();
|
|
364
|
+
|
|
365
|
+
// Sort by updatedAt descending (most recent first)
|
|
366
|
+
records.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
|
367
|
+
|
|
368
|
+
const schema = await getTableSchema({ tableName: TABLE_THREADS, client: this.client });
|
|
369
|
+
const threads = records.map(record => processResultWithTypeConversion(record, schema)) as StorageThreadType[];
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
threads,
|
|
373
|
+
total,
|
|
374
|
+
page,
|
|
375
|
+
perPage,
|
|
376
|
+
hasMore: total > (page + 1) * perPage,
|
|
377
|
+
};
|
|
378
|
+
} catch (error: any) {
|
|
379
|
+
throw new MastraError(
|
|
380
|
+
{
|
|
381
|
+
id: 'LANCE_STORE_GET_THREADS_BY_RESOURCE_ID_PAGINATED_FAILED',
|
|
382
|
+
domain: ErrorDomain.STORAGE,
|
|
383
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
384
|
+
},
|
|
385
|
+
error,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Processes messages to include context messages based on withPreviousMessages and withNextMessages
|
|
392
|
+
* @param records - The sorted array of records to process
|
|
393
|
+
* @param include - The array of include specifications with context parameters
|
|
394
|
+
* @returns The processed array with context messages included
|
|
395
|
+
*/
|
|
396
|
+
private processMessagesWithContext(
|
|
397
|
+
records: any[],
|
|
398
|
+
include: { id: string; withPreviousMessages?: number; withNextMessages?: number }[],
|
|
399
|
+
): any[] {
|
|
400
|
+
const messagesWithContext = include.filter(item => item.withPreviousMessages || item.withNextMessages);
|
|
401
|
+
|
|
402
|
+
if (messagesWithContext.length === 0) {
|
|
403
|
+
return records;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Create a map of message id to index in the sorted array for quick lookup
|
|
407
|
+
const messageIndexMap = new Map<string, number>();
|
|
408
|
+
records.forEach((message, index) => {
|
|
409
|
+
messageIndexMap.set(message.id, index);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Keep track of additional indices to include
|
|
413
|
+
const additionalIndices = new Set<number>();
|
|
414
|
+
|
|
415
|
+
for (const item of messagesWithContext) {
|
|
416
|
+
const messageIndex = messageIndexMap.get(item.id);
|
|
417
|
+
|
|
418
|
+
if (messageIndex !== undefined) {
|
|
419
|
+
// Add previous messages if requested
|
|
420
|
+
if (item.withPreviousMessages) {
|
|
421
|
+
const startIdx = Math.max(0, messageIndex - item.withPreviousMessages);
|
|
422
|
+
for (let i = startIdx; i < messageIndex; i++) {
|
|
423
|
+
additionalIndices.add(i);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Add next messages if requested
|
|
428
|
+
if (item.withNextMessages) {
|
|
429
|
+
const endIdx = Math.min(records.length - 1, messageIndex + item.withNextMessages);
|
|
430
|
+
for (let i = messageIndex + 1; i <= endIdx; i++) {
|
|
431
|
+
additionalIndices.add(i);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// If we need to include additional messages, create a new set of records
|
|
438
|
+
if (additionalIndices.size === 0) {
|
|
439
|
+
return records;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Get IDs of the records that matched the original query
|
|
443
|
+
const originalMatchIds = new Set(include.map(item => item.id));
|
|
444
|
+
|
|
445
|
+
// Create a set of all indices we need to include
|
|
446
|
+
const allIndices = new Set<number>();
|
|
447
|
+
|
|
448
|
+
// Add indices of originally matched messages
|
|
449
|
+
records.forEach((record, index) => {
|
|
450
|
+
if (originalMatchIds.has(record.id)) {
|
|
451
|
+
allIndices.add(index);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Add the additional context message indices
|
|
456
|
+
additionalIndices.forEach(index => {
|
|
457
|
+
allIndices.add(index);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Create a new filtered array with only the required messages
|
|
461
|
+
// while maintaining chronological order
|
|
462
|
+
return Array.from(allIndices)
|
|
463
|
+
.sort((a, b) => a - b)
|
|
464
|
+
.map(index => records[index]);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async getMessagesPaginated(
|
|
468
|
+
args: StorageGetMessagesArg & { format?: 'v1' | 'v2' },
|
|
469
|
+
): Promise<PaginationInfo & { messages: MastraMessageV1[] | MastraMessageV2[] }> {
|
|
470
|
+
try {
|
|
471
|
+
const { threadId, resourceId, selectBy, format = 'v1' } = args;
|
|
472
|
+
|
|
473
|
+
if (!threadId) {
|
|
474
|
+
throw new Error('Thread ID is required for getMessagesPaginated');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Extract pagination and dateRange from selectBy.pagination
|
|
478
|
+
const page = selectBy?.pagination?.page ?? 0;
|
|
479
|
+
const perPage = selectBy?.pagination?.perPage ?? 10;
|
|
480
|
+
const dateRange = selectBy?.pagination?.dateRange;
|
|
481
|
+
const fromDate = dateRange?.start;
|
|
482
|
+
const toDate = dateRange?.end;
|
|
483
|
+
|
|
484
|
+
const table = await this.client.openTable(TABLE_MESSAGES);
|
|
485
|
+
const messages: any[] = [];
|
|
486
|
+
|
|
487
|
+
// Handle selectBy.include first (before pagination)
|
|
488
|
+
if (selectBy?.include && Array.isArray(selectBy.include)) {
|
|
489
|
+
// Get all unique thread IDs from include items
|
|
490
|
+
const threadIds = [...new Set(selectBy.include.map(item => item.threadId))];
|
|
491
|
+
|
|
492
|
+
// Fetch all messages from all relevant threads
|
|
493
|
+
const allThreadMessages: any[] = [];
|
|
494
|
+
for (const threadId of threadIds) {
|
|
495
|
+
const threadQuery = table.query().where(`thread_id = '${threadId}'`);
|
|
496
|
+
let threadRecords = await threadQuery.toArray();
|
|
497
|
+
|
|
498
|
+
// Apply date filtering in JS for context
|
|
499
|
+
if (fromDate) threadRecords = threadRecords.filter(m => m.createdAt >= fromDate.getTime());
|
|
500
|
+
if (toDate) threadRecords = threadRecords.filter(m => m.createdAt <= toDate.getTime());
|
|
501
|
+
|
|
502
|
+
allThreadMessages.push(...threadRecords);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Sort all messages by createdAt
|
|
506
|
+
allThreadMessages.sort((a, b) => a.createdAt - b.createdAt);
|
|
507
|
+
|
|
508
|
+
// Apply processMessagesWithContext to the combined array
|
|
509
|
+
const contextMessages = this.processMessagesWithContext(allThreadMessages, selectBy.include);
|
|
510
|
+
messages.push(...contextMessages);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Build query conditions for the main thread
|
|
514
|
+
const conditions: string[] = [`thread_id = '${threadId}'`];
|
|
515
|
+
if (resourceId) {
|
|
516
|
+
conditions.push(`\`resourceId\` = '${resourceId}'`);
|
|
517
|
+
}
|
|
518
|
+
if (fromDate) {
|
|
519
|
+
conditions.push(`\`createdAt\` >= ${fromDate.getTime()}`);
|
|
520
|
+
}
|
|
521
|
+
if (toDate) {
|
|
522
|
+
conditions.push(`\`createdAt\` <= ${toDate.getTime()}`);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Get total count (excluding already included messages)
|
|
526
|
+
let total = 0;
|
|
527
|
+
if (conditions.length > 0) {
|
|
528
|
+
total = await table.countRows(conditions.join(' AND '));
|
|
529
|
+
} else {
|
|
530
|
+
total = await table.countRows();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// If no messages and no included messages, return empty result
|
|
534
|
+
if (total === 0 && messages.length === 0) {
|
|
535
|
+
return {
|
|
536
|
+
messages: [],
|
|
537
|
+
total: 0,
|
|
538
|
+
page,
|
|
539
|
+
perPage,
|
|
540
|
+
hasMore: false,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Fetch paginated messages (excluding already included ones)
|
|
545
|
+
const excludeIds = messages.map(m => m.id);
|
|
546
|
+
let selectedMessages: any[] = [];
|
|
547
|
+
|
|
548
|
+
if (selectBy?.last && selectBy.last > 0) {
|
|
549
|
+
// Handle selectBy.last: get last N messages for the main thread
|
|
550
|
+
const query = table.query();
|
|
551
|
+
if (conditions.length > 0) {
|
|
552
|
+
query.where(conditions.join(' AND '));
|
|
553
|
+
}
|
|
554
|
+
let records = await query.toArray();
|
|
555
|
+
records = records.sort((a, b) => a.createdAt - b.createdAt);
|
|
556
|
+
|
|
557
|
+
// Exclude already included messages
|
|
558
|
+
if (excludeIds.length > 0) {
|
|
559
|
+
records = records.filter(m => !excludeIds.includes(m.id));
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
selectedMessages = records.slice(-selectBy.last);
|
|
563
|
+
} else {
|
|
564
|
+
// Regular pagination
|
|
565
|
+
const query = table.query();
|
|
566
|
+
if (conditions.length > 0) {
|
|
567
|
+
query.where(conditions.join(' AND '));
|
|
568
|
+
}
|
|
569
|
+
let records = await query.toArray();
|
|
570
|
+
records = records.sort((a, b) => a.createdAt - b.createdAt);
|
|
571
|
+
|
|
572
|
+
// Exclude already included messages
|
|
573
|
+
if (excludeIds.length > 0) {
|
|
574
|
+
records = records.filter(m => !excludeIds.includes(m.id));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
selectedMessages = records.slice(page * perPage, (page + 1) * perPage);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Merge all messages and deduplicate
|
|
581
|
+
const allMessages = [...messages, ...selectedMessages];
|
|
582
|
+
const seen = new Set();
|
|
583
|
+
const dedupedMessages = allMessages.filter(m => {
|
|
584
|
+
const key = `${m.id}:${m.thread_id}`;
|
|
585
|
+
if (seen.has(key)) return false;
|
|
586
|
+
seen.add(key);
|
|
587
|
+
return true;
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// Convert to correct format (v1/v2)
|
|
591
|
+
const formattedMessages = dedupedMessages.map((msg: any) => {
|
|
592
|
+
const { thread_id, ...rest } = msg;
|
|
593
|
+
return {
|
|
594
|
+
...rest,
|
|
595
|
+
threadId: thread_id,
|
|
596
|
+
content:
|
|
597
|
+
typeof msg.content === 'string'
|
|
598
|
+
? (() => {
|
|
599
|
+
try {
|
|
600
|
+
return JSON.parse(msg.content);
|
|
601
|
+
} catch {
|
|
602
|
+
return msg.content;
|
|
603
|
+
}
|
|
604
|
+
})()
|
|
605
|
+
: msg.content,
|
|
606
|
+
};
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
const list = new MessageList().add(formattedMessages, 'memory');
|
|
610
|
+
return {
|
|
611
|
+
messages: format === 'v2' ? list.get.all.v2() : list.get.all.v1(),
|
|
612
|
+
total: total, // Total should be the count of messages matching the filters
|
|
613
|
+
page,
|
|
614
|
+
perPage,
|
|
615
|
+
hasMore: total > (page + 1) * perPage,
|
|
616
|
+
};
|
|
617
|
+
} catch (error: any) {
|
|
618
|
+
throw new MastraError(
|
|
619
|
+
{
|
|
620
|
+
id: 'LANCE_STORE_GET_MESSAGES_PAGINATED_FAILED',
|
|
621
|
+
domain: ErrorDomain.STORAGE,
|
|
622
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
623
|
+
},
|
|
624
|
+
error,
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Parse message data from LanceDB record format to MastraMessageV2 format
|
|
631
|
+
*/
|
|
632
|
+
private parseMessageData(data: any): MastraMessageV2 {
|
|
633
|
+
const { thread_id, ...rest } = data;
|
|
634
|
+
return {
|
|
635
|
+
...rest,
|
|
636
|
+
threadId: thread_id,
|
|
637
|
+
content:
|
|
638
|
+
typeof data.content === 'string'
|
|
639
|
+
? (() => {
|
|
640
|
+
try {
|
|
641
|
+
return JSON.parse(data.content);
|
|
642
|
+
} catch {
|
|
643
|
+
return data.content;
|
|
644
|
+
}
|
|
645
|
+
})()
|
|
646
|
+
: data.content,
|
|
647
|
+
createdAt: new Date(data.createdAt),
|
|
648
|
+
updatedAt: new Date(data.updatedAt),
|
|
649
|
+
} as MastraMessageV2;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async updateMessages(args: {
|
|
653
|
+
messages: Partial<Omit<MastraMessageV2, 'createdAt'>> &
|
|
654
|
+
{
|
|
655
|
+
id: string;
|
|
656
|
+
content?: { metadata?: MastraMessageContentV2['metadata']; content?: MastraMessageContentV2['content'] };
|
|
657
|
+
}[];
|
|
658
|
+
}): Promise<MastraMessageV2[]> {
|
|
659
|
+
const { messages } = args;
|
|
660
|
+
this.logger.debug('Updating messages', { count: messages.length });
|
|
661
|
+
|
|
662
|
+
if (!messages.length) {
|
|
663
|
+
return [];
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const updatedMessages: MastraMessageV2[] = [];
|
|
667
|
+
const affectedThreadIds = new Set<string>();
|
|
668
|
+
|
|
669
|
+
try {
|
|
670
|
+
for (const updateData of messages) {
|
|
671
|
+
const { id, ...updates } = updateData;
|
|
672
|
+
|
|
673
|
+
// Get the existing message
|
|
674
|
+
const existingMessage = await this.operations.load({ tableName: TABLE_MESSAGES, keys: { id } });
|
|
675
|
+
if (!existingMessage) {
|
|
676
|
+
this.logger.warn('Message not found for update', { id });
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const existingMsg = this.parseMessageData(existingMessage);
|
|
681
|
+
const originalThreadId = existingMsg.threadId;
|
|
682
|
+
affectedThreadIds.add(originalThreadId!);
|
|
683
|
+
|
|
684
|
+
// Prepare the update payload
|
|
685
|
+
const updatePayload: any = {};
|
|
686
|
+
|
|
687
|
+
// Handle basic field updates
|
|
688
|
+
if ('role' in updates && updates.role !== undefined) updatePayload.role = updates.role;
|
|
689
|
+
if ('type' in updates && updates.type !== undefined) updatePayload.type = updates.type;
|
|
690
|
+
if ('resourceId' in updates && updates.resourceId !== undefined) updatePayload.resourceId = updates.resourceId;
|
|
691
|
+
if ('threadId' in updates && updates.threadId !== undefined && updates.threadId !== null) {
|
|
692
|
+
updatePayload.thread_id = updates.threadId;
|
|
693
|
+
affectedThreadIds.add(updates.threadId as string);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Handle content updates
|
|
697
|
+
if (updates.content) {
|
|
698
|
+
const existingContent = existingMsg.content;
|
|
699
|
+
let newContent = { ...existingContent };
|
|
700
|
+
|
|
701
|
+
// Deep merge metadata if provided
|
|
702
|
+
if (updates.content.metadata !== undefined) {
|
|
703
|
+
newContent.metadata = {
|
|
704
|
+
...(existingContent.metadata || {}),
|
|
705
|
+
...(updates.content.metadata || {}),
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Update content string if provided
|
|
710
|
+
if (updates.content.content !== undefined) {
|
|
711
|
+
newContent.content = updates.content.content;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Update parts if provided (only if it exists in the content type)
|
|
715
|
+
if ('parts' in updates.content && updates.content.parts !== undefined) {
|
|
716
|
+
(newContent as any).parts = updates.content.parts;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
updatePayload.content = JSON.stringify(newContent);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Update the message using merge insert
|
|
723
|
+
await this.operations.insert({ tableName: TABLE_MESSAGES, record: { id, ...updatePayload } });
|
|
724
|
+
|
|
725
|
+
// Get the updated message
|
|
726
|
+
const updatedMessage = await this.operations.load({ tableName: TABLE_MESSAGES, keys: { id } });
|
|
727
|
+
if (updatedMessage) {
|
|
728
|
+
updatedMessages.push(this.parseMessageData(updatedMessage));
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Update timestamps for all affected threads
|
|
733
|
+
for (const threadId of affectedThreadIds) {
|
|
734
|
+
await this.operations.insert({
|
|
735
|
+
tableName: TABLE_THREADS,
|
|
736
|
+
record: { id: threadId, updatedAt: Date.now() },
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return updatedMessages;
|
|
741
|
+
} catch (error: any) {
|
|
742
|
+
throw new MastraError(
|
|
743
|
+
{
|
|
744
|
+
id: 'LANCE_STORE_UPDATE_MESSAGES_FAILED',
|
|
745
|
+
domain: ErrorDomain.STORAGE,
|
|
746
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
747
|
+
details: { count: messages.length },
|
|
748
|
+
},
|
|
749
|
+
error,
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
async getResourceById({ resourceId }: { resourceId: string }): Promise<StorageResourceType | null> {
|
|
755
|
+
try {
|
|
756
|
+
const resource = await this.operations.load({ tableName: TABLE_RESOURCES, keys: { id: resourceId } });
|
|
757
|
+
|
|
758
|
+
if (!resource) {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Handle date conversion - LanceDB stores timestamps as numbers
|
|
763
|
+
let createdAt: Date;
|
|
764
|
+
let updatedAt: Date;
|
|
765
|
+
|
|
766
|
+
// Convert ISO strings back to Date objects with error handling
|
|
767
|
+
try {
|
|
768
|
+
// If createdAt is already a Date object, use it directly
|
|
769
|
+
if (resource.createdAt instanceof Date) {
|
|
770
|
+
createdAt = resource.createdAt;
|
|
771
|
+
} else if (typeof resource.createdAt === 'string') {
|
|
772
|
+
// If it's an ISO string, parse it
|
|
773
|
+
createdAt = new Date(resource.createdAt);
|
|
774
|
+
} else if (typeof resource.createdAt === 'number') {
|
|
775
|
+
// If it's a timestamp, convert it to Date
|
|
776
|
+
createdAt = new Date(resource.createdAt);
|
|
777
|
+
} else {
|
|
778
|
+
// If it's null or undefined, use current date
|
|
779
|
+
createdAt = new Date();
|
|
780
|
+
}
|
|
781
|
+
if (isNaN(createdAt.getTime())) {
|
|
782
|
+
createdAt = new Date(); // Fallback to current date if invalid
|
|
783
|
+
}
|
|
784
|
+
} catch {
|
|
785
|
+
createdAt = new Date(); // Fallback to current date if conversion fails
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
try {
|
|
789
|
+
// If updatedAt is already a Date object, use it directly
|
|
790
|
+
if (resource.updatedAt instanceof Date) {
|
|
791
|
+
updatedAt = resource.updatedAt;
|
|
792
|
+
} else if (typeof resource.updatedAt === 'string') {
|
|
793
|
+
// If it's an ISO string, parse it
|
|
794
|
+
updatedAt = new Date(resource.updatedAt);
|
|
795
|
+
} else if (typeof resource.updatedAt === 'number') {
|
|
796
|
+
// If it's a timestamp, convert it to Date
|
|
797
|
+
updatedAt = new Date(resource.updatedAt);
|
|
798
|
+
} else {
|
|
799
|
+
// If it's null or undefined, use current date
|
|
800
|
+
updatedAt = new Date();
|
|
801
|
+
}
|
|
802
|
+
if (isNaN(updatedAt.getTime())) {
|
|
803
|
+
updatedAt = new Date(); // Fallback to current date if invalid
|
|
804
|
+
}
|
|
805
|
+
} catch {
|
|
806
|
+
updatedAt = new Date(); // Fallback to current date if conversion fails
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Handle workingMemory - return undefined for null/undefined, empty string for empty string
|
|
810
|
+
let workingMemory = resource.workingMemory;
|
|
811
|
+
if (workingMemory === null || workingMemory === undefined) {
|
|
812
|
+
workingMemory = undefined;
|
|
813
|
+
} else if (workingMemory === '') {
|
|
814
|
+
workingMemory = ''; // Return empty string for empty strings to match test expectations
|
|
815
|
+
} else if (typeof workingMemory === 'object') {
|
|
816
|
+
workingMemory = JSON.stringify(workingMemory);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Handle metadata - return undefined for empty strings, parse JSON safely
|
|
820
|
+
let metadata = resource.metadata;
|
|
821
|
+
if (metadata === '' || metadata === null || metadata === undefined) {
|
|
822
|
+
metadata = undefined;
|
|
823
|
+
} else if (typeof metadata === 'string') {
|
|
824
|
+
try {
|
|
825
|
+
metadata = JSON.parse(metadata);
|
|
826
|
+
} catch {
|
|
827
|
+
// If JSON parsing fails, return the original string
|
|
828
|
+
metadata = metadata;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return {
|
|
833
|
+
...resource,
|
|
834
|
+
createdAt,
|
|
835
|
+
updatedAt,
|
|
836
|
+
workingMemory,
|
|
837
|
+
metadata,
|
|
838
|
+
} as StorageResourceType;
|
|
839
|
+
} catch (error: any) {
|
|
840
|
+
throw new MastraError(
|
|
841
|
+
{
|
|
842
|
+
id: 'LANCE_STORE_GET_RESOURCE_BY_ID_FAILED',
|
|
843
|
+
domain: ErrorDomain.STORAGE,
|
|
844
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
845
|
+
},
|
|
846
|
+
error,
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async saveResource({ resource }: { resource: StorageResourceType }): Promise<StorageResourceType> {
|
|
852
|
+
try {
|
|
853
|
+
const record = {
|
|
854
|
+
...resource,
|
|
855
|
+
metadata: resource.metadata ? JSON.stringify(resource.metadata) : '',
|
|
856
|
+
createdAt: resource.createdAt.getTime(), // Store as timestamp (milliseconds)
|
|
857
|
+
updatedAt: resource.updatedAt.getTime(), // Store as timestamp (milliseconds)
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
const table = await this.client.openTable(TABLE_RESOURCES);
|
|
861
|
+
await table.add([record], { mode: 'append' });
|
|
862
|
+
|
|
863
|
+
return resource;
|
|
864
|
+
} catch (error: any) {
|
|
865
|
+
throw new MastraError(
|
|
866
|
+
{
|
|
867
|
+
id: 'LANCE_STORE_SAVE_RESOURCE_FAILED',
|
|
868
|
+
domain: ErrorDomain.STORAGE,
|
|
869
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
870
|
+
},
|
|
871
|
+
error,
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
async updateResource({
|
|
877
|
+
resourceId,
|
|
878
|
+
workingMemory,
|
|
879
|
+
metadata,
|
|
880
|
+
}: {
|
|
881
|
+
resourceId: string;
|
|
882
|
+
workingMemory?: string;
|
|
883
|
+
metadata?: Record<string, unknown>;
|
|
884
|
+
}): Promise<StorageResourceType> {
|
|
885
|
+
const maxRetries = 3;
|
|
886
|
+
|
|
887
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
888
|
+
try {
|
|
889
|
+
const existingResource = await this.getResourceById({ resourceId });
|
|
890
|
+
|
|
891
|
+
if (!existingResource) {
|
|
892
|
+
// Create new resource if it doesn't exist
|
|
893
|
+
const newResource: StorageResourceType = {
|
|
894
|
+
id: resourceId,
|
|
895
|
+
workingMemory,
|
|
896
|
+
metadata: metadata || {},
|
|
897
|
+
createdAt: new Date(),
|
|
898
|
+
updatedAt: new Date(),
|
|
899
|
+
};
|
|
900
|
+
return this.saveResource({ resource: newResource });
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const updatedResource = {
|
|
904
|
+
...existingResource,
|
|
905
|
+
workingMemory: workingMemory !== undefined ? workingMemory : existingResource.workingMemory,
|
|
906
|
+
metadata: {
|
|
907
|
+
...existingResource.metadata,
|
|
908
|
+
...metadata,
|
|
909
|
+
},
|
|
910
|
+
updatedAt: new Date(),
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
const record = {
|
|
914
|
+
id: resourceId,
|
|
915
|
+
workingMemory: updatedResource.workingMemory || '',
|
|
916
|
+
metadata: updatedResource.metadata ? JSON.stringify(updatedResource.metadata) : '',
|
|
917
|
+
updatedAt: updatedResource.updatedAt.getTime(), // Store as timestamp (milliseconds)
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
const table = await this.client.openTable(TABLE_RESOURCES);
|
|
921
|
+
await table.mergeInsert('id').whenMatchedUpdateAll().whenNotMatchedInsertAll().execute([record]);
|
|
922
|
+
|
|
923
|
+
return updatedResource;
|
|
924
|
+
} catch (error: any) {
|
|
925
|
+
if (error.message?.includes('Commit conflict') && attempt < maxRetries - 1) {
|
|
926
|
+
// Wait with exponential backoff before retrying
|
|
927
|
+
const delay = Math.pow(2, attempt) * 10; // 10ms, 20ms, 40ms
|
|
928
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// If it's not a commit conflict or we've exhausted retries, throw the error
|
|
933
|
+
throw new MastraError(
|
|
934
|
+
{
|
|
935
|
+
id: 'LANCE_STORE_UPDATE_RESOURCE_FAILED',
|
|
936
|
+
domain: ErrorDomain.STORAGE,
|
|
937
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
938
|
+
},
|
|
939
|
+
error,
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// This should never be reached, but TypeScript requires it
|
|
945
|
+
throw new Error('Unexpected end of retry loop');
|
|
946
|
+
}
|
|
947
|
+
}
|