@mastra/libsql 0.10.3-alpha.0 → 0.10.4-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1,23 @@
1
1
 
2
- > @mastra/libsql@0.10.3-alpha.0 build /home/runner/work/mastra/mastra/stores/libsql
2
+ > @mastra/libsql@0.10.4-alpha.0 build /home/runner/work/mastra/mastra/stores/libsql
3
3
  > tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting
4
4
 
5
5
  CLI Building entry: src/index.ts
6
6
  CLI Using tsconfig: tsconfig.json
7
7
  CLI tsup v8.5.0
8
8
  TSC Build start
9
- TSC ⚡️ Build success in 9305ms
9
+ TSC ⚡️ Build success in 10773ms
10
10
  DTS Build start
11
11
  CLI Target: es2022
12
12
  Analysis will use the bundled TypeScript version 5.8.3
13
13
  Writing package typings: /home/runner/work/mastra/mastra/stores/libsql/dist/_tsup-dts-rollup.d.ts
14
14
  Analysis will use the bundled TypeScript version 5.8.3
15
15
  Writing package typings: /home/runner/work/mastra/mastra/stores/libsql/dist/_tsup-dts-rollup.d.cts
16
- DTS ⚡️ Build success in 12455ms
16
+ DTS ⚡️ Build success in 11908ms
17
17
  CLI Cleaning output folder
18
18
  ESM Build start
19
19
  CJS Build start
20
- CJS dist/index.cjs 58.84 KB
21
- CJS ⚡️ Build success in 1634ms
22
- ESM dist/index.js 58.55 KB
23
- ESM ⚡️ Build success in 1636ms
20
+ ESM dist/index.js 61.63 KB
21
+ ESM ⚡️ Build success in 1696ms
22
+ CJS dist/index.cjs 61.96 KB
23
+ CJS ⚡️ Build success in 1707ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # @mastra/libsql
2
2
 
3
+ ## 0.10.4-alpha.0
4
+
5
+ ### Patch Changes
6
+
7
+ - d8f2d19: Add updateMessages API to storage classes (only support for PG and LibSQL for now) and to memory class. Additionally allow for metadata to be saved in the content field of a message.
8
+ - Updated dependencies [d8f2d19]
9
+ - Updated dependencies [9d52b17]
10
+ - Updated dependencies [8ba1b51]
11
+ - @mastra/core@0.10.7-alpha.0
12
+
13
+ ## 0.10.3
14
+
15
+ ### Patch Changes
16
+
17
+ - 63f6b7d: dependencies updates:
18
+ - Updated dependency [`@libsql/client@^0.15.9` ↗︎](https://www.npmjs.com/package/@libsql/client/v/0.15.9) (from `^0.15.8`, in `dependencies`)
19
+ - Updated dependencies [63f6b7d]
20
+ - Updated dependencies [12a95fc]
21
+ - Updated dependencies [4b0f8a6]
22
+ - Updated dependencies [51264a5]
23
+ - Updated dependencies [8e6f677]
24
+ - Updated dependencies [d70c420]
25
+ - Updated dependencies [ee9af57]
26
+ - Updated dependencies [36f1c36]
27
+ - Updated dependencies [2a16996]
28
+ - Updated dependencies [10d352e]
29
+ - Updated dependencies [9589624]
30
+ - Updated dependencies [53d3c37]
31
+ - Updated dependencies [751c894]
32
+ - Updated dependencies [577ce3a]
33
+ - Updated dependencies [9260b3a]
34
+ - @mastra/core@0.10.6
35
+
3
36
  ## 0.10.3-alpha.0
4
37
 
5
38
  ### Patch Changes
@@ -6,6 +6,7 @@ import type { DescribeIndexParams } from '@mastra/core/vector';
6
6
  import type { EvalRow } from '@mastra/core/storage';
7
7
  import type { IndexStats } from '@mastra/core/vector';
8
8
  import type { InValue } from '@libsql/client';
9
+ import type { MastraMessageContentV2 } from '@mastra/core/agent';
9
10
  import type { MastraMessageV1 } from '@mastra/core/memory';
10
11
  import type { MastraMessageV2 } from '@mastra/core/agent';
11
12
  import { MastraStorage } from '@mastra/core/storage';
@@ -172,6 +173,15 @@ declare class LibSQLStore extends MastraStorage {
172
173
  messages: MastraMessageV2[];
173
174
  format: 'v2';
174
175
  }): Promise<MastraMessageV2[]>;
