@lobehub/lobehub 2.0.0-next.40 → 2.0.0-next.42

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 (45) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/chat.json +1 -0
  4. package/locales/bg-BG/chat.json +1 -0
  5. package/locales/de-DE/chat.json +1 -0
  6. package/locales/en-US/chat.json +1 -0
  7. package/locales/es-ES/chat.json +1 -0
  8. package/locales/fa-IR/chat.json +1 -0
  9. package/locales/fr-FR/chat.json +1 -0
  10. package/locales/it-IT/chat.json +1 -0
  11. package/locales/ja-JP/chat.json +1 -0
  12. package/locales/ko-KR/chat.json +1 -0
  13. package/locales/nl-NL/chat.json +1 -0
  14. package/locales/pl-PL/chat.json +1 -0
  15. package/locales/pt-BR/chat.json +1 -0
  16. package/locales/ru-RU/chat.json +1 -0
  17. package/locales/tr-TR/chat.json +1 -0
  18. package/locales/vi-VN/chat.json +1 -0
  19. package/locales/zh-CN/chat.json +1 -0
  20. package/locales/zh-TW/chat.json +1 -0
  21. package/package.json +1 -1
  22. package/packages/database/src/models/__tests__/messages/message.create.test.ts +549 -0
  23. package/packages/database/src/models/__tests__/messages/message.delete.test.ts +481 -0
  24. package/packages/database/src/models/__tests__/messages/message.query.test.ts +1187 -0
  25. package/packages/database/src/models/__tests__/messages/message.stats.test.ts +633 -0
  26. package/packages/database/src/models/__tests__/messages/message.update.test.ts +757 -0
  27. package/packages/database/src/models/message.ts +5 -55
  28. package/packages/utils/src/clientIP.ts +6 -6
  29. package/packages/utils/src/compressImage.ts +3 -3
  30. package/packages/utils/src/fetch/fetchSSE.ts +15 -15
  31. package/packages/utils/src/format.ts +2 -2
  32. package/packages/utils/src/merge.ts +3 -3
  33. package/packages/utils/src/parseModels.ts +3 -3
  34. package/packages/utils/src/sanitizeUTF8.ts +4 -4
  35. package/packages/utils/src/toolManifest.ts +4 -4
  36. package/packages/utils/src/trace.test.ts +359 -0
  37. package/packages/utils/src/uriParser.ts +4 -4
  38. package/src/features/ChatItem/components/Title.tsx +20 -16
  39. package/src/features/Conversation/Messages/Assistant/index.tsx +3 -2
  40. package/src/features/Conversation/Messages/Group/index.tsx +10 -3
  41. package/src/server/services/message/index.ts +14 -4
  42. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +8 -2
  43. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +1 -4
  44. package/src/store/chat/slices/message/actions/optimisticUpdate.ts +1 -1
  45. package/packages/database/src/models/__tests__/message.test.ts +0 -2632
