@lobehub/lobehub 2.0.0-next.137 → 2.0.0-next.139

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 (34) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/CLAUDE.md +38 -0
  3. package/changelog/v1.json +14 -0
  4. package/locales/ar/modelProvider.json +2 -0
  5. package/locales/bg-BG/modelProvider.json +2 -0
  6. package/locales/de-DE/modelProvider.json +2 -0
  7. package/locales/en-US/modelProvider.json +2 -0
  8. package/locales/es-ES/modelProvider.json +2 -0
  9. package/locales/fa-IR/modelProvider.json +2 -0
  10. package/locales/fr-FR/modelProvider.json +2 -0
  11. package/locales/it-IT/modelProvider.json +2 -0
  12. package/locales/ja-JP/modelProvider.json +2 -0
  13. package/locales/ko-KR/modelProvider.json +2 -0
  14. package/locales/nl-NL/modelProvider.json +2 -0
  15. package/locales/pl-PL/modelProvider.json +2 -0
  16. package/locales/pt-BR/modelProvider.json +2 -0
  17. package/locales/ru-RU/modelProvider.json +2 -0
  18. package/locales/tr-TR/modelProvider.json +2 -0
  19. package/locales/vi-VN/modelProvider.json +2 -0
  20. package/locales/zh-CN/modelProvider.json +2 -0
  21. package/locales/zh-TW/modelProvider.json +2 -0
  22. package/package.json +1 -1
  23. package/packages/conversation-flow/src/transformation/BranchResolver.ts +24 -14
  24. package/packages/conversation-flow/src/transformation/ContextTreeBuilder.ts +6 -1
  25. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +15 -0
  26. package/packages/conversation-flow/src/transformation/__tests__/BranchResolver.test.ts +66 -3
  27. package/packages/conversation-flow/src/transformation/__tests__/ContextTreeBuilder.test.ts +64 -0
  28. package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +47 -0
  29. package/packages/database/src/models/__tests__/agent.test.ts +102 -3
  30. package/packages/database/src/models/__tests__/document.test.ts +163 -0
  31. package/packages/database/src/models/__tests__/embedding.test.ts +294 -0
  32. package/packages/database/src/models/__tests__/oauthHandoff.test.ts +261 -0
  33. package/packages/database/src/models/__tests__/thread.test.ts +327 -0
  34. package/packages/database/src/models/__tests__/user.test.ts +372 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.139](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.138...v2.0.0-next.139)