176
+ updateMessages({ messages, }: {
177
+ messages: (Partial<Omit<MastraMessageV2, 'createdAt'>> & {
178
+ id: string;
179
+ content?: {
180
+ metadata?: MastraMessageContentV2['metadata'];
181
+ content?: MastraMessageContentV2['content'];
182
+ };
183
+ })[];
184
+ }): Promise<MastraMessageV2[]>;
175
185
  private transformEvalRow;
176
186
  /** @deprecated use getEvals instead */
177
187
  getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]>;
@@ -6,6 +6,7 @@ import type { DescribeIndexParams } from '@mastra/core/vector';
6
6
  import type { EvalRow } from '@mastra/core/storage';
7
7
  import type { IndexStats } from '@mastra/core/vector';
8
8
  import type { InValue } from '@libsql/client';
9
+ import type { MastraMessageContentV2 } from '@mastra/core/agent';
9
10
  import type { MastraMessageV1 } from '@mastra/core/memory';
10
11
  import type { MastraMessageV2 } from '@mastra/core/agent';
11
12
  import { MastraStorage } from '@mastra/core/storage';
@@ -172,6 +173,15 @@ declare class LibSQLStore extends MastraStorage {
172
173
  messages: MastraMessageV2[];
173
174
  format: 'v2';
174
175
  }): Promise<MastraMessageV2[]>;
176
+ updateMessages({ messages, }: {
177
+ messages: (Partial<Omit<MastraMessageV2, 'createdAt'>> & {
178
+ id: string;
179
+ content?: {
180
+ metadata?: MastraMessageContentV2['metadata'];
181
+ content?: MastraMessageContentV2['content'];
182
+ };
183
+ })[];
184
+ }): Promise<MastraMessageV2[]>;
175
185
  private transformEvalRow;
176
186
  /** @deprecated use getEvals instead */
177
187
  getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]>;
package/dist/index.cjs CHANGED
@@ -1351,6 +1351,85 @@ var LibSQLStore = class extends storage.MastraStorage {
1351
1351
  throw error;
1352
1352
  }
1353
1353
  }
1354
+ async updateMessages({
1355
+ messages
1356
+ }) {
1357
+ if (messages.length === 0) {
1358
+ return [];
1359
+ }
1360
+ const messageIds = messages.map((m) => m.id);
1361
+ const placeholders = messageIds.map(() => "?").join(",");
1362
+ const selectSql = `SELECT * FROM ${storage.TABLE_MESSAGES} WHERE id IN (${placeholders})`;
1363
+ const existingResult = await this.client.execute({ sql: selectSql, args: messageIds });
1364
+ const existingMessages = existingResult.rows.map((row) => this.parseRow(row));
1365
+ if (existingMessages.length === 0) {
1366
+ return [];
1367
+ }
1368
+ const batchStatements = [];
1369
+ const threadIdsToUpdate = /* @__PURE__ */ new Set();
1370
+ const columnMapping = {
1371
+ threadId: "thread_id"
1372
+ };
1373
+ for (const existingMessage of existingMessages) {
1374
+ const updatePayload = messages.find((m) => m.id === existingMessage.id);
1375
+ if (!updatePayload) continue;
1376
+ const { id, ...fieldsToUpdate } = updatePayload;
1377
+ if (Object.keys(fieldsToUpdate).length === 0) continue;
1378
+ threadIdsToUpdate.add(existingMessage.threadId);
1379
+ if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) {
1380
+ threadIdsToUpdate.add(updatePayload.threadId);
1381
+ }
1382
+ const setClauses = [];
1383
+ const args = [];
1384
+ const updatableFields = { ...fieldsToUpdate };
1385
+ if (updatableFields.content) {
1386
+ const newContent = {
1387
+ ...existingMessage.content,
1388
+ ...updatableFields.content,
1389
+ // Deep merge metadata if it exists on both
1390
+ ...existingMessage.content?.metadata && updatableFields.content.metadata ? {
1391
+ metadata: {
1392
+ ...existingMessage.content.metadata,
1393
+ ...updatableFields.content.metadata
1394
+ }
1395
+ } : {}
1396
+ };
1397
+ setClauses.push(`${utils.parseSqlIdentifier("content", "column name")} = ?`);
1398
+ args.push(JSON.stringify(newContent));
1399
+ delete updatableFields.content;
1400
+ }
1401
+ for (const key in updatableFields) {
1402
+ if (Object.prototype.hasOwnProperty.call(updatableFields, key)) {
1403
+ const dbKey = columnMapping[key] || key;
1404
+ setClauses.push(`${utils.parseSqlIdentifier(dbKey, "column name")} = ?`);
1405
+ let value = updatableFields[key];
1406
+ if (typeof value === "object" && value !== null) {
1407
+ value = JSON.stringify(value);
1408
+ }
1409
+ args.push(value);
1410
+ }
1411
+ }
1412
+ if (setClauses.length === 0) continue;
1413
+ args.push(id);
1414
+ const sql = `UPDATE ${storage.TABLE_MESSAGES} SET ${setClauses.join(", ")} WHERE id = ?`;
1415
+ batchStatements.push({ sql, args });
1416
+ }
1417
+ if (batchStatements.length === 0) {
1418
+ return existingMessages;
1419
+ }
1420
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1421
+ for (const threadId of threadIdsToUpdate) {
1422
+ if (threadId) {
1423
+ batchStatements.push({
1424
+ sql: `UPDATE ${storage.TABLE_THREADS} SET updatedAt = ? WHERE id = ?`,
1425
+ args: [now, threadId]
1426
+ });
1427
+ }
1428
+ }
1429
+ await this.client.batch(batchStatements, "write");
1430
+ const updatedResult = await this.client.execute({ sql: selectSql, args: messageIds });
1431
+ return updatedResult.rows.map((row) => this.parseRow(row));
1432
+ }
1354
1433
  transformEvalRow(row) {
1355
1434
  const resultValue = JSON.parse(row.result);
1356
1435
  const testInfoValue = row.test_info ? JSON.parse(row.test_info) : void 0;
package/dist/index.js CHANGED
@@ -1349,6 +1349,85 @@ var LibSQLStore = class extends MastraStorage {
1349
1349
  throw error;
1350
1350
  }
1351
1351
  }