@@ -0,0 +1,359 @@
1
+ import { LOBE_CHAT_TRACE_HEADER, LOBE_CHAT_TRACE_ID } from '@lobechat/const';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { createTraceHeader, getTraceId, getTracePayload } from './trace';
5
+
6
+ describe('trace utilities', () => {
7
+ describe('getTracePayload', () => {
8
+ it('should extract and decode trace payload from request headers', () => {
9
+ const payload = {
10
+ traceId: '123-456-789',
11
+ sessionId: 'session-abc',
12
+ timestamp: 1234567890,
13
+ };
14
+
15
+ const encoded = Buffer.from(JSON.stringify(payload)).toString('base64');
16
+ const mockRequest = new Request('http://localhost', {
17
+ headers: {
18
+ [LOBE_CHAT_TRACE_HEADER]: encoded,
19
+ },
20
+ });
21
+
22
+ const result = getTracePayload(mockRequest);
23
+
24
+ expect(result).toEqual(payload);
25
+ });
26
+
27
+ it('should return undefined when trace header is not present', () => {
28
+ const mockRequest = new Request('http://localhost');
29
+
30
+ const result = getTracePayload(mockRequest);
31
+
32
+ expect(result).toBeUndefined();
33
+ });
34
+
35
+ it('should handle empty trace header', () => {
36
+ const mockRequest = new Request('http://localhost', {
37
+ headers: {
38
+ [LOBE_CHAT_TRACE_HEADER]: '',
39
+ },
40
+ });
41
+
42
+ const result = getTracePayload(mockRequest);
43
+
44
+ expect(result).toBeUndefined();
45
+ });
46
+
47
+ it('should decode complex payload with nested objects', () => {
48
+ const payload = {
49
+ traceId: 'trace-123',
50
+ metadata: {
51
+ user: { id: 'user-1', role: 'admin' },
52
+ context: { env: 'production', region: 'us-west-2' },
53
+ },
54
+ tags: ['important', 'production'],
55
+ };
56
+
57
+ const encoded = Buffer.from(JSON.stringify(payload)).toString('base64');
58
+ const mockRequest = new Request('http://localhost', {
59
+ headers: {
60
+ [LOBE_CHAT_TRACE_HEADER]: encoded,
61
+ },
62
+ });
63
+
64
+ const result = getTracePayload(mockRequest);
65
+
66
+ expect(result).toEqual(payload);
67
+ });
68
+
69
+ it('should handle payload with special characters', () => {
70
+ const payload = {
71
+ traceId: 'trace-with-特殊字符-🔥',
72
+ message: 'Test with émojis 😀',
73
+ };
74
+
75
+ const encoded = Buffer.from(JSON.stringify(payload)).toString('base64');
76
+ const mockRequest = new Request('http://localhost', {
77
+ headers: {
78
+ [LOBE_CHAT_TRACE_HEADER]: encoded,
79
+ },
80
+ });
81
+
82
+ const result = getTracePayload(mockRequest);
83
+
84
+ expect(result).toEqual(payload);
85
+ });
86
+
87
+ it('should handle payload with null values', () => {
88
+ const payload = {
89
+ traceId: 'trace-123',
90
+ optionalField: null,
91
+ anotherField: undefined,
92
+ };
93
+
94
+ const encoded = Buffer.from(JSON.stringify(payload)).toString('base64');
95
+ const mockRequest = new Request('http://localhost', {
96
+ headers: {
97
+ [LOBE_CHAT_TRACE_HEADER]: encoded,
98
+ },
99
+ });
100
+
101
+ const result = getTracePayload(mockRequest);
102
+
103
+ // Note: undefined values are removed during JSON.stringify
104
+ expect(result).toEqual({
105
+ traceId: 'trace-123',
106
+ optionalField: null,
107
+ });
108
+ });
109
+
110
+ it('should handle numeric and boolean values in payload', () => {
111
+ const payload = {
112
+ traceId: 'trace-123',
113
+ count: 42,
114
+ isActive: true,
115
+ ratio: 3.14159,
116
+ isDisabled: false,
117
+ };
118
+
119
+ const encoded = Buffer.from(JSON.stringify(payload)).toString('base64');
120
+ const mockRequest = new Request('http://localhost', {
121
+ headers: {
122
+ [LOBE_CHAT_TRACE_HEADER]: encoded,
123
+ },
124
+ });
125
+
126
+ const result = getTracePayload(mockRequest);
127
+
128
+ expect(result).toEqual(payload);
129
+ });
130
+ });
131
+
132
+ describe('getTraceId', () => {
133
+ it('should extract trace ID from response headers', () => {
134
+ const traceId = 'trace-xyz-789';
135
+ const mockResponse = new Response(null, {
136
+ headers: {
137
+ [LOBE_CHAT_TRACE_ID]: traceId,
138
+ },
139
+ });
140
+
141
+ const result = getTraceId(mockResponse);
142
+
143
+ expect(result).toBe(traceId);
144
+ });
145
+
146
+ it('should return null when trace ID header is not present', () => {
147
+ const mockResponse = new Response(null);
148
+
149
+ const result = getTraceId(mockResponse);
150
+
151
+ expect(result).toBeNull();
152
+ });
153
+
154
+ it('should handle empty trace ID', () => {
155
+ const mockResponse = new Response(null, {
156
+ headers: {
157
+ [LOBE_CHAT_TRACE_ID]: '',
158
+ },
159
+ });
160
+
161
+ const result = getTraceId(mockResponse);
162
+
163
+ expect(result).toBe('');
164
+ });
165
+
166
+ it('should handle trace ID with special characters', () => {
167
+ const traceId = 'trace-123-abc-特殊-🔥';
168
+ const mockResponse = new Response(null, {
169
+ headers: {
170
+ [LOBE_CHAT_TRACE_ID]: traceId,
171
+ },
172
+ });
173
+
174
+ const result = getTraceId(mockResponse);
175
+
176
+ expect(result).toBe(traceId);
177
+ });
178
+
179
+ it('should handle UUID-formatted trace IDs', () => {
180
+ const traceId = '550e8400-e29b-41d4-a716-446655440000';
181
+ const mockResponse = new Response(null, {
182
+ headers: {
183
+ [LOBE_CHAT_TRACE_ID]: traceId,
184
+ },
185
+ });
186
+
187
+ const result = getTraceId(mockResponse);
188
+
189
+ expect(result).toBe(traceId);
190
+ });
191
+ });
192
+
193
+ describe('createTraceHeader', () => {
194
+ it('should create a base64-encoded trace header from payload', () => {
195
+ const payload = {
196
+ traceId: 'trace-123',
197
+ sessionId: 'session-456',
198
+ timestamp: 1234567890,
199
+ };
200
+
201
+ const result = createTraceHeader(payload);
202
+
203
+ expect(result).toHaveProperty(LOBE_CHAT_TRACE_HEADER);
204
+ expect(typeof result[LOBE_CHAT_TRACE_HEADER]).toBe('string');
205
+
206
+ // Verify it's valid base64
207
+ const decoded = Buffer.from(result[LOBE_CHAT_TRACE_HEADER], 'base64').toString('utf8');
208
+ expect(JSON.parse(decoded)).toEqual(payload);
209
+ });
210
+
211
+ it('should create header for empty payload object', () => {
212
+ const payload = {};
213
+
214
+ const result = createTraceHeader(payload);
215
+
216
+ expect(result).toHaveProperty(LOBE_CHAT_TRACE_HEADER);
217
+
218
+ const decoded = Buffer.from(result[LOBE_CHAT_TRACE_HEADER], 'base64').toString('utf8');
219
+ expect(JSON.parse(decoded)).toEqual({});
220
+ });
221
+
222
+ it('should handle payload with nested objects', () => {
223
+ const payload = {
224
+ traceId: 'trace-123',
225
+ metadata: {
226
+ user: { id: 'user-1', name: 'John' },
227
+ context: { env: 'prod' },
228
+ },
229
+ };
230
+
231
+ const result = createTraceHeader(payload);
232
+
233
+ const decoded = Buffer.from(result[LOBE_CHAT_TRACE_HEADER], 'base64').toString('utf8');
234
+ expect(JSON.parse(decoded)).toEqual(payload);
235
+ });
236
+
237
+ it('should handle payload with arrays', () => {
238
+ const payload = {
239
+ traceId: 'trace-123',
240
+ tags: ['tag1', 'tag2', 'tag3'],
241
+ values: [1, 2, 3, 4, 5],
242
+ };
243
+
244
+ const result = createTraceHeader(payload);
245
+
246
+ const decoded = Buffer.from(result[LOBE_CHAT_TRACE_HEADER], 'base64').toString('utf8');
247
+ expect(JSON.parse(decoded)).toEqual(payload);
248
+ });
249
+
250
+ it('should handle payload with Unicode characters', () => {
251
+ const payload = {
252
+ traceId: 'trace-特殊-🔥',
253
+ message: 'Hello 世界 😀',
254
+ description: 'Тест тест',
255
+ };
256
+
257
+ const result = createTraceHeader(payload);
258
+
259
+ const decoded = Buffer.from(result[LOBE_CHAT_TRACE_HEADER], 'base64').toString('utf8');
260
+ expect(JSON.parse(decoded)).toEqual(payload);
261
+ });
262
+
263
+ it('should handle payload with null values', () => {
264
+ const payload = {
265
+ traceId: 'trace-123',
266
+ optionalField: null,
267
+ };
268
+
269
+ const result = createTraceHeader(payload);
270
+
271
+ const decoded = Buffer.from(result[LOBE_CHAT_TRACE_HEADER], 'base64').toString('utf8');
272
+ expect(JSON.parse(decoded)).toEqual(payload);
273
+ });
274
+
275
+ it('should handle payload with boolean and numeric values', () => {
276
+ const payload = {
277
+ traceId: 'trace-123',
278
+ count: 42,
279
+ isActive: true,
280
+ ratio: 3.14,
281
+ isDisabled: false,
282
+ };
283
+
284
+ const result = createTraceHeader(payload);
285
+
286
+ const decoded = Buffer.from(result[LOBE_CHAT_TRACE_HEADER], 'base64').toString('utf8');
287
+ expect(JSON.parse(decoded)).toEqual(payload);
288
+ });
289
+
290
+ it('should create header that works with getTracePayload', () => {
291
+ const payload = {
292
+ traceId: 'trace-round-trip',
293
+ sessionId: 'session-test',
294
+ timestamp: Date.now(),
295
+ };
296
+
297
+ const header = createTraceHeader(payload);
298
+ const mockRequest = new Request('http://localhost', {
299
+ headers: header,
300
+ });
301
+
302
+ const extractedPayload = getTracePayload(mockRequest);
303
+
304
+ expect(extractedPayload).toEqual(payload);
305
+ });
306
+ });
307
+
308
+ describe('round-trip encoding and decoding', () => {
309
+ it('should correctly encode and decode simple payload', () => {
310
+ const originalPayload = {
311
+ traceId: 'test-123',
312
+ data: 'test-data',
313
+ };
314
+
315
+ const header = createTraceHeader(originalPayload);
316
+ const mockRequest = new Request('http://localhost', { headers: header });
317
+ const decodedPayload = getTracePayload(mockRequest);
318
+
319
+ expect(decodedPayload).toEqual(originalPayload);
320
+ });
321
+
322
+ it('should correctly encode and decode complex payload', () => {
323
+ const originalPayload = {
324
+ traceId: 'complex-trace',
325
+ metadata: {
326
+ user: { id: 'usr-123', roles: ['admin', 'user'] },
327
+ timestamps: { created: 1234567890, updated: 1234567900 },
328
+ },
329
+ tags: ['production', 'critical'],
330
+ metrics: { duration: 123.45, count: 10 },
331
+ flags: { enabled: true, debug: false },
332
+ };
333
+
334
+ const header = createTraceHeader(originalPayload);
335
+ const mockRequest = new Request('http://localhost', { headers: header });
336
+ const decodedPayload = getTracePayload(mockRequest);
337
+
338
+ expect(decodedPayload).toEqual(originalPayload);
339
+ });
340
+
341
+ it('should preserve data types through encoding and decoding', () => {
342
+ const originalPayload = {
343
+ traceId: 'trace-types-test',
344
+ sessionId: 'session-123',
345
+ userId: 'user-456',
346
+ topicId: 'topic-789',
347
+ observationId: 'obs-abc',
348
+ enabled: true,
349
+ tags: ['tag1', 'tag2', 'tag3'],
350
+ };
351
+
352
+ const header = createTraceHeader(originalPayload);
353
+ const mockRequest = new Request('http://localhost', { headers: header });
354
+ const decodedPayload = getTracePayload(mockRequest);
355
+
356
+ expect(decodedPayload).toEqual(originalPayload);
357
+ });
358
+ });
359
+ });
@@ -5,20 +5,20 @@ interface UriParserResult {
5
5
  }
