@lobehub/lobehub 2.0.0-next.357 → 2.0.0-next.358

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/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.358](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.357...v2.0.0-next.358)
6
+
7
+ <sup>Released on **2026-01-23**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **store**: Delete message before regeneration.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **store**: Delete message before regeneration, closes [#11760](https://github.com/lobehub/lobe-chat/issues/11760) ([a8a6300](https://github.com/lobehub/lobe-chat/commit/a8a6300))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ## [Version 2.0.0-next.357](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.356...v2.0.0-next.357)
6
31
 
7
32
  <sup>Released on **2026-01-23**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,9 @@
1
1
  [
2
+ {
3
+ "children": {},
4
+ "date": "2026-01-23",
5
+ "version": "2.0.0-next.358"
6
+ },
2
7
  {
3
8
  "children": {
4
9
  "fixes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.357",
3
+ "version": "2.0.0-next.358",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -462,6 +462,122 @@ describe('Generation Actions', () => {
462
462
  // Should complete operation
463
463
  expect(mockCompleteOperation).toHaveBeenCalledWith('test-op-id');
464
464
  });
465
+
466
+ it('should delete message BEFORE regeneration to prevent message not found issue (LOBE-2533)', async () => {
467
+ // This test verifies the fix for LOBE-2533:
468
+ // When "delete and regenerate" is called, if regeneration happens first,
469
+ // it switches to a new branch, causing the original message to no longer
470
+ // appear in displayMessages. Then deleteMessage cannot find the message
471
+ // and fails silently.
472
+ //
473
+ // The fix: delete first, then regenerate.
474
+
475
+ const callOrder: string[] = [];
476
+
477
+ // Re-setup mock to track call order
478
+ const { useChatStore } = await import('@/store/chat');
479
+ vi.mocked(useChatStore.getState).mockReturnValue({
480
+ messagesMap: {},
481
+ operations: {},
482
+ messageLoadingIds: [],
483
+ cancelOperations: mockCancelOperations,
484
+ cancelOperation: mockCancelOperation,
485
+ deleteMessage: vi.fn().mockImplementation(() => {
486
+ callOrder.push('deleteMessage');
487
+ return Promise.resolve();
488
+ }),
489
+ switchMessageBranch: vi.fn().mockImplementation(() => {
490
+ callOrder.push('switchMessageBranch');
491
+ return Promise.resolve();
492
+ }),
493
+ startOperation: mockStartOperation,
494
+ completeOperation: mockCompleteOperation,
495
+ failOperation: mockFailOperation,
496
+ internal_execAgentRuntime: vi.fn().mockImplementation(() => {
497
+ callOrder.push('internal_execAgentRuntime');
498
+ return Promise.resolve();
499
+ }),
500
+ } as any);
501
+
502
+ const context: ConversationContext = {
503
+ agentId: 'session-1',
504
+ topicId: 'topic-1',
505
+ threadId: null,
506
+ groupId: 'group-1',
507
+ };
508
+
509
+ const store = createStore({ context });
510
+
511
+ // Set displayMessages and dbMessages
512
+ act(() => {
513
+ store.setState({
514
+ displayMessages: [
515
+ { id: 'msg-1', role: 'user', content: 'Hello' },
516
+ { id: 'msg-2', role: 'assistant', content: 'Hi there', parentId: 'msg-1' },
517
+ ],
518
+ dbMessages: [
519
+ { id: 'msg-1', role: 'user', content: 'Hello' },
520
+ { id: 'msg-2', role: 'assistant', content: 'Hi there', parentId: 'msg-1' },
521
+ ],
522
+ } as any);
523
+ });
524
+
525
+ await act(async () => {
526
+ await store.getState().delAndRegenerateMessage('msg-2');
527
+ });
528
+
529
+ // CRITICAL: deleteMessage must be called BEFORE switchMessageBranch and internal_execAgentRuntime
530
+ // If regeneration (which calls switchMessageBranch) happens first, the message
531
+ // won't be found in displayMessages and deletion will fail silently.
532
+ expect(callOrder[0]).toBe('deleteMessage');
533
+ expect(callOrder).toContain('switchMessageBranch');
534
+ expect(callOrder).toContain('internal_execAgentRuntime');
535
+
536
+ // Verify deleteMessage is called before any regeneration-related calls
537
+ const deleteIndex = callOrder.indexOf('deleteMessage');
538
+ const switchIndex = callOrder.indexOf('switchMessageBranch');
539
+ const execIndex = callOrder.indexOf('internal_execAgentRuntime');
540
+
541
+ expect(deleteIndex).toBeLessThan(switchIndex);
542
+ expect(deleteIndex).toBeLessThan(execIndex);
543
+ });
544
+
545
+ it('should not proceed if assistant message has no parentId', async () => {
546
+ const { useChatStore } = await import('@/store/chat');
547
+ vi.mocked(useChatStore.getState).mockReturnValue({
548
+ messagesMap: {},
549
+ operations: {},
550
+ messageLoadingIds: [],
551
+ startOperation: mockStartOperation,
552
+ completeOperation: mockCompleteOperation,
553
+ deleteMessage: mockDeleteMessage,
554
+ } as any);
555
+
556
+ const context: ConversationContext = {
557
+ agentId: 'session-1',
558
+ topicId: null,
559
+ threadId: null,
560
+ };
561
+
562
+ const store = createStore({ context });
563
+
564
+ // Set displayMessages with assistant message that has no parentId
565
+ act(() => {
566
+ store.setState({
567
+ displayMessages: [
568
+ { id: 'msg-1', role: 'assistant', content: 'Hi there' }, // no parentId
569
+ ],
570
+ } as any);
571
+ });
572
+
573
+ await act(async () => {
574
+ await store.getState().delAndRegenerateMessage('msg-1');
575
+ });
576
+
577
+ // Should not proceed - no operation created, no delete called
578
+ expect(mockStartOperation).not.toHaveBeenCalled();
579
+ expect(mockDeleteMessage).not.toHaveBeenCalled();
580
+ });
465
581
  });
466
582
 
467
583
  describe('delAndResendThreadMessage', () => {
@@ -206,18 +206,29 @@ export const generationSlice: StateCreator<
206
206
  },
207
207
 
208
208
  delAndRegenerateMessage: async (messageId: string) => {
209
- const { context } = get();
209
+ const { context, displayMessages } = get();
210
210
  const chatStore = useChatStore.getState();
211
211
 
212
+ // Find the assistant message and get parent user message ID before deletion
213
+ // This is needed because after deletion, we can't find the parent anymore
214
+ const currentMessage = displayMessages.find((c) => c.id === messageId);
215
+ if (!currentMessage) return;
216
+
217
+ const userId = currentMessage.parentId;
218
+ if (!userId) return;
219
+
212
220
  // Create operation to track context (use 'regenerate' type since this is a regenerate action)
213
221
  const { operationId } = chatStore.startOperation({
214
222
  context: { ...context, messageId },
215
223
  type: 'regenerate',
216
224
  });
217
225
 
218
- // Regenerate first, then delete
219
- await get().regenerateAssistantMessage(messageId);
226
+ // IMPORTANT: Delete first, then regenerate (LOBE-2533)
227
+ // If we regenerate first, it switches to a new branch, causing the original
228
+ // message to no longer appear in displayMessages. Then deleteMessage cannot
229
+ // find the message and fails silently.
220
230
  await chatStore.deleteMessage(messageId, { operationId });
231
+ await get().regenerateUserMessage(userId);
221
232
  chatStore.completeOperation(operationId);
222
233
  },
223
234