1352
+ async updateMessages({
1353
+ messages
1354
+ }) {
1355
+ if (messages.length === 0) {
1356
+ return [];
1357
+ }
1358
+ const messageIds = messages.map((m) => m.id);
1359
+ const placeholders = messageIds.map(() => "?").join(",");
1360
+ const selectSql = `SELECT * FROM ${TABLE_MESSAGES} WHERE id IN (${placeholders})`;
1361
+ const existingResult = await this.client.execute({ sql: selectSql, args: messageIds });
1362
+ const existingMessages = existingResult.rows.map((row) => this.parseRow(row));
1363
+ if (existingMessages.length === 0) {
1364
+ return [];
1365
+ }
1366
+ const batchStatements = [];
1367
+ const threadIdsToUpdate = /* @__PURE__ */ new Set();
1368
+ const columnMapping = {
1369
+ threadId: "thread_id"
1370
+ };
1371
+ for (const existingMessage of existingMessages) {
1372
+ const updatePayload = messages.find((m) => m.id === existingMessage.id);
1373
+ if (!updatePayload) continue;
1374
+ const { id, ...fieldsToUpdate } = updatePayload;
1375
+ if (Object.keys(fieldsToUpdate).length === 0) continue;
1376
+ threadIdsToUpdate.add(existingMessage.threadId);
1377
+ if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) {
1378
+ threadIdsToUpdate.add(updatePayload.threadId);
1379
+ }
1380
+ const setClauses = [];
1381
+ const args = [];
1382
+ const updatableFields = { ...fieldsToUpdate };
1383
+ if (updatableFields.content) {
1384
+ const newContent = {
1385
+ ...existingMessage.content,
1386
+ ...updatableFields.content,
1387
+ // Deep merge metadata if it exists on both
1388
+ ...existingMessage.content?.metadata && updatableFields.content.metadata ? {
1389
+ metadata: {
1390
+ ...existingMessage.content.metadata,
1391
+ ...updatableFields.content.metadata
1392
+ }
1393
+ } : {}
1394
+ };
1395
+ setClauses.push(`${parseSqlIdentifier("content", "column name")} = ?`);
1396
+ args.push(JSON.stringify(newContent));
1397
+ delete updatableFields.content;
1398
+ }
1399
+ for (const key in updatableFields) {
1400
+ if (Object.prototype.hasOwnProperty.call(updatableFields, key)) {
1401
+ const dbKey = columnMapping[key] || key;
1402
+ setClauses.push(`${parseSqlIdentifier(dbKey, "column name")} = ?`);
1403
+ let value = updatableFields[key];
1404
+ if (typeof value === "object" && value !== null) {
1405
+ value = JSON.stringify(value);
1406
+ }
1407
+ args.push(value);
1408
+ }
1409
+ }
1410
+ if (setClauses.length === 0) continue;
1411
+ args.push(id);
1412
+ const sql = `UPDATE ${TABLE_MESSAGES} SET ${setClauses.join(", ")} WHERE id = ?`;
1413
+ batchStatements.push({ sql, args });
1414
+ }
1415
+ if (batchStatements.length === 0) {
1416
+ return existingMessages;
1417
+ }
1418
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1419
+ for (const threadId of threadIdsToUpdate) {
1420
+ if (threadId) {
1421
+ batchStatements.push({
1422
+ sql: `UPDATE ${TABLE_THREADS} SET updatedAt = ? WHERE id = ?`,
1423
+ args: [now, threadId]
1424
+ });
1425
+ }
1426
+ }
1427
+ await this.client.batch(batchStatements, "write");
1428
+ const updatedResult = await this.client.execute({ sql: selectSql, args: messageIds });
1429
+ return updatedResult.rows.map((row) => this.parseRow(row));
1430
+ }
1352
1431
  transformEvalRow(row) {
1353
1432
  const resultValue = JSON.parse(row.result);
1354
1433
  const testInfoValue = row.test_info ? JSON.parse(row.test_info) : void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/libsql",
3
- "version": "0.10.3-alpha.0",
3
+ "version": "0.10.4-alpha.0",
4
4
  "description": "Libsql provider for Mastra - includes both vector and db storage capabilities",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,9 +29,9 @@
29
29
  "tsup": "^8.5.0",
30
30
  "typescript": "^5.8.3",
31
31
  "vitest": "^3.2.3",
32
- "@internal/lint": "0.0.12",
33
- "@internal/storage-test-utils": "0.0.8",
34
- "@mastra/core": "0.10.6-alpha.0"
32
+ "@internal/storage-test-utils": "0.0.9",
33
+ "@internal/lint": "0.0.13",
34
+ "@mastra/core": "0.10.7-alpha.0"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@mastra/core": ">=0.10.4-0 <0.11.0"
@@ -8,6 +8,7 @@ import {
8
8
  resetRole,
9
9
  } from '@internal/storage-test-utils';
10
10
  import type { MastraMessageV1, StorageThreadType } from '@mastra/core';
11
+ import type { MastraMessageV2, MastraMessageContentV2 } from '@mastra/core/agent';
11
12
  import { Mastra } from '@mastra/core/mastra';
12
13
  import { TABLE_EVALS, TABLE_TRACES, TABLE_MESSAGES, TABLE_THREADS } from '@mastra/core/storage';
13
14
  import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
@@ -362,3 +363,196 @@ describe('LibSQLStore Pagination Features', () => {
362
363
  });
363
364
  });