6
6
 
7
7
  export const parseDataUri = (dataUri: string): UriParserResult => {
8
- // 正则表达式匹配整个 Data URI 结构
8
+ // Regular expression to match the entire Data URI structure
9
9
  const dataUriMatch = dataUri.match(/^data:([^;]+);base64,(.+)$/);
10
10
 
11
11
  if (dataUriMatch) {
12
- // 如果是合法的 Data URI
12
+ // If it's a valid Data URI
13
13
  return { base64: dataUriMatch[2], mimeType: dataUriMatch[1], type: 'base64' };
14
14
  }
15
15
 
16
16
  try {
17
17
  new URL(dataUri);
18
- // 如果是合法的 URL
18
+ // If it's a valid URL
19
19
  return { base64: null, mimeType: null, type: 'url' };
20
20
  } catch {
21
- // 既不是 Data URI 也不是合法 URL
21
+ // Neither a Data URI nor a valid URL
22
22
  return { base64: null, mimeType: null, type: null };
23
23
  }
24
24
  };
@@ -1,5 +1,5 @@
1
1
  import dayjs from 'dayjs';
2
- import { memo } from 'react';
2
+ import { CSSProperties, memo } from 'react';
3
3
  import { Flexbox } from 'react-layout-kit';
4
4
 
5
5
  import { useStyles } from '../style';