6
+
7
+ <sup>Released on **2025-12-01**</sup>
8
+
9
+ #### 💄 Styles
10
+
11
+ - **misc**: Update i18n.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Styles
19
+
20
+ - **misc**: Update i18n, closes [#10519](https://github.com/lobehub/lobe-chat/issues/10519) ([bd9a38c](https://github.com/lobehub/lobe-chat/commit/bd9a38c))
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
+
30
+ ## [Version 2.0.0-next.138](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.137...v2.0.0-next.138)
31
+
32
+ <sup>Released on **2025-11-30**</sup>
33
+
34
+ #### 🐛 Bug Fixes
35
+
36
+ - **conversation-flow**: Support optimistic update for activeBranchIndex.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's fixed
44
+
45
+ - **conversation-flow**: Support optimistic update for activeBranchIndex, closes [#10517](https://github.com/lobehub/lobe-chat/issues/10517) ([9b5b234](https://github.com/lobehub/lobe-chat/commit/9b5b234))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ## [Version 2.0.0-next.137](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.136...v2.0.0-next.137)
6
56
 
7
57
  <sup>Released on **2025-11-30**</sup>
package/CLAUDE.md CHANGED
@@ -55,6 +55,44 @@ see @.cursor/rules/typescript.mdc
55
55
  - **Dev**: Translate `locales/zh-CN/namespace.json` and `locales/en-US/namespace.json` locales file only for dev preview
56
56
  - DON'T run `pnpm i18n`, let CI auto handle it
57
57
 
58
+ ## Linear Issue Management
59
+
60
+ When working with Linear issues:
61
+
62
+ 1. **Retrieve issue details** before starting work using `mcp__linear-server__get_issue`
63
+ 2. **Check for sub-issues**: If the issue has sub-issues, retrieve and review ALL sub-issues using `mcp__linear-server__list_issues` with `parentId` filter before starting work
64
+ 3. **Update issue status** when completing tasks using `mcp__linear-server__update_issue`
65
+ 4. **MUST add completion comment** using `mcp__linear-server__create_comment`
66
+
67
+ ### Completion Comment (REQUIRED)
68
+
69
+ **Every time you complete an issue, you MUST add a comment summarizing the work done.** This is critical for:
70
+
71
+ - Team visibility and knowledge sharing
72
+ - Code review context
73
+ - Future reference and debugging
74
+
75
+ ### IMPORTANT: Per-Issue Completion Rule
76
+
77
+ **When working on multiple issues (e.g., parent issue with sub-issues), you MUST update status and add comment for EACH issue IMMEDIATELY after completing it.** Do NOT wait until all issues are done to update them in batch.
78
+
79
+ **Workflow for EACH individual issue:**
80
+
81
+ 1. Complete the implementation for this specific issue
82
+ 2. Run type check: `bun run type-check`
83
+ 3. Run related tests if applicable
84
+ 4. **IMMEDIATELY** update issue status to "Done": `mcp__linear-server__update_issue`
85
+ 5. **IMMEDIATELY** add completion comment: `mcp__linear-server__create_comment`
86
+ 6. Only then move on to the next issue
87
+
88
+ **❌ Wrong approach:**
89
+
90
+ - Complete Issue A → Complete Issue B → Complete Issue C → Update all statuses → Add all comments
91
+
92
+ **✅ Correct approach:**
93
+
94
+ - Complete Issue A → Update A status → Add A comment → Complete Issue B → Update B status → Add B comment → ...
95
+
58
96
  ## Rules Index
59
97
 
60
98
  Some useful project rules are listed in @.cursor/rules/rules-index.mdc
package/changelog/v1.json CHANGED
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Update i18n."
6
+ ]
7
+ },
8
+ "date": "2025-12-01",
9
+ "version": "2.0.0-next.139"
10
+ },
11
+ {
12
+ "children": {},
13
+ "date": "2025-11-30",
14
+ "version": "2.0.0-next.138"
15
+ },
2
16
  {
3
17
  "children": {
4
18
  "fixes": [
@@ -190,6 +190,7 @@
190
190
  },
191
191
  "list": {
192
192
  "title": {
193
+ "custom": "لم يتم تفعيل مزود الخدمة المخصص",
193
194
  "disabled": "مزود الخدمة غير مفعل",
194
195
  "enabled": "مزود الخدمة مفعل"
195
196
  }
@@ -198,6 +199,7 @@
198
199
  "addCustomProvider": "إضافة مزود خدمة مخصص",
199
200
  "all": "الكل",
200
201
  "list": {
202
+ "custom": "المزود المخصص غير مفعل",
201
203
  "disabled": "غير مفعل",
202
204
  "disabledActions": {
203
205
  "sort": "طريقة الترتيب",
@@ -190,6 +190,7 @@
190
190
  },
191
191
  "list": {
192
192
  "title": {
193
+ "custom": "Персонализираният доставчик на услуги не е активиран",
193
194
  "disabled": "Неактивен доставчик",
194
195
  "enabled": "Активен доставчик"
195
196
  }
@@ -198,6 +199,7 @@
198
199
  "addCustomProvider": "Добавяне на персонализиран доставчик",
199
200
  "all": "Всички",
200
201
  "list": {
202
+ "custom": "Персонализираният не е активиран",
201
203
  "disabled": "Неактивиран",
202
204
  "disabledActions": {
203
205
  "sort": "Сортиране",
@@ -190,6 +190,7 @@
190
190
  },
191
191
  "list": {
192
192
  "title": {
193
+ "custom": "Benutzerdefinierter Anbieter nicht aktiviert",
193
194
  "disabled": "Dienstanbieter nicht aktiviert",
194
195
  "enabled": "Dienstanbieter aktiviert"
195
196
  }
@@ -198,6 +199,7 @@
198
199
  "addCustomProvider": "Benutzerdefinierten Anbieter hinzufügen",
199
200
  "all": "Alle",
200
201
  "list": {
202
+ "custom": "Benutzerdefiniert nicht aktiviert",
201
203
  "disabled": "Nicht aktiviert",
202
204
  "disabledActions": {
203
205
  "sort": "Sortieroptionen",
@@ -190,6 +190,7 @@
190
190
  },
191
191
  "list": {
192
192
  "title": {
193
+ "custom": "Custom provider not enabled",
193
194
  "disabled": "Disabled",
194
195
  "enabled": "Enabled"
195
196
  }
@@ -198,6 +199,7 @@
198
199
  "addCustomProvider": "Add Custom Provider",
199
200
  "all": "All",
200
201
  "list": {
202
+ "custom": "Custom not enabled",
201
203
  "disabled": "Disabled",
202
204
  "disabledActions": {
203
205
  "sort": "Sort By",
@@ -190,6 +190,7 @@
190
190
  },
191
191
  "list": {
192
192
  "title": {
193
+ "custom": "Proveedor personalizado no habilitado",
193
194
  "disabled": "Proveedor no habilitado",
194
195
  "enabled": "Proveedor habilitado"
195
196
  }
@@ -198,6 +199,7 @@
198
199
  "addCustomProvider": "Agregar proveedor personalizado",
199
200
  "all": "Todo",
200
201
  "list": {
202
+ "custom": "Personalización no habilitada",
201
203
  "disabled": "No habilitado",
202
204
  "disabledActions": {
203
205
  "sort": "Ordenar por",
@@ -190,6 +190,7 @@
190
190
  },
191
191
  "list": {
192
192
  "title": {
193
+ "custom": "ارائه‌دهنده سفارشی فعال نشده است",
193
194
  "disabled": "سرویس‌دهنده غیرفعال",
194
195
  "enabled": "سرویس‌دهنده فعال"
195
196
  }
@@ -198,6 +199,7 @@
198
199
  "addCustomProvider": "اضافه کردن ارائه‌دهنده سفارشی",
199
200
  "all": "همه",
200
201
  "list": {
202
+ "custom": "سفارشی‌سازی فعال نشده است",
201
203
  "disabled": "غیرفعال",
202
204
  "disabledActions": {
203
205
  "sort": "نحوه مرتب‌سازی",
@@ -190,6 +190,7 @@
190
190
  },
191
191
  "list": {
192
192
  "title": {
193
+ "custom": "Fournisseur personnalisé non activé",
193
194
  "disabled": "Fournisseur non activé",
194
195
  "enabled": "Fournisseur activé"
195
196
  }
@@ -198,6 +199,7 @@
198
199
  "addCustomProvider": "Ajouter un fournisseur personnalisé",
199
200
  "all": "Tout",
200
201
  "list": {
202
+ "custom": "Personnalisation non activée",
201
203
  "disabled": "Non activé",
202
204
  "disabledActions": {
203
205
  "sort": "Méthode de tri",
@@ -190,6 +190,7 @@
190
190
  },
191
191
  "list": {
192
192
  "title": {
193
+ "custom": "Fornitore personalizzato non abilitato",
193
194
  "disabled": "Fornitore non attivato",
194
195
  "enabled": "Fornitore attivato"
195
196
  }
@@ -198,6 +199,7 @@
198
199
  "addCustomProvider": "Aggiungi fornitore personalizzato",
199
200
  "all": "Tutti",
200
201
  "list": {
202
+ "custom": "Personalizzazione non abilitata",
201
203
  "disabled": "Non attivato",
202
204
  "disabledActions": {
203
205
  "sort": "Ordina per",
@@ -190,6 +190,7 @@
190
190
  },
191
191
  "list": {
192
192
  "title": {
193
+ "custom": "カスタムプロバイダーは有効化されていません",
193
194
  "disabled": "サービスプロバイダーは無効です",
194
195
  "enabled": "サービスプロバイダーは有効です"
195
196
  }
@@ -198,6 +199,7 @@
198
199
  "addCustomProvider": "カスタムサービスプロバイダーを追加",
199
200
  "all": "すべて",
200
201
  "list": {
202
+ "custom": "カスタムは無効です",
201
203
  "disabled": "未使用",
202
204
  "disabledActions": {
203
205
  "sort": "並び替え方法",
@@ -190,6 +190,7 @@
190
190
  },
191
191
  "list": {
192
192
  "title": {
193
+ "custom": "사용자 지정 서비스 제공자가 활성화되지 않았습니다",
193
194
  "disabled": "비활성화된 서비스 제공자",
194
195
  "enabled": "활성화된 서비스 제공자"
195
196
  }
@@ -198,6 +199,7 @@
198
199
  "addCustomProvider": "사용자 지정 서비스 제공자 추가",
199
200
  "all": "전체",
200
201
  "list": {
202
+ "custom": "사용자 지정이 활성화되지 않았습니다",
201
203
  "disabled": "비활성화됨",
202
204
  "disabledActions": {
203
205
  "sort": "정렬 방식",
@@ -190,6 +190,7 @@
190
190
  },
191
191
  "list": {
192
192
  "title": {
193
+ "custom": "Aangepaste provider niet ingeschakeld",
193
194
  "disabled": "Dienstverlener niet ingeschakeld",
194
195
  "enabled": "Dienstverlener ingeschakeld"
195
196
  }
@@ -198,6 +199,7 @@
198
199
  "addCustomProvider": "Voeg aangepaste provider toe",
199
200
  "all": "Alles",
200
201
  "list": {
202
+ "custom": "Aangepaste provider niet ingeschakeld",
201
203
  "disabled": "Niet ingeschakeld",
202
204
  "disabledActions": {
203
205
  "sort": "Sorteervolgorde",
@@ -190,6 +190,7 @@
190
190
  },
191
191
  "list": {
192
192
  "title": {
193
+ "custom": "Niestandardowy dostawca nie został włączony",
193
194
  "disabled": "Usługa nieaktywna",
194
195
  "enabled": "Usługa aktywna"
195
196
  }
@@ -198,6 +199,7 @@
198
199
  "addCustomProvider": "Dodaj niestandardowego dostawcę",
199
200
  "all": "Wszystko",
200
201
  "list": {
202
+ "custom": "Niestandardowy nie został włączony",
201
203
  "disabled": "Nieaktywny",
202
204
  "disabledActions": {
203
205
  "sort": "Sposób sortowania",
@@ -190,6 +190,7 @@
190
190
  },
191
191
  "list": {
192
192
  "title": {
193
+ "custom": "Serviço personalizado não ativado",
193
194
  "disabled": "Fornecedor não habilitado",
194
195
  "enabled": "Fornecedor habilitado"
195
196
  }
@@ -198,6 +199,7 @@
198
199
  "addCustomProvider": "Adicionar Provedor Personalizado",
199
200
  "all": "Todos",
200
201
  "list": {
202
+ "custom": "Personalização não ativada",
201
203
  "disabled": "Desativado",
202
204
  "disabledActions": {
203
205
  "sort": "Ordenar por",
@@ -190,6 +190,7 @@
190
190
  },
191
191
  "list": {
192
192
  "title": {
193
+ "custom": "Пользовательский поставщик не активирован",
193
194
  "disabled": "Поставщик не активирован",
194
195
  "enabled": "Поставщик активирован"
195
196
  }
@@ -198,6 +199,7 @@
198
199
  "addCustomProvider": "Добавить пользовательского провайдера",
199
200
  "all": "Все",
200
201
  "list": {
202
+ "custom": "Пользовательский не активирован",
201
203
  "disabled": "Не активирован",
202
204
  "disabledActions": {
203
205
  "sort": "Сортировка",
@@ -190,6 +190,7 @@
190
190
  },
191
191
  "list": {
192
192
  "title": {
193
+ "custom": "Özel hizmet sağlayıcısı etkin değil",
193
194
  "disabled": "Hizmet sağlayıcı devre dışı",
194
195
  "enabled": "Hizmet sağlayıcı etkin"
195
196
  }
@@ -198,6 +199,7 @@
198
199
  "addCustomProvider": "Özel Hizmet Sağlayıcı Ekle",
199
200
  "all": "Tümü",
200
201
  "list": {
202
+ "custom": "Özel ayar etkin değil",
201
203
  "disabled": "Devre Dışı",
202
204
  "disabledActions": {
203
205
  "sort": "Sıralama Yöntemi",
@@ -190,6 +190,7 @@
190
190
  },
191
191
  "list": {
192
192
  "title": {
193
+ "custom": "Chưa kích hoạt nhà cung cấp tùy chỉnh",
193
194
  "disabled": "Nhà cung cấp chưa được kích hoạt",
194
195
  "enabled": "Nhà cung cấp đã được kích hoạt"
195
196
  }
@@ -198,6 +199,7 @@
198
199
  "addCustomProvider": "Thêm nhà cung cấp tùy chỉnh",
199
200
  "all": "Tất cả",
200
201
  "list": {
202
+ "custom": "Tùy chỉnh chưa được kích hoạt",
201
203
  "disabled": "Chưa kích hoạt",
202
204
  "disabledActions": {
203
205
  "sort": "Cách sắp xếp",
@@ -190,6 +190,7 @@
190
190
  },
191
191
  "list": {
192
192
  "title": {
193
+ "custom": "未启用自定义服务商",
193
194
  "disabled": "未启用服务商",
194
195
  "enabled": "已启用服务商"
195
196
  }
@@ -198,6 +199,7 @@
198
199
  "addCustomProvider": "添加自定义服务商",
199
200
  "all": "全部",
200
201
  "list": {
202
+ "custom": "自定义未启用",
201
203
  "disabled": "未启用",
202
204
  "disabledActions": {
203
205
  "sort": "排序方式",
@@ -190,6 +190,7 @@
190
190
  },
191
191
  "list": {
192
192
  "title": {
193
+ "custom": "未啟用自訂服務商",
193
194
  "disabled": "未啟用服務商",
194
195
  "enabled": "已啟用服務商"
195
196
  }
@@ -198,6 +199,7 @@
198
199
  "addCustomProvider": "添加自定義服務商",
199
200
  "all": "全部",
200
201
  "list": {
202
+ "custom": "自訂未啟用",
201
203
  "disabled": "未啟用",
202
204
  "disabledActions": {
203
205
  "sort": "排序方式",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.137",
3
+ "version": "2.0.0-next.139",
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",
@@ -11,16 +11,21 @@ import type { IdNode, Message } from '../types';
11
11
  export class BranchResolver {
12
12
  /**
13
13
  * Get active branch ID from IdNode structure (used in contextTree building)
14
+ * Returns undefined for optimistic updates when the branch hasn't been created yet
14
15
  */
15
- getActiveBranchId(message: Message, idNode: IdNode): string {
16
+ getActiveBranchId(message: Message, idNode: IdNode): string | undefined {
16
17
  // Priority 1: Try to get from metadata.activeBranchIndex (index-based)
17
18
  const activeBranchIndex = (message.metadata as any)?.activeBranchIndex;
18
- if (
19
- typeof activeBranchIndex === 'number' &&
20
- activeBranchIndex >= 0 &&
21
- activeBranchIndex < idNode.children.length
22
- ) {
23
- return idNode.children[activeBranchIndex].id;
19
+ if (typeof activeBranchIndex === 'number' && activeBranchIndex >= 0) {
20
+ // If index is within bounds, return the branch at that index
21
+ if (activeBranchIndex < idNode.children.length) {
22
+ return idNode.children[activeBranchIndex].id;
23
+ }
24
+ // Optimistic update: index === children.length means branch is being created
25
+ if (activeBranchIndex === idNode.children.length) {
26
+ return undefined;
27
+ }
28
+ // Invalid index (> children.length), ignore and continue to other strategies
24
29
  }
25
30
 
26
31
  // Priority 2: Infer from which branch has children
@@ -36,20 +41,25 @@ export class BranchResolver {
36
41
 
37
42
  /**
38
43
  * Get active branch ID from flat list (used in flatList building)
44
+ * Returns undefined for optimistic updates when the branch hasn't been created yet
39
45
  */
40
46
  getActiveBranchIdFromMetadata(
41
47
  message: Message,
42
48
  childIds: string[],
43
49
  childrenMap: Map<string | null, string[]>,
44
- ): string {
50
+ ): string | undefined {
45
51
  // Priority 1: Try to get from metadata.activeBranchIndex (index-based)
46
52
  const activeBranchIndex = (message.metadata as any)?.activeBranchIndex;
47
- if (
48
- typeof activeBranchIndex === 'number' &&
49
- activeBranchIndex >= 0 &&
50
- activeBranchIndex < childIds.length
51
- ) {
52
- return childIds[activeBranchIndex];
53
+ if (typeof activeBranchIndex === 'number' && activeBranchIndex >= 0) {
54
+ // If index is within bounds, return the branch at that index
55
+ if (activeBranchIndex < childIds.length) {
56
+ return childIds[activeBranchIndex];
57
+ }
58
+ // Optimistic update: index === childIds.length means branch is being created
59
+ if (activeBranchIndex === childIds.length) {
60
+ return undefined;
61
+ }
62
+ // Invalid index (> childIds.length), ignore and continue to other strategies
53
63
  }
54
64
 
55
65
  // Priority 2: Infer from which child has descendants
@@ -185,7 +185,12 @@ export class ContextTreeBuilder {
185
185
  */
186
186
  private createBranchNode(message: Message, idNode: IdNode): BranchNode {
187
187
  const activeBranchId = this.branchResolver.getActiveBranchId(message, idNode);
188
- const activeBranchIndex = idNode.children.findIndex((child) => child.id === activeBranchId);
188
+
189
+ // For optimistic update (activeBranchId is undefined), use children.length as the index
190
+ // This indicates the branch is being created but doesn't exist yet
191
+ const activeBranchIndex = activeBranchId
192
+ ? idNode.children.findIndex((child) => child.id === activeBranchId)
193
+ : idNode.children.length;
189
194
 
190
195
  // Each branch is a tree starting from that child
191
196
  const branches = idNode.children.map((child) => {
@@ -165,6 +165,15 @@ export class FlatListBuilder {
165
165
  childMessages,
166
166
  this.childrenMap,
167
167
  );
168
+
169
+ // Optimistic update: activeBranchId is undefined when branch is being created
170
+ // In this case, just add user message without branch info and continue
171
+ if (!activeBranchId) {
172
+ flatList.push(message);
173
+ processedIds.add(message.id);
174
+ continue;
175
+ }
176
+
168
177
  const activeBranchIndex = childMessages.indexOf(activeBranchId);
169
178
  const userWithBranches = this.createUserMessageWithBranches(
170
179
  message,
@@ -248,6 +257,12 @@ export class FlatListBuilder {
248
257
  flatList.push(message);
249
258
  processedIds.add(message.id);
250
259
 
260
+ // Optimistic update: activeBranchId is undefined when branch is being created
261
+ // In this case, just add assistant message and continue (no active branch yet)
262
+ if (!activeBranchId) {
263
+ continue;
264
+ }
265
+
251
266
  // Continue with active branch and process its message
252
267
  const activeBranchMsg = this.messageMap.get(activeBranchId);
253
268
  if (activeBranchMsg) {
@@ -71,13 +71,13 @@ describe('BranchResolver', () => {
71
71
  expect(resolver.getActiveBranchId(message, idNode)).toBe('msg-2');
72
72
  });
73
73
 
74
- it('should ignore invalid activeBranchIndex', () => {
74
+ it('should return undefined for optimistic update (activeBranchIndex === children.length)', () => {
75
75
  const message: Message = {
76
76
  content: 'test',
77
77
  createdAt: 0,
78
78
  id: 'msg-1',
79
79
  meta: {},
80
- metadata: { activeBranchIndex: 5 }, // out of bounds
80
+ metadata: { activeBranchIndex: 2 }, // index = children.length (optimistic update)
81
81
  role: 'user',
82
82
  updatedAt: 0,
83
83
  };
@@ -90,7 +90,31 @@ describe('BranchResolver', () => {
90
90
  id: 'msg-1',
91
91
  };
92
92
 
93
- // Should default to first branch
93
+ // When activeBranchIndex === children.length, it's an optimistic update
94
+ // The branch hasn't been created yet, so return undefined
95
+ expect(resolver.getActiveBranchId(message, idNode)).toBeUndefined();
96
+ });
97
+
98
+ it('should ignore activeBranchIndex when it exceeds optimistic update range', () => {
99
+ const message: Message = {
100
+ content: 'test',
101
+ createdAt: 0,
102
+ id: 'msg-1',
103
+ meta: {},
104
+ metadata: { activeBranchIndex: 5 }, // > children.length (invalid)
105
+ role: 'user',
106
+ updatedAt: 0,
107
+ };
108
+
109
+ const idNode: IdNode = {
110
+ children: [
111
+ { children: [], id: 'msg-2' },
112
+ { children: [], id: 'msg-3' },
113
+ ],
114
+ id: 'msg-1',
115
+ };
116
+
117
+ // activeBranchIndex > children.length should be ignored, fallback to default
94
118
  expect(resolver.getActiveBranchId(message, idNode)).toBe('msg-2');
95
119
  });
96
120
  });
@@ -147,5 +171,44 @@ describe('BranchResolver', () => {
147
171
 
148
172
  expect(resolver.getActiveBranchIdFromMetadata(message, childIds, childrenMap)).toBe('msg-2');
149
173
  });
174
+
175
+ it('should return undefined for optimistic update (activeBranchIndex === childIds.length)', () => {
176
+ const message: Message = {
177
+ content: 'test',
178
+ createdAt: 0,
179
+ id: 'msg-1',
180
+ meta: {},
181
+ metadata: { activeBranchIndex: 2 }, // index = childIds.length (optimistic update)
182
+ role: 'user',
183
+ updatedAt: 0,
184
+ };
185
+
186
+ const childIds = ['msg-2', 'msg-3'];
187
+ const childrenMap = new Map<string | null, string[]>();
188
+
189
+ // When activeBranchIndex === childIds.length, it's an optimistic update
190
+ // The branch hasn't been created yet, so return undefined
191
+ expect(
192
+ resolver.getActiveBranchIdFromMetadata(message, childIds, childrenMap),
193
+ ).toBeUndefined();
194
+ });
195
+
196
+ it('should ignore activeBranchIndex when it exceeds optimistic update range', () => {
197
+ const message: Message = {
198
+ content: 'test',
199
+ createdAt: 0,
200
+ id: 'msg-1',
201
+ meta: {},
202
+ metadata: { activeBranchIndex: 5 }, // > childIds.length (invalid)
203
+ role: 'user',
204
+ updatedAt: 0,
205
+ };
206
+
207
+ const childIds = ['msg-2', 'msg-3'];
208
+ const childrenMap = new Map<string | null, string[]>();
209
+
210
+ // activeBranchIndex > childIds.length should be ignored, fallback to default
211
+ expect(resolver.getActiveBranchIdFromMetadata(message, childIds, childrenMap)).toBe('msg-2');
212
+ });
150
213
  });
151
214
  });