A React Native app for the ultimate thinking partner.

fix: preserve message history by merging instead of replacing after streaming

Fixed critical issue where all message history disappeared after streaming
completed, leaving users with only 5-10 visible messages.

Root cause:
- Stream completion called loadMessages() without parameters
- This replaced ALL messages with only INITIAL_LOAD_LIMIT (20) messages
- System messages and filtered content reduced visible count to 5-10
- Users lost all scrollback history

Solution:
- Fetch only recent 10 messages after streaming completes
- Merge with existing messages using Map to deduplicate by ID
- Sort merged messages by timestamp to maintain order
- Preserve all previously loaded message history

Changes:
- Add optional 'limit' parameter to loadMessages()
- Replace loadMessages() call with intelligent merge logic
- Use Map<id, message> to deduplicate and update existing messages
- Keep streaming state visible until merge completes

Now users retain full message history while getting finalized versions
of newly streamed messages with proper reasoning and metadata.

+37 -9
+37 -9
App.tsx
··· 309 309 return msgs; 310 310 }; 311 311 312 - const loadMessages = async (before?: string) => { 312 + const loadMessages = async (before?: string, limit?: number) => { 313 313 if (!coAgent) return; 314 314 315 315 try { ··· 321 321 322 322 const loadedMessages = await lettaApi.listMessages(coAgent.id, { 323 323 before: before || undefined, 324 - limit: before ? PAGE_SIZE : INITIAL_LOAD_LIMIT, 324 + limit: limit || (before ? PAGE_SIZE : INITIAL_LOAD_LIMIT), 325 325 use_assistant_message: true, 326 326 }); 327 327 ··· 341 341 }, 100); 342 342 } 343 343 } 344 - setHasMoreBefore(loadedMessages.length === (before ? PAGE_SIZE : INITIAL_LOAD_LIMIT)); 344 + setHasMoreBefore(loadedMessages.length === (limit || (before ? PAGE_SIZE : INITIAL_LOAD_LIMIT))); 345 345 } else if (before) { 346 346 // No more messages to load before 347 347 setHasMoreBefore(false); ··· 691 691 }, 692 692 async (response) => { 693 693 console.log('Stream complete'); 694 - console.log('[STREAM COMPLETE] Reloading messages from server'); 694 + console.log('[STREAM COMPLETE] Fetching finalized messages from server'); 695 695 696 - // Wait for server to finalize messages, then reload and clear streaming state 696 + // Wait for server to finalize messages 697 697 setTimeout(async () => { 698 - await loadMessages(); 699 - // Clear streaming state after messages are loaded 700 - setIsStreaming(false); 701 - setCurrentStream({ reasoning: '', toolCalls: [], assistantMessage: '' }); 698 + try { 699 + // Fetch recent messages to get finalized versions 700 + const recentMessages = await lettaApi.listMessages(coAgent.id, { 701 + limit: 10, 702 + use_assistant_message: true, 703 + }); 704 + 705 + if (recentMessages.length > 0) { 706 + setMessages(prev => { 707 + // Create a map of existing messages by ID 708 + const messageMap = new Map(prev.map(msg => [msg.id, msg])); 709 + 710 + // Add/update with recent messages 711 + recentMessages.forEach(msg => { 712 + messageMap.set(msg.id, msg); 713 + }); 714 + 715 + // Convert back to array and sort by timestamp 716 + const merged = Array.from(messageMap.values()).sort((a, b) => { 717 + return new Date(a.created_at || 0).getTime() - new Date(b.created_at || 0).getTime(); 718 + }); 719 + 720 + return filterFirstMessage(merged); 721 + }); 722 + } 723 + } catch (error) { 724 + console.error('Failed to fetch finalized messages:', error); 725 + } finally { 726 + // Clear streaming state after attempting to load 727 + setIsStreaming(false); 728 + setCurrentStream({ reasoning: '', toolCalls: [], assistantMessage: '' }); 729 + } 702 730 }, 300); 703 731 }, 704 732 (error) => {