@@ -10,6 +10,7 @@ export interface TitleProps {
10
10
  className?: string;
11
11
  placement?: ChatItemProps['placement'];
12
12
  showTitle?: ChatItemProps['showTitle'];
13
+ style?: CSSProperties;
13
14
  time?: ChatItemProps['time'];
14
15
  titleAddon?: ChatItemProps['titleAddon'];
15
16
  }
@@ -27,21 +28,24 @@ const formatTime = (time: number): string => {
27
28
  }
28
29
  };
29
30
 
30
- const Title = memo<TitleProps>(({ showTitle, placement, time, avatar, titleAddon, className }) => {
31
- const { styles, cx } = useStyles({ placement, showTitle, time });
31
+ const Title = memo<TitleProps>(
32
+ ({ showTitle, placement, time, avatar, titleAddon, className, style }) => {
33
+ const { styles, cx } = useStyles({ placement, showTitle, time });
32
34
 
33
- return (
34
- <Flexbox
35
- align={'center'}
36
- className={cx(styles.name, className)}
37
- direction={placement === 'left' ? 'horizontal' : 'horizontal-reverse'}
38
- gap={4}
39
- >
40
- {showTitle ? avatar.title || 'untitled' : undefined}
41
- {showTitle ? titleAddon : undefined}
42
- {time && <time>{formatTime(time)}</time>}
43
- </Flexbox>
44
- );
45
- });
35
+ return (
36
+ <Flexbox
37
+ align={'center'}
38
+ className={cx(styles.name, className)}
39
+ direction={placement === 'left' ? 'horizontal' : 'horizontal-reverse'}
40
+ gap={4}
41
+ style={style}
42
+ >
43
+ {showTitle ? avatar.title || 'untitled' : undefined}
44
+ {showTitle ? titleAddon : undefined}
45
+ {time && <time>{formatTime(time)}</time>}
46
+ </Flexbox>
47
+ );
48
+ },
49
+ );
46
50
 
47
51
  export default Title;
@@ -4,6 +4,7 @@ import { LOADING_FLAT } from '@lobechat/const';
4
4
  import { UIChatMessage } from '@lobechat/types';
5
5
  import { Tag } from '@lobehub/ui';
6
6
  import { useResponsive } from 'antd-style';
7
+ import isEqual from 'fast-deep-equal';
7
8
  import { ReactNode, memo, useCallback, useMemo } from 'react';
8
9
  import { useTranslation } from 'react-i18next';
9
10
  import { Flexbox } from 'react-layout-kit';
@@ -213,12 +214,12 @@ const AssistantMessage = memo<AssistantMessageProps>((props) => {
213
214
  onClick={onAvatarClick}
214
215
  placement={placement}
215
216
  size={MOBILE_AVATAR_SIZE}
216
- style={{ marginTop: 6 }}
217
217
  />
218
218
  <Title
219
219
  avatar={avatar}
220
220
  placement={placement}
221
221
  showTitle
222
+ style={{ marginBlockEnd: 0 }}
222
223
  time={createdAt}
223
224
  titleAddon={dmIndicator}
224
225
  />
@@ -274,6 +275,6 @@ const AssistantMessage = memo<AssistantMessageProps>((props) => {
274
275
  {mobile && <BorderSpacing borderSpacing={MOBILE_AVATAR_SIZE} />}
275
276
  </Flexbox>
276
277
  );
277
- });
278
+ }, isEqual);
278
279
 
279
280
  export default AssistantMessage;
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { UIChatMessage } from '@lobechat/types';
4
4
  import { useResponsive } from 'antd-style';
5
+ import isEqual from 'fast-deep-equal';
5
6
  import { memo, useCallback } from 'react';
6
7
  import { Flexbox } from 'react-layout-kit';
7
8
 
@@ -49,6 +50,7 @@ const GroupMessage = memo<GroupMessageProps>((props) => {
49
50
  provider,
50
51
  } = props;
51
52
  const avatar = meta;
53
+ console.log('render');
52
54
  const { mobile } = useResponsive();
53
55
  const placement = 'left';
54
56
  const type = useAgentStore(agentChatConfigSelectors.displayMode);
@@ -96,9 +98,14 @@ const GroupMessage = memo<GroupMessageProps>((props) => {
96
98
  onClick={onAvatarClick}
97
99
  placement={placement}
98
100
  size={MOBILE_AVATAR_SIZE}
99
- style={{ marginTop: 6 }}
100
101
  />
101
- <Title avatar={avatar} placement={placement} showTitle time={createdAt} />
102
+ <Title
103
+ avatar={avatar}
104
+ placement={placement}
105
+ showTitle
106
+ style={{ marginBlockEnd: 0 }}
107
+ time={createdAt}
108
+ />
102
109
  </Flexbox>
103
110
  {isEditing && contentId ? (
104
111
  <EditState content={lastAssistantMsg?.content} id={contentId} />
@@ -141,6 +148,6 @@ const GroupMessage = memo<GroupMessageProps>((props) => {
141
148
  {mobile && <BorderSpacing borderSpacing={MOBILE_AVATAR_SIZE} />}
142
149
  </Flexbox>
143
150
  );
144
- });
151
+ }, isEqual);
145
152
 
146
153
  export default GroupMessage;
@@ -1,5 +1,5 @@
1
1
  import { LobeChatDatabase } from '@lobechat/database';
2
- import { CreateMessageParams, UpdateMessageParams } from '@lobechat/types';
2
+ import { CreateMessageParams, UIChatMessage, UpdateMessageParams } from '@lobechat/types';
3
3
 
4
4
  import { MessageModel } from '@/database/models/message';
5
5
 
@@ -51,7 +51,9 @@ export class MessageService {
51
51
  /**
52
52
  * Query messages and return response with success status (used after mutations)
53
53
  */
54
- private async queryWithSuccess(options?: QueryOptions) {
54
+ private async queryWithSuccess(
55
+ options?: QueryOptions,
56
+ ): Promise<{ messages?: UIChatMessage[], success: boolean; }> {
55
57
  if (!options || (options.sessionId === undefined && options.topicId === undefined)) {
56
58
  return { success: true };
57
59
  }
@@ -139,7 +141,11 @@ export class MessageService {
139
141
  * Update plugin state and return message list
140
142
  * Pattern: update + conditional query
141
143
  */
142
- async updatePluginState(id: string, value: any, options: QueryOptions): Promise<any> {
144
+ async updatePluginState(
145
+ id: string,
146
+ value: any,
147
+ options: QueryOptions,
148
+ ): Promise<{ messages?: UIChatMessage[], success: boolean; }> {
143
149
  await this.messageModel.updatePluginState(id, value);
144
150
  return this.queryWithSuccess(options);
145
151
  }
@@ -148,7 +154,11 @@ export class MessageService {
148
154
  * Update message and return message list
149
155
  * Pattern: update + conditional query
150
156
  */
151
- async updateMessage(id: string, value: UpdateMessageParams, options: QueryOptions): Promise<any> {
157
+ async updateMessage(
158
+ id: string,
159
+ value: UpdateMessageParams,
160
+ options: QueryOptions,
161
+ ): Promise<{ messages?: UIChatMessage[], success: boolean; }> {
152
162
  await this.messageModel.update(id, value as any);
153
163
  return this.queryWithSuccess(options);
154
164
  }
@@ -166,7 +166,11 @@ export const conversationLifecycle: StateCreator<
166
166
  topicId = data.topicId;
167
167
  }
168
168
 
169
- get().replaceMessages(data.messages, { sessionId: activeId, topicId: topicId });
169
+ get().replaceMessages(data.messages, {
170
+ sessionId: activeId,
171
+ topicId: topicId,
172
+ action: 'sendMessage/serverResponse',
173
+ });
170
174
 
171
175
  if (data.isCreateNewTopic && data.topicId) {
172
176
  await get().switchTopic(data.topicId, true);
@@ -250,7 +254,9 @@ export const conversationLifecycle: StateCreator<
250
254
  .map((f) => f?.id)
251
255
  .filter(Boolean) as string[];
252
256
 
253
- await getAgentStoreState().addFilesToAgent(userFiles, false);
257
+ if (userFiles.length > 0) {
258
+ await getAgentStoreState().addFilesToAgent(userFiles, false);
259
+ }
254
260
  } catch (e) {
255
261
  console.error(e);
256
262
  } finally {
@@ -521,10 +521,7 @@ export const streamingExecutor: StateCreator<
521
521
  // Handle completion and error events
522
522
  for (const event of result.events) {
523
523
  if (event.type === 'done') {
524
- log('[internal_execAgentRuntime] Received done event, syncing to database');
525
- // Sync final state to database
526
- const finalMessages = get().messagesMap[messageKey] || [];
527
- get().replaceMessages(finalMessages);
524
+ log('[internal_execAgentRuntime] Received done event');
528
525
  }
529
526
 
530
527
  if (event.type === 'error') {
@@ -195,7 +195,7 @@ export const messageOptimisticUpdate: StateCreator<
195
195
  );
196
196
 
197
197
  if (result && result.success && result.messages) {
198
- replaceMessages(result.messages);
198
+ replaceMessages(result.messages, { action: 'optimisticUpdateMessageContent' });
199
199
  } else {
200
200
  await refreshMessages();
201
201
  }