364
365
  });
366
+
367
+ describe('LibSQLStore updateMessages', () => {
368
+ let store: LibSQLStore;
369
+ let thread: StorageThreadType;
370
+
371
+ const createSampleMessageV2 = ({
372
+ threadId,
373
+ resourceId,
374
+ role = 'user',
375
+ content,
376
+ createdAt,
377
+ }: {
378
+ threadId: string;
379
+ resourceId?: string;
380
+ role?: 'user' | 'assistant';
381
+ content?: Partial<MastraMessageContentV2>;
382
+ createdAt?: Date;
383
+ }): MastraMessageV2 => {
384
+ return {
385
+ id: randomUUID(),
386
+ threadId,
387
+ resourceId: resourceId || thread.resourceId,
388
+ role,
389
+ createdAt: createdAt || new Date(),
390
+ content: {
391
+ format: 2,
392
+ parts: content?.parts || [],
393
+ content: content?.content || `Sample content ${randomUUID()}`,
394
+ ...content,
395
+ },
396
+ type: 'v2',
397
+ };
398
+ };
399
+
400
+ beforeAll(async () => {
401
+ store = libsql;
402
+ });
403
+
404
+ beforeEach(async () => {
405
+ await store.clearTable({ tableName: TABLE_MESSAGES });
406
+ await store.clearTable({ tableName: TABLE_THREADS });
407
+ const threadData = createSampleThread();
408
+ thread = await store.saveThread({ thread: threadData as StorageThreadType });
409
+ });
410
+
411
+ it('should update a single field of a message (e.g., role)', async () => {
412
+ const originalMessage = createSampleMessageV2({ threadId: thread.id, role: 'user' });
413
+ await store.saveMessages({ messages: [originalMessage], format: 'v2' });
414
+
415
+ const updatedMessages = await store.updateMessages({
416
+ messages: [{ id: originalMessage.id, role: 'assistant' }],
417
+ });
418
+
419
+ expect(updatedMessages).toHaveLength(1);
420
+ expect(updatedMessages[0].role).toBe('assistant');
421
+
422
+ const fromDb = await store.getMessages({ threadId: thread.id, format: 'v2' });
423
+ expect(fromDb[0].role).toBe('assistant');
424
+ });
425
+
426
+ it('should update only the metadata within the content field, preserving other content fields', async () => {
427
+ const originalMessage = createSampleMessageV2({
428
+ threadId: thread.id,
429
+ content: { content: 'hello world', parts: [{ type: 'text', text: 'hello world' }] },
430
+ });
431
+ await store.saveMessages({ messages: [originalMessage], format: 'v2' });
432
+
433
+ const newMetadata = { someKey: 'someValue' };
434
+ await store.updateMessages({
435
+ messages: [{ id: originalMessage.id, content: { metadata: newMetadata } as any }],
436
+ });
437
+
438
+ const fromDb = await store.getMessages({ threadId: thread.id, format: 'v2' });
439
+ expect(fromDb).toHaveLength(1);
440
+ expect(fromDb[0].content.metadata).toEqual(newMetadata);
441
+ expect(fromDb[0].content.content).toBe('hello world');
442
+ expect(fromDb[0].content.parts).toEqual([{ type: 'text', text: 'hello world' }]);
443
+ });
444
+
445
+ it('should update only the content string within the content field, preserving metadata', async () => {
446
+ const originalMessage = createSampleMessageV2({
447
+ threadId: thread.id,
448
+ content: { metadata: { initial: true } },
449
+ });
450
+ await store.saveMessages({ messages: [originalMessage], format: 'v2' });
451
+
452
+ const newContentString = 'This is the new content string';
453
+ await store.updateMessages({
454
+ messages: [{ id: originalMessage.id, content: { content: newContentString } as any }],
455
+ });
456
+
457
+ const fromDb = await store.getMessages({ threadId: thread.id, format: 'v2' });
458
+ expect(fromDb[0].content.content).toBe(newContentString);
459
+ expect(fromDb[0].content.metadata).toEqual({ initial: true });
460
+ });
461
+
462
+ it('should deep merge metadata, not overwrite it', async () => {
463
+ const originalMessage = createSampleMessageV2({
464
+ threadId: thread.id,
465
+ content: { metadata: { initial: true }, content: 'old content' },
466
+ });
467
+ await store.saveMessages({ messages: [originalMessage], format: 'v2' });
468
+
469
+ const newMetadata = { updated: true };
470
+ await store.updateMessages({
471
+ messages: [{ id: originalMessage.id, content: { metadata: newMetadata } as any }],
472
+ });
473
+
474
+ const fromDb = await store.getMessages({ threadId: thread.id, format: 'v2' });
475
+ expect(fromDb[0].content.content).toBe('old content');
476
+ expect(fromDb[0].content.metadata).toEqual({ initial: true, updated: true });
477
+ });
478
+
479
+ it('should update multiple messages at once', async () => {
480
+ const msg1 = createSampleMessageV2({ threadId: thread.id, role: 'user' });
481
+ const msg2 = createSampleMessageV2({ threadId: thread.id, content: { content: 'original' } });
482
+ await store.saveMessages({ messages: [msg1, msg2], format: 'v2' });
483
+
484
+ await store.updateMessages({
485
+ messages: [
486
+ { id: msg1.id, role: 'assistant' },
487
+ { id: msg2.id, content: { content: 'updated' } as any },
488
+ ],
489
+ });
490
+
491
+ const fromDb = await store.getMessages({ threadId: thread.id, format: 'v2' });
492
+ const updatedMsg1 = fromDb.find(m => m.id === msg1.id)!;
493
+ const updatedMsg2 = fromDb.find(m => m.id === msg2.id)!;
494
+
495
+ expect(updatedMsg1.role).toBe('assistant');
496
+ expect(updatedMsg2.content.content).toBe('updated');
497
+ });
498
+
499
+ it('should update the parent thread updatedAt timestamp', async () => {
500
+ const originalMessage = createSampleMessageV2({ threadId: thread.id });
501
+ await store.saveMessages({ messages: [originalMessage], format: 'v2' });
502
+ const initialThread = await store.getThreadById({ threadId: thread.id });
503
+
504
+ await new Promise(r => setTimeout(r, 10));
505
+
506
+ await store.updateMessages({ messages: [{ id: originalMessage.id, role: 'assistant' }] });
507
+
508
+ const updatedThread = await store.getThreadById({ threadId: thread.id });
509
+
510
+ expect(new Date(updatedThread!.updatedAt).getTime()).toBeGreaterThan(new Date(initialThread!.updatedAt).getTime());
511
+ });
512
+
513
+ it('should update timestamps on both threads when moving a message', async () => {
514
+ const thread2 = await store.saveThread({ thread: createSampleThread() });
515
+ const message = createSampleMessageV2({ threadId: thread.id });
516
+ await store.saveMessages({ messages: [message], format: 'v2' });
517
+
518
+ const initialThread1 = await store.getThreadById({ threadId: thread.id });
519
+ const initialThread2 = await store.getThreadById({ threadId: thread2.id });
520
+
521
+ await new Promise(r => setTimeout(r, 10));
522
+
523
+ await store.updateMessages({
524
+ messages: [{ id: message.id, threadId: thread2.id }],
525
+ });
526
+
527
+ const updatedThread1 = await store.getThreadById({ threadId: thread.id });
528
+ const updatedThread2 = await store.getThreadById({ threadId: thread2.id });
529
+
530
+ expect(new Date(updatedThread1!.updatedAt).getTime()).toBeGreaterThan(
531
+ new Date(initialThread1!.updatedAt).getTime(),
532
+ );
533
+ expect(new Date(updatedThread2!.updatedAt).getTime()).toBeGreaterThan(
534
+ new Date(initialThread2!.updatedAt).getTime(),
535
+ );
536
+
537
+ // Verify the message was moved
538
+ const thread1Messages = await store.getMessages({ threadId: thread.id, format: 'v2' });
539
+ const thread2Messages = await store.getMessages({ threadId: thread2.id, format: 'v2' });
540
+ expect(thread1Messages).toHaveLength(0);
541
+ expect(thread2Messages).toHaveLength(1);
542
+ expect(thread2Messages[0].id).toBe(message.id);
543
+ });
544
+
545
+ it('should not fail when trying to update a non-existent message', async () => {
546
+ const originalMessage = createSampleMessageV2({ threadId: thread.id });
547
+ await store.saveMessages({ messages: [originalMessage], format: 'v2' });
548
+
549
+ await expect(
550
+ store.updateMessages({
551
+ messages: [{ id: randomUUID(), role: 'assistant' }],
552
+ }),
553
+ ).resolves.not.toThrow();
554
+
555
+ const fromDb = await store.getMessages({ threadId: thread.id, format: 'v2' });
556
+ expect(fromDb[0].role).toBe(originalMessage.role);
557
+ });
558
+ });
@@ -1,7 +1,7 @@
1
1
  import { createClient } from '@libsql/client';
2
2
  import type { Client, InValue } from '@libsql/client';
3
3
  import { MessageList } from '@mastra/core/agent';
4
- import type { MastraMessageV2 } from '@mastra/core/agent';
4
+ import type { MastraMessageContentV2, MastraMessageV2 } from '@mastra/core/agent';
5
5
  import type { MetricResult, TestInfo } from '@mastra/core/eval';
6
6
  import type { MastraMessageV1, StorageThreadType } from '@mastra/core/memory';
7
7
  import {
@@ -755,6 +755,112 @@ export class LibSQLStore extends MastraStorage {
755
755
  }
756
756
  }
757
757
 
758
+ async updateMessages({
759
+ messages,
760
+ }: {
761
+ messages: (Partial<Omit<MastraMessageV2, 'createdAt'>> & {
762
+ id: string;
763
+ content?: { metadata?: MastraMessageContentV2['metadata']; content?: MastraMessageContentV2['content'] };
764
+ })[];
765
+ }): Promise<MastraMessageV2[]> {
766
+ if (messages.length === 0) {
767
+ return [];
768
+ }
769
+
770
+ const messageIds = messages.map(m => m.id);
771
+ const placeholders = messageIds.map(() => '?').join(',');
772
+
773
+ const selectSql = `SELECT * FROM ${TABLE_MESSAGES} WHERE id IN (${placeholders})`;
774
+ const existingResult = await this.client.execute({ sql: selectSql, args: messageIds });
775
+ const existingMessages: MastraMessageV2[] = existingResult.rows.map(row => this.parseRow(row));
776
+
777
+ if (existingMessages.length === 0) {
778
+ return [];
779
+ }
780
+
781
+ const batchStatements = [];
782
+ const threadIdsToUpdate = new Set<string>();
783
+ const columnMapping: Record<string, string> = {
784
+ threadId: 'thread_id',
785
+ };
786
+
787
+ for (const existingMessage of existingMessages) {
788
+ const updatePayload = messages.find(m => m.id === existingMessage.id);
789
+ if (!updatePayload) continue;
790
+
791
+ const { id, ...fieldsToUpdate } = updatePayload;
792
+ if (Object.keys(fieldsToUpdate).length === 0) continue;
793
+
794
+ threadIdsToUpdate.add(existingMessage.threadId!);
795
+ if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) {
796
+ threadIdsToUpdate.add(updatePayload.threadId);
797
+ }
798
+
799
+ const setClauses = [];
800
+ const args: InValue[] = [];
801
+ const updatableFields = { ...fieldsToUpdate };
802
+
803
+ // Special handling for the 'content' field to merge instead of overwrite
804
+ if (updatableFields.content) {
805
+ const newContent = {
806
+ ...existingMessage.content,
807
+ ...updatableFields.content,
808
+ // Deep merge metadata if it exists on both
809
+ ...(existingMessage.content?.metadata && updatableFields.content.metadata
810
+ ? {
811
+ metadata: {
812
+ ...existingMessage.content.metadata,
813
+ ...updatableFields.content.metadata,
814
+ },
815
+ }
816
+ : {}),
817
+ };
818
+ setClauses.push(`${parseSqlIdentifier('content', 'column name')} = ?`);
819
+ args.push(JSON.stringify(newContent));
820
+ delete updatableFields.content;
821
+ }
822
+
823
+ for (const key in updatableFields) {
824
+ if (Object.prototype.hasOwnProperty.call(updatableFields, key)) {
825
+ const dbKey = columnMapping[key] || key;
826
+ setClauses.push(`${parseSqlIdentifier(dbKey, 'column name')} = ?`);
827
+ let value = updatableFields[key as keyof typeof updatableFields];
828
+
829
+ if (typeof value === 'object' && value !== null) {
830
+ value = JSON.stringify(value);
831
+ }
832
+ args.push(value as InValue);
833
+ }
834
+ }
835
+
836
+ if (setClauses.length === 0) continue;
837
+
838
+ args.push(id);
839
+
840
+ const sql = `UPDATE ${TABLE_MESSAGES} SET ${setClauses.join(', ')} WHERE id = ?`;
841
+ batchStatements.push({ sql, args });
842
+ }
843
+
844
+ if (batchStatements.length === 0) {
845
+ return existingMessages;
846
+ }
847
+
848
+ const now = new Date().toISOString();
849
+ for (const threadId of threadIdsToUpdate) {
850
+ if (threadId) {
851
+ batchStatements.push({
852
+ sql: `UPDATE ${TABLE_THREADS} SET updatedAt = ? WHERE id = ?`,
853
+ args: [now, threadId],
854
+ });
855
+ }
856
+ }
857
+
858
+ await this.client.batch(batchStatements, 'write');
859
+
860
+ const updatedResult = await this.client.execute({ sql: selectSql, args: messageIds });
861
+ return updatedResult.rows.map(row => this.parseRow(row));
862
+ }
863
+
758
864
  private transformEvalRow(row: Record<string, any>): EvalRow {
759
865
  const resultValue = JSON.parse(row.result as string);
760
866
  const testInfoValue = row.test_info ? JSON.parse(row.test_info as string) : undefined;