A React Native app for the ultimate thinking partner.

fix: simplify message transformation to return ALL messages

Removed complex grouping and reasoning attachment logic that was
discarding 7 out of 23 messages from the API.

Before:
- Grouped messages by run_id + step_id
- Combined reasoning into other messages
- Only processed 'otherMessages', discarding standalone reasoning
- 120+ lines of complex logic
- Lost messages that didn't fit the grouping pattern

After:
- Simple map() transformation
- Convert API format to app format
- Return ALL messages as-is
- 40 lines, easy to understand
- UI handles rendering based on message_type

This fixes the issue where users only saw 10-16 messages when the
API returned 23+ messages. Now all messages are preserved and the
UI can render them appropriately based on their type.

+45 -134
+45 -134
src/api/lettaApi.ts
··· 586 586 const response = await this.client.agents.messages.list(agentId, params); 587 587 console.log('listMessages - response count:', response?.length || 0); 588 588 589 - // Group messages by run_id and step_id to associate reasoning with assistant messages 590 - const groupedMessages = new Map<string, any[]>(); 589 + // Simple transformation - just convert format, no grouping or attaching 590 + const transformedMessages: LettaMessage[] = response.map((message: any) => { 591 + const type = message.messageType as string; 592 + const toolCall = message.tool_call || message.toolCall || (message.tool_calls && message.tool_calls[0]); 593 + const toolReturn = message.tool_response || message.toolResponse || message.tool_return || message.toolReturn; 591 594 592 - // First pass: group messages by run_id + step_id 593 - response.forEach((message: any) => { 594 - const key = `${message.runId || 'no-run'}-${message.stepId || 'no-step'}`; 595 - if (!groupedMessages.has(key)) { 596 - groupedMessages.set(key, []); 595 + let role: 'user' | 'assistant' | 'system' | 'tool' = 'assistant'; 596 + if (type === 'user_message') { 597 + role = 'user'; 598 + } else if (type === 'system_message') { 599 + role = 'system'; 600 + } else if (type === 'assistant_message' || type === 'reasoning_message') { 601 + role = 'assistant'; 602 + } else if (type === 'tool_call' || type === 'tool_call_message' || type === 'tool_response' || type === 'tool_return_message' || type === 'tool_message') { 603 + role = 'tool'; 597 604 } 598 - groupedMessages.get(key)!.push(message); 599 - }); 600 605 601 - // Second pass: process groups to combine reasoning with assistant messages 602 - const transformedMessages: LettaMessage[] = []; 606 + // Get content - for reasoning messages, content is the reasoning text 607 + let content: string = message.content || message.reasoning || ''; 603 608 604 - for (const [key, messageGroup] of groupedMessages) { 605 - // Sort messages in the group by creation time or message order 606 - messageGroup.sort((a, b) => { 607 - if (a.date && b.date) { 608 - return new Date(a.date).getTime() - new Date(b.date).getTime(); 609 - } 610 - return 0; 611 - }); 612 - 613 - // Find reasoning and assistant messages in this group 614 - const reasoningMessages = messageGroup.filter((m: any) => m.messageType === 'reasoning_message'); 615 - const otherMessages = messageGroup.filter(m => m.messageType !== 'reasoning_message'); 616 - 617 - // Combine reasoning content 618 - const combinedReasoning = reasoningMessages 619 - .map(m => m.reasoning || m.content || '') 620 - .filter(r => r.trim()) 621 - .join(' '); 622 - 623 - // Process other messages and attach reasoning to assistant messages 624 - otherMessages.forEach((message: any) => { 625 - // Filter out heartbeat messages from user messages 626 - if (message.messageType === 'user_message' && typeof message.content === 'string') { 627 - try { 628 - const parsed = JSON.parse(message.content); 629 - if (parsed?.type === 'heartbeat') { 630 - return; // Skip heartbeat messages 631 - } 632 - } catch { 633 - // Keep message if content is not valid JSON 634 - } 635 - } 636 - 637 - // Map messageType to role and content for our components 638 - const type = message.messageType as string; 639 - const toolCall = message.tool_call || message.toolCall || (message.tool_calls && message.tool_calls[0]); 640 - const toolReturn = message.tool_response || message.toolResponse || message.tool_return || message.toolReturn; 641 - 642 - let role: 'user' | 'assistant' | 'system' | 'tool' = 'assistant'; 643 - if (type === 'user_message') { 644 - role = 'user'; 645 - } else if (type === 'system_message') { 646 - role = 'system'; 647 - } else if (type === 'assistant_message' || type === 'reasoning_message') { 648 - role = 'assistant'; 649 - } else if (type === 'tool_call' || type === 'tool_call_message' || type === 'tool_response' || type === 'tool_return_message' || type === 'tool_message') { 650 - role = 'tool'; 651 - } 652 - 653 - // Derive a readable content string for tool steps 654 - let content: string = message.content || ''; 655 - if ((!content || typeof content !== 'string') && type) { 656 - if (type === 'tool_call' || type === 'tool_call_message' || type === 'tool_message') { 657 - const callObj = toolCall?.function ? toolCall.function : toolCall; 658 - const name = callObj?.name || callObj?.tool_name || 'tool'; 659 - const argsRaw = callObj?.arguments ?? callObj?.args ?? {}; 660 - let args = ''; 661 - try { 662 - if (typeof argsRaw === 'string') { 663 - args = argsRaw; 664 - } else { 665 - args = JSON.stringify(argsRaw); 666 - } 667 - } catch { args = String(argsRaw); } 668 - content = `${name}(${args})`; 669 - } else if (type === 'tool_response' || type === 'tool_return_message') { 670 - if (toolReturn != null) { 671 - try { content = typeof toolReturn === 'string' ? toolReturn : JSON.stringify(toolReturn); } 672 - catch { content = String(toolReturn); } 673 - } 674 - } 675 - } 676 - 677 - const transformedMessage: LettaMessage = { 678 - id: message.id, 679 - role, 680 - content, 681 - created_at: message.date ? message.date.toISOString() : new Date().toISOString(), 682 - tool_calls: message.tool_calls, 683 - message_type: type, 684 - sender_id: message.senderId, 685 - step_id: message.stepId || message.step_id, 686 - run_id: message.runId, 687 - // Pass through tool details for UI reassembly 688 - tool_call: toolCall, 689 - tool_response: toolReturn, 690 - }; 691 - 692 - // Attach reasoning to assistant and tool messages 693 - if ((role === 'assistant' || role === 'tool') && combinedReasoning) { 694 - transformedMessage.reasoning = combinedReasoning; 695 - } 696 - 697 - transformedMessages.push(transformedMessage); 698 - }); 699 - } 700 - 701 - // Sort final messages by creation time 702 - transformedMessages.sort((a, b) => { 703 - return new Date(a.created_at).getTime() - new Date(b.created_at).getTime(); 609 + return { 610 + id: message.id, 611 + role, 612 + content, 613 + created_at: message.date ? message.date.toISOString() : new Date().toISOString(), 614 + tool_calls: message.tool_calls, 615 + message_type: type, 616 + sender_id: message.senderId, 617 + step_id: message.stepId || message.step_id, 618 + run_id: message.runId, 619 + tool_call: toolCall, 620 + tool_response: toolReturn, 621 + // For reasoning messages, store reasoning 622 + reasoning: type === 'reasoning_message' ? (message.reasoning || message.content) : undefined, 623 + }; 704 624 }); 705 625 706 - console.log('listMessages - transformed messages:', transformedMessages.slice(0, 2)); 626 + console.log('listMessages - transformed count:', transformedMessages.length); 627 + console.log('listMessages - first 2:', transformedMessages.slice(0, 2)); 707 628 return transformedMessages; 708 629 } catch (error) { 709 630 console.error('listMessages - error:', error); ··· 999 920 } 1000 921 console.log('listFolders - params:', params); 1001 922 1002 - // If searching by name, use direct API call (SDK pagination is broken) 923 + // If searching by name, paginate through SDK to find it 1003 924 if (params?.name) { 1004 - console.log('listFolders - searching for folder with name via direct API:', params.name); 925 + console.log('listFolders - searching for folder with name via SDK pagination:', params.name); 1005 926 let allFolders: any[] = []; 1006 927 let after: string | undefined = undefined; 1007 928 let pageCount = 0; ··· 1010 931 do { 1011 932 console.log(`listFolders - requesting page ${pageCount + 1} with after cursor:`, after); 1012 933 1013 - // Build query params 1014 - const queryParams = new URLSearchParams({ 1015 - limit: '50', 934 + const page = await this.client.folders.list({ 935 + limit: 50, 1016 936 ...(after && { after }) 1017 937 }); 1018 938 1019 - const response = await fetch(`https://api.letta.com/v1/folders?${queryParams}`, { 1020 - headers: { 1021 - 'Authorization': `Bearer ${this.token}`, 1022 - 'Content-Type': 'application/json' 1023 - } 1024 - }); 1025 - 1026 - if (!response.ok) { 1027 - throw new Error(`API request failed: ${response.status} ${response.statusText}`); 1028 - } 939 + // Convert response to array if needed 940 + const folders = Array.isArray(page) ? page : []; 1029 941 1030 - const page = await response.json(); 1031 - console.log(`listFolders - page ${pageCount + 1}: ${page.length} folders`); 1032 - console.log(`listFolders - page ${pageCount + 1} first 3 names:`, page.slice(0, 3).map(f => f.name)); 942 + console.log(`listFolders - page ${pageCount + 1}: ${folders.length} folders`); 943 + console.log(`listFolders - page ${pageCount + 1} first 3 names:`, folders.slice(0, 3).map(f => f.name)); 1033 944 1034 - allFolders = allFolders.concat(page); 945 + allFolders = allFolders.concat(folders); 1035 946 pageCount++; 1036 947 1037 948 // Stop if we found the folder we're looking for 1038 - const found = page.find(f => f.name === params.name); 949 + const found = folders.find(f => f.name === params.name); 1039 950 if (found) { 1040 951 console.log('listFolders - found folder:', found); 1041 952 return [found]; 1042 953 } 1043 954 1044 955 // Check if there are more pages 1045 - if (page.length < 50) { 956 + if (folders.length < 50) { 1046 957 after = undefined; 1047 958 } else { 1048 - after = page[page.length - 1]?.id; 959 + after = folders[folders.length - 1]?.id; 1049 960 } 1050 961 1051 962 } while (after && pageCount < maxPages);