From fcfd3873eaf9541c8abbbf8d3c890369fb0fdab4 Mon Sep 17 00:00:00 2001 From: santino1919 Date: Tue, 20 Jan 2026 22:58:32 +0100 Subject: [PATCH 1/9] fix: preserve conversationId in URL when navigating to chat tab - Prevents conversationId from being lost when switching tabs - Reads from storage and includes in URL for web navigation - Fixes issue where chat would reload unnecessarily when switching tabs --- apps/client/components/navigation/TabBar.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/client/components/navigation/TabBar.tsx b/apps/client/components/navigation/TabBar.tsx index 00e76571..5119ee1a 100644 --- a/apps/client/components/navigation/TabBar.tsx +++ b/apps/client/components/navigation/TabBar.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { View, TouchableOpacity, StyleSheet, Platform } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { usePathname, useRouter } from 'expo-router'; +import { storage, SECURE_STORAGE_KEYS } from '../../lib/storage'; type TabBarProps = { state?: any; @@ -20,8 +21,24 @@ export function TabBar({ state, descriptors, navigation }: TabBarProps) { { name: 'account', icon: 'person', route: '/(main)/account' }, ]; - const handlePress = (route: string) => { + const handlePress = async (route: string) => { if (Platform.OS === 'web') { + // Preserve conversationId when navigating to chat tab + if (route === '/(main)/chat') { + try { + const currentConversationId = await storage.persistent.getItem( + SECURE_STORAGE_KEYS.CURRENT_CONVERSATION_ID + ); + const url = currentConversationId + ? `/(main)/chat?conversationId=${currentConversationId}` + : '/(main)/chat'; + router.push(url as any); + return; + } catch (error) { + console.warn('Could not read conversation ID from storage:', error); + // Fallback to regular route if storage read fails + } + } router.push(route as any); } else if (navigation) { navigation.navigate(route); From 7a936387dd3687715e06865291d1b464a0120778 Mon Sep 17 00:00:00 2001 From: santino1919 Date: Tue, 20 Jan 2026 22:59:47 +0100 Subject: [PATCH 2/9] fix: prevent new conversations from being created on tab switch - Add race condition protection with isLoadingRef - Add retry logic with 100ms delay for storage reads - Only create new conversation if none exists after retry - Prevents duplicate conversations when switching tabs --- apps/client/hooks/useActiveConversation.ts | 39 ++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/apps/client/hooks/useActiveConversation.ts b/apps/client/hooks/useActiveConversation.ts index 26db35d9..00d568e2 100644 --- a/apps/client/hooks/useActiveConversation.ts +++ b/apps/client/hooks/useActiveConversation.ts @@ -15,32 +15,44 @@ export function useActiveConversation({ userId }: UseActiveConversationProps) { const [conversationId, setConversationId] = useState(null); const [isLoading, setIsLoading] = useState(true); const hasUpdatedUrlRef = useRef(false); + const isLoadingRef = useRef(false); // Prevent concurrent loads const { setConversationId: setGlobalConversationId } = useActiveConversationContext(); useEffect(() => { const loadActiveConversation = async () => { + // Prevent concurrent loads + if (isLoadingRef.current) { + console.log('⏳ [useActiveConversation] Load already in progress, skipping...'); + return; + } + if (!userId) { setConversationId(null); setGlobalConversationId(null); setIsLoading(false); + isLoadingRef.current = false; return; } try { + isLoadingRef.current = true; setIsLoading(true); const conversationIdParam = params.conversationId as string; + // Priority 1: URL param (most reliable) if (conversationIdParam) { hasUpdatedUrlRef.current = false; setConversationId(conversationIdParam); setGlobalConversationId(conversationIdParam); await storage.persistent.setItem(SECURE_STORAGE_KEYS.CURRENT_CONVERSATION_ID, conversationIdParam); setIsLoading(false); + isLoadingRef.current = false; return; } + // Priority 2: Storage (wait for it properly) let activeConversationId: string | null = null; try { activeConversationId = await storage.persistent.getItem(SECURE_STORAGE_KEYS.CURRENT_CONVERSATION_ID); @@ -49,6 +61,8 @@ export function useActiveConversation({ userId }: UseActiveConversationProps) { } if (activeConversationId) { + console.log('✅ [useActiveConversation] Found conversationId in storage:', activeConversationId); + // Update URL for web to preserve it if (Platform.OS === 'web' && !params.conversationId && !hasUpdatedUrlRef.current) { hasUpdatedUrlRef.current = true; router.replace(`/(main)/chat?conversationId=${activeConversationId}`); @@ -56,19 +70,44 @@ export function useActiveConversation({ userId }: UseActiveConversationProps) { setConversationId(activeConversationId); setGlobalConversationId(activeConversationId); setIsLoading(false); + isLoadingRef.current = false; + return; + } + + // Priority 3: Only create new if we truly don't have one + // Add a small delay to allow any pending storage writes to complete + console.log('⚠️ [useActiveConversation] No conversationId found, waiting before creating new...'); + await new Promise(resolve => setTimeout(resolve, 100)); + + // Double-check storage after delay (in case write was in progress) + const retryConversationId = await storage.persistent.getItem(SECURE_STORAGE_KEYS.CURRENT_CONVERSATION_ID); + if (retryConversationId) { + console.log('✅ [useActiveConversation] Found conversationId on retry:', retryConversationId); + if (Platform.OS === 'web' && !params.conversationId && !hasUpdatedUrlRef.current) { + hasUpdatedUrlRef.current = true; + router.replace(`/(main)/chat?conversationId=${retryConversationId}`); + } + setConversationId(retryConversationId); + setGlobalConversationId(retryConversationId); + setIsLoading(false); + isLoadingRef.current = false; return; } + // Only now create new conversation if we really don't have one + console.log('🆕 [useActiveConversation] Creating new conversation...'); const conversationData = await getCurrentOrCreateConversation(userId); setConversationId(conversationData.conversationId); setGlobalConversationId(conversationData.conversationId); setIsLoading(false); + isLoadingRef.current = false; } catch (error) { console.error('Error loading active conversation:', error); setConversationId(null); setGlobalConversationId(null); setIsLoading(false); + isLoadingRef.current = false; } }; From a8b019f5dbf2676442fd60c8141d8d89b39685ec Mon Sep 17 00:00:00 2001 From: santino1919 Date: Tue, 20 Jan 2026 23:00:01 +0100 Subject: [PATCH 3/9] fix: reload messages when returning to conversation with no messages - Check if messages exist before skipping reload - Reset refs to allow reload if same conversation has no messages - Prevents empty chat when switching back to previous conversation --- apps/client/components/chat/ChatManager.tsx | 56 ++++++++++++++++----- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/apps/client/components/chat/ChatManager.tsx b/apps/client/components/chat/ChatManager.tsx index ac9582a4..867417a2 100644 --- a/apps/client/components/chat/ChatManager.tsx +++ b/apps/client/components/chat/ChatManager.tsx @@ -117,14 +117,22 @@ export function ChatManager({}: ChatManagerProps) { useEffect(() => { const previousId = previousConversationIdRef.current; - if (previousId && previousId !== currentConversationId) { + // Only treat as actual change if both IDs are valid and different + const previousWasValid = previousId && previousId !== 'temp-loading'; + const currentIsValid = currentConversationId && currentConversationId !== 'temp-loading'; + const isActualChange = previousWasValid && currentIsValid && previousId !== currentConversationId; + + if (isActualChange) { console.log('🔄 [ChatManager] Conversation changed:', { from: previousId, to: currentConversationId }); console.log('🛑 [ChatManager] Stopping previous conversation stream'); stop(); clearChatCache(); } - previousConversationIdRef.current = currentConversationId; + // Only update ref with valid IDs (preserve previous during loading) + if (currentConversationId && currentConversationId !== 'temp-loading') { + previousConversationIdRef.current = currentConversationId; + } }, [currentConversationId, stop]); // Load historical messages when conversation ID changes @@ -143,16 +151,40 @@ export function ChatManager({}: ChatManagerProps) { return; } - // Clear messages immediately when conversation changes to prevent showing old messages - console.log('🧹 [ChatManager] Clearing messages for conversation switch to:', currentConversationId); - console.log(' Before clear - messages.length:', messages.length); - console.log(' Before clear - initialMessages.length:', initialMessages.length); - setMessages([]); - setInitialMessages([]); - setInitialMessagesConversationId(null); // Track what conversation initialMessages belong to - conversationMessagesSetRef.current = null; // Reset ref so new messages can be set - console.log('✅ [ChatManager] Messages cleared (setMessages([]) and setInitialMessages([]) called)'); - console.log('✅ [ChatManager] Ref and conversationId tracker reset'); + const previousId = previousConversationIdRef.current; + const previousWasValid = previousId && previousId !== 'temp-loading'; + const currentIsValid = currentConversationId && currentConversationId !== 'temp-loading'; + const isActualChange = previousWasValid && currentIsValid && previousId !== currentConversationId; + const isSameConversation = previousId === currentConversationId; + + // Check if we already loaded messages for this conversation + const alreadyLoaded = conversationMessagesSetRef.current === currentConversationId; + const hasMessages = messages.length > 0 || initialMessages.length > 0; + + // Only clear messages if this is an ACTUAL conversation change (not loading state) + if (isActualChange) { + console.log('🧹 [ChatManager] Clearing messages for actual conversation change to:', currentConversationId); + console.log(' Before clear - messages.length:', messages.length); + console.log(' Before clear - initialMessages.length:', initialMessages.length); + setMessages([]); + setInitialMessages([]); + setInitialMessagesConversationId(null); + conversationMessagesSetRef.current = null; + console.log('✅ [ChatManager] Messages cleared for actual conversation change'); + } else if (isSameConversation && alreadyLoaded && !hasMessages) { + // Same conversation but no messages (they were cleared) - reset to allow reload + console.log('⚠️ [ChatManager] Same conversation but no messages - resetting to allow reload'); + conversationMessagesSetRef.current = null; + setInitialMessagesConversationId(null); + } else if (isSameConversation && alreadyLoaded && hasMessages) { + // Same conversation with messages already loaded - skip reload + console.log('⏭️ [ChatManager] Same conversation with messages - preserving (skipping reload)'); + setIsLoadingHistory(false); + updateChatCache({ isLoadingHistory: false }); + return; // Exit early, don't reload + } else { + console.log('⏭️ [ChatManager] Loading state or initial load - will load messages'); + } let isCancelled = false; From 7042b600326ab0b77fe463a284e585cba04a6790 Mon Sep 17 00:00:00 2001 From: santino1919 Date: Tue, 20 Jan 2026 23:00:55 +0100 Subject: [PATCH 4/9] fix: add timeout handling for chat creation - Add 15s timeout guard to prevent hanging loading state - Add 10s timeout for createNewConversation promise - Improve error handling and state cleanup --- apps/client/app/(main)/chat-history.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/client/app/(main)/chat-history.tsx b/apps/client/app/(main)/chat-history.tsx index b831d69e..91695604 100644 --- a/apps/client/app/(main)/chat-history.tsx +++ b/apps/client/app/(main)/chat-history.tsx @@ -467,17 +467,34 @@ export default function ChatHistoryScreen() { setIsCreatingChat(true); + // Add a timeout guard to ensure loading state is reset even if operation hangs + const timeoutId = setTimeout(() => { + console.warn('⚠️ Chat creation taking too long, resetting loading state'); + setIsCreatingChat(false); + }, 15000); // 15 second timeout + try { - const conversationData = await createNewConversation(user?.id); + // Add timeout wrapper around createNewConversation + const conversationData = await Promise.race([ + createNewConversation(user?.id), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Chat creation timeout')), 10000) + ) + ]); + + clearTimeout(timeoutId); setCurrentConversationId(conversationData.conversationId); router.push(`/(main)/chat?conversationId=${conversationData.conversationId}`); + // Reset loading state after navigation setTimeout(() => { setIsCreatingChat(false); }, 100); } catch (error) { + clearTimeout(timeoutId); console.error('Error creating new chat:', error); setIsCreatingChat(false); + // Optionally show an error message to the user } }; From 28bf8dc0745ff4220881b3dda32b5cc7b03360bb Mon Sep 17 00:00:00 2001 From: santino1919 Date: Tue, 20 Jan 2026 23:01:08 +0100 Subject: [PATCH 5/9] refactor: clean up debug code and improve auth timeout handling - Remove verbose debug logging from createConversationDirectly - Simplify auth flow by using provided userId first - Add timeout handling for auth operations --- .../features/chat/services/conversations.ts | 207 +++++++++++++----- 1 file changed, 154 insertions(+), 53 deletions(-) diff --git a/apps/client/features/chat/services/conversations.ts b/apps/client/features/chat/services/conversations.ts index 4f38ecb3..0717aa16 100644 --- a/apps/client/features/chat/services/conversations.ts +++ b/apps/client/features/chat/services/conversations.ts @@ -19,34 +19,7 @@ async function createConversationDirectly(conversationId: string, userId?: strin console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.log('[createConversation] Starting with conversationId:', conversationId); - // First, let's check the current auth state - const { data: { session }, error: sessionError } = await supabase.auth.getSession(); - console.log('[createConversation] Current session state:', { - hasSession: !!session, - hasUser: !!session?.user, - userId: session?.user?.id, - isExpired: session && session.expires_at ? new Date() > new Date(session.expires_at * 1000) : 'no session', - sessionError - }); - - // Test if we can read from conversations table (to verify RLS is working) - if (session?.user?.id) { - console.log('[createConversation] Testing SELECT permission on conversations table...'); - const { data: testData, error: testError } = await supabase - .from('conversations') - .select('id') - .eq('user_id', session.user.id) - .limit(1); - - console.log('[createConversation] SELECT test result:', { - canRead: !testError, - testError: testError?.message, - testErrorCode: testError?.code, - foundConversations: testData?.length || 0 - }); - } - - // Use provided userId if available, otherwise get from Supabase auth + // Use provided userId if available FIRST (skip unnecessary auth calls) let authUser; if (userId) { console.log('[createConversation] Using provided userId:', userId); @@ -54,30 +27,70 @@ async function createConversationDirectly(conversationId: string, userId?: strin } else { console.log('[createConversation] Getting authenticated user from Supabase...'); - // Try with a timeout to avoid hanging - const authPromise = supabase.auth.getUser(); - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('Auth timeout')), 5000) + // Add timeout to getSession call + const sessionPromise = supabase.auth.getSession(); + const sessionTimeout = new Promise((_, reject) => + setTimeout(() => reject(new Error('getSession timeout after 5 seconds')), 5000) ); + let session; try { - const { data: { user } } = await Promise.race([authPromise, timeoutPromise]) as any; - authUser = user; - console.log('[createConversation] Auth user result:', authUser ? 'found' : 'not found'); + const result = await Promise.race([sessionPromise, sessionTimeout]); + session = result.data?.session; + console.log('[createConversation] Current session state:', { + hasSession: !!session, + hasUser: !!session?.user, + userId: session?.user?.id, + isExpired: session && session.expires_at ? new Date() > new Date(session.expires_at * 1000) : 'no session', + }); } catch (error) { - console.error('[createConversation] Auth error or timeout:', error); + console.error('[createConversation] getSession error or timeout:', error); + // Try getUser as fallback with timeout + const authPromise = supabase.auth.getUser(); + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Auth timeout')), 5000) + ); - // Try alternative: get session instead - console.log('[createConversation] Trying getSession as fallback...'); try { - const { data: { session } } = await supabase.auth.getSession(); - authUser = session?.user; - console.log('[createConversation] Session user result:', authUser ? 'found' : 'not found'); - } catch (sessionError) { - console.error('[createConversation] Session error:', sessionError); + const { data: { user } } = await Promise.race([authPromise, timeoutPromise]) as any; + authUser = user; + console.log('[createConversation] Auth user result:', authUser ? 'found' : 'not found'); + } catch (authError) { + console.error('[createConversation] Auth error or timeout:', authError); return false; } } + + if (!authUser && session?.user) { + authUser = session.user; + } + + // Test if we can read from conversations table (to verify RLS is working) + if (authUser?.id) { + console.log('[createConversation] Testing SELECT permission on conversations table...'); + const selectPromise = supabase + .from('conversations') + .select('id') + .eq('user_id', authUser.id) + .limit(1); + + const selectTimeout = new Promise((_, reject) => + setTimeout(() => reject(new Error('SELECT test timeout')), 3000) + ); + + try { + const { data: testData, error: testError } = await Promise.race([selectPromise, selectTimeout]); + console.log('[createConversation] SELECT test result:', { + canRead: !testError, + testError: testError?.message, + testErrorCode: testError?.code, + foundConversations: testData?.length || 0 + }); + } catch (error) { + console.warn('[createConversation] SELECT test timed out or failed:', error); + // Continue anyway - this is just a test + } + } } if (!authUser?.id) { @@ -100,7 +113,8 @@ async function createConversationDirectly(conversationId: string, userId?: strin console.log('[createConversation] Calling Supabase INSERT...'); const insertStartTime = Date.now(); - const { data, error } = await supabase + // Wrap the INSERT to capture any errors before timeout + const insertPromise = supabase .from('conversations') .insert({ id: conversationId, @@ -113,6 +127,67 @@ async function createConversationDirectly(conversationId: string, userId?: strin }) .select(); // Add select to get the inserted data back + // Add error logging wrapper + const insertWithLogging = insertPromise.then( + (result) => { + console.log('[createConversation] INSERT promise resolved'); + return result; + }, + (error) => { + console.error('[createConversation] INSERT promise rejected with error:', { + error, + message: error?.message, + code: error?.code, + details: error?.details, + hint: error?.hint, + status: error?.status, + statusText: error?.statusText + }); + throw error; + } + ); + + const insertTimeout = new Promise((_, reject) => + setTimeout(() => { + console.error('[createConversation] INSERT operation timed out after 15 seconds'); + console.error('[createConversation] This usually indicates:'); + console.error(' 1. RLS policy blocking the INSERT'); + console.error(' 2. Network connectivity issue'); + console.error(' 3. Database trigger hanging'); + console.error(' 4. Supabase service issue'); + console.error(' 5. CORS issue (if on web)'); + console.error('[createConversation] Check browser Network tab to see if request is being sent'); + reject(new Error('INSERT timeout after 15 seconds')); + }, 15000) + ); + + let result; + try { + result = await Promise.race([insertWithLogging, insertTimeout]); + } catch (raceError) { + // If it's our timeout error, try to get the actual error from the original promise + if (raceError instanceof Error && raceError.message === 'INSERT timeout after 8 seconds') { + console.error('[createConversation] Timeout occurred. Checking if original promise has error...'); + // Wait a bit more to see if we get an actual error + try { + const actualResult = await Promise.race([ + insertPromise, + new Promise((_, reject) => + setTimeout(() => reject(new Error('Second timeout')), 2000) + ) + ]); + result = actualResult; + } catch (actualError) { + console.error('[createConversation] Actual error from Supabase:', actualError); + throw raceError; // Still throw timeout error but we've logged the real one + } + } else { + throw raceError; + } + } + + const { data, error } = result; + const insertDuration = Date.now() - insertStartTime; console.log(`[createConversation] INSERT completed in ${insertDuration}ms`); @@ -134,11 +209,19 @@ async function createConversationDirectly(conversationId: string, userId?: strin return false; } - console.log('✅ [createConversation] Successfully created conversation in database!'); - console.log('[createConversation] Inserted data:', { - conversationId, - insertedData: data - }); + // Check if data is empty (can happen with RLS filtering) + if (!data || (Array.isArray(data) && data.length === 0)) { + console.log('⚠️ [createConversation] INSERT succeeded but response data is empty (likely RLS filtering)'); + console.log('[createConversation] This is OK - conversation was created (status 201), just not returned in response'); + console.log('✅ [createConversation] Successfully created conversation in database!'); + console.log('[createConversation] Conversation ID:', conversationId); + } else { + console.log('✅ [createConversation] Successfully created conversation in database!'); + console.log('[createConversation] Inserted data:', { + conversationId, + insertedData: data + }); + } // NOTE: Broadcast is now handled by database trigger (migration 089) // No need for manual broadcast anymore @@ -164,7 +247,8 @@ async function createConversationWithMetadata(conversationId: string, userId: st metadata }); - const { data, error } = await supabase + // Add timeout to INSERT operation + const insertPromise = supabase .from('conversations') .insert({ id: conversationId, @@ -177,6 +261,12 @@ async function createConversationWithMetadata(conversationId: string, userId: st }) .select(); + const insertTimeout = new Promise((_, reject) => + setTimeout(() => reject(new Error('INSERT timeout after 8 seconds')), 8000) + ); + + const { data, error } = await Promise.race([insertPromise, insertTimeout]); + if (error) { if (error.code === '23505') { console.log('[createConversationWithMetadata] Conversation already exists (race condition)'); @@ -216,9 +306,20 @@ export async function createNewConversation(userId?: string, metadata?: Record((_, reject) => + setTimeout(() => reject(new Error('getUser timeout after 5 seconds')), 5000) + ); + + try { + const { data: { user } } = await Promise.race([getUserPromise, getUserTimeout]); + authUserId = user?.id; + console.log('✅ Got userId:', authUserId); + } catch (error) { + console.error('❌ Error getting userId:', error); + throw new Error('Failed to get user ID from authentication'); + } } if (!authUserId) { From cb69b66678c8d3de995de7a303eda8e15dacf195 Mon Sep 17 00:00:00 2001 From: santino1919 Date: Wed, 21 Jan 2026 13:43:32 +0100 Subject: [PATCH 6/9] fix(chat): add context conversationId check to prevent duplicate creation - Add Priority 2 check for contextConversationId before checking storage - Use context conversationId if available (fastest path) - Update URL for web to preserve conversationId - Prevents unnecessary new conversation creation --- apps/client/app/(main)/chat-history.tsx | 16 ++++++++++ apps/client/hooks/useActiveConversation.ts | 37 +++++++++++++++++++--- apps/client/hooks/useChatHistoryData.ts | 3 +- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/apps/client/app/(main)/chat-history.tsx b/apps/client/app/(main)/chat-history.tsx index 91695604..7e55fc2e 100644 --- a/apps/client/app/(main)/chat-history.tsx +++ b/apps/client/app/(main)/chat-history.tsx @@ -483,6 +483,22 @@ export default function ChatHistoryScreen() { ]); clearTimeout(timeoutId); + + // Manually add conversation to list immediately (real-time subscriptions are failing) + // This ensures it appears instantly even if real-time subscriptions are broken + const now = new Date().toISOString(); + const newConversation = { + id: conversationData.conversationId, + title: 'mallory-global', + token_ca: GLOBAL_TOKEN_ID, + created_at: now, + updated_at: now, + metadata: {} + }; + + console.log('✅ [handleNewChat] Manually adding conversation to list:', newConversation.id); + handleConversationInsert(newConversation); + setCurrentConversationId(conversationData.conversationId); router.push(`/(main)/chat?conversationId=${conversationData.conversationId}`); diff --git a/apps/client/hooks/useActiveConversation.ts b/apps/client/hooks/useActiveConversation.ts index 00d568e2..f20937df 100644 --- a/apps/client/hooks/useActiveConversation.ts +++ b/apps/client/hooks/useActiveConversation.ts @@ -17,7 +17,7 @@ export function useActiveConversation({ userId }: UseActiveConversationProps) { const hasUpdatedUrlRef = useRef(false); const isLoadingRef = useRef(false); // Prevent concurrent loads - const { setConversationId: setGlobalConversationId } = useActiveConversationContext(); + const { conversationId: contextConversationId, setConversationId: setGlobalConversationId } = useActiveConversationContext(); useEffect(() => { const loadActiveConversation = async () => { @@ -52,7 +52,25 @@ export function useActiveConversation({ userId }: UseActiveConversationProps) { return; } - // Priority 2: Storage (wait for it properly) + // Priority 2: Context (already loaded, fastest) + if (contextConversationId) { + console.log('✅ [useActiveConversation] Found conversationId in context:', contextConversationId); + console.log('📍 [useActiveConversation] Using context - NOT creating new conversation'); + // Update URL for web to preserve it + if (Platform.OS === 'web' && !params.conversationId && !hasUpdatedUrlRef.current) { + hasUpdatedUrlRef.current = true; + router.replace(`/(main)/chat?conversationId=${contextConversationId}`); + } + setConversationId(contextConversationId); + setGlobalConversationId(contextConversationId); + setIsLoading(false); + isLoadingRef.current = false; + return; + } else { + console.log('⚠️ [useActiveConversation] Context conversationId is null/undefined'); + } + + // Priority 3: Storage (wait for it properly) let activeConversationId: string | null = null; try { activeConversationId = await storage.persistent.getItem(SECURE_STORAGE_KEYS.CURRENT_CONVERSATION_ID); @@ -62,6 +80,7 @@ export function useActiveConversation({ userId }: UseActiveConversationProps) { if (activeConversationId) { console.log('✅ [useActiveConversation] Found conversationId in storage:', activeConversationId); + console.log('📍 [useActiveConversation] Using storage - NOT creating new conversation'); // Update URL for web to preserve it if (Platform.OS === 'web' && !params.conversationId && !hasUpdatedUrlRef.current) { hasUpdatedUrlRef.current = true; @@ -72,9 +91,11 @@ export function useActiveConversation({ userId }: UseActiveConversationProps) { setIsLoading(false); isLoadingRef.current = false; return; + } else { + console.log('⚠️ [useActiveConversation] Storage conversationId is null/undefined'); } - // Priority 3: Only create new if we truly don't have one + // Priority 4: Only create new if we truly don't have one // Add a small delay to allow any pending storage writes to complete console.log('⚠️ [useActiveConversation] No conversationId found, waiting before creating new...'); await new Promise(resolve => setTimeout(resolve, 100)); @@ -95,8 +116,16 @@ export function useActiveConversation({ userId }: UseActiveConversationProps) { } // Only now create new conversation if we really don't have one - console.log('🆕 [useActiveConversation] Creating new conversation...'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('🆕 [useActiveConversation] ⚠️ CREATING NEW CONVERSATION ⚠️'); + console.log(' Reason: No conversationId found in URL, context, or storage'); + console.log(' URL param:', params.conversationId || 'none'); + console.log(' Context:', contextConversationId || 'none'); + console.log(' Storage:', activeConversationId || 'none'); + console.log(' Retry storage:', retryConversationId || 'none'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); const conversationData = await getCurrentOrCreateConversation(userId); + console.log('✅ [useActiveConversation] New conversation created:', conversationData.conversationId); setConversationId(conversationData.conversationId); setGlobalConversationId(conversationData.conversationId); setIsLoading(false); diff --git a/apps/client/hooks/useChatHistoryData.ts b/apps/client/hooks/useChatHistoryData.ts index 2f328aab..5cd8109c 100644 --- a/apps/client/hooks/useChatHistoryData.ts +++ b/apps/client/hooks/useChatHistoryData.ts @@ -196,7 +196,8 @@ export function useChatHistoryData(userId?: string) { setConversations(prev => { console.log('📝 [HANDLE INSERT] Previous conversations count:', prev.length); - const updated = [newConversation, ...prev]; + const updated = [newConversation, ...prev] + .sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()); // Sort after adding console.log('📝 [HANDLE INSERT] Updated conversations count:', updated.length); cache.conversations = updated; // Update cache console.log('📝 [HANDLE INSERT] Cache updated'); From 426304d60df4f8a7ead65e7c93761ae9c5ef887b Mon Sep 17 00:00:00 2001 From: santino1919 Date: Wed, 21 Jan 2026 13:55:16 +0100 Subject: [PATCH 7/9] chore: add changeset for chat state persistence fixes --- .changeset/fix-chat-state-persistence.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/fix-chat-state-persistence.md diff --git a/.changeset/fix-chat-state-persistence.md b/.changeset/fix-chat-state-persistence.md new file mode 100644 index 00000000..8d08312c --- /dev/null +++ b/.changeset/fix-chat-state-persistence.md @@ -0,0 +1,6 @@ +--- +"@darkresearch/mallory-client": patch +--- + +Fix chat state persistence and prevent duplicate conversation creation on tab switch. Prevents infinite chat creation when navigating between modals on mobile web. + From 07e70ff491ddfb987cae0643b0bc4cdfa8059b44 Mon Sep 17 00:00:00 2001 From: santino1919 Date: Wed, 21 Jan 2026 14:42:04 +0100 Subject: [PATCH 8/9] fix: add missing dependency and correct timeout message check - Add contextConversationId to useEffect dependency array in useActiveConversation - Fix timeout error message check from 8 seconds to 15 seconds in conversations service --- apps/client/features/chat/services/conversations.ts | 2 +- apps/client/hooks/useActiveConversation.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/client/features/chat/services/conversations.ts b/apps/client/features/chat/services/conversations.ts index 0717aa16..dd41c83f 100644 --- a/apps/client/features/chat/services/conversations.ts +++ b/apps/client/features/chat/services/conversations.ts @@ -166,7 +166,7 @@ async function createConversationDirectly(conversationId: string, userId?: strin result = await Promise.race([insertWithLogging, insertTimeout]); } catch (raceError) { // If it's our timeout error, try to get the actual error from the original promise - if (raceError instanceof Error && raceError.message === 'INSERT timeout after 8 seconds') { + if (raceError instanceof Error && raceError.message === 'INSERT timeout after 15 seconds') { console.error('[createConversation] Timeout occurred. Checking if original promise has error...'); // Wait a bit more to see if we get an actual error try { diff --git a/apps/client/hooks/useActiveConversation.ts b/apps/client/hooks/useActiveConversation.ts index f20937df..de5684fb 100644 --- a/apps/client/hooks/useActiveConversation.ts +++ b/apps/client/hooks/useActiveConversation.ts @@ -141,7 +141,7 @@ export function useActiveConversation({ userId }: UseActiveConversationProps) { }; loadActiveConversation(); - }, [userId, params.conversationId]); + }, [userId, params.conversationId, contextConversationId]); return { conversationId, From ed38e9b38da11b0d61de48acac6609a8a2090b66 Mon Sep 17 00:00:00 2001 From: santino1919 Date: Wed, 21 Jan 2026 16:57:34 +0100 Subject: [PATCH 9/9] fix: handle promise rejection in TabBar navigation - Add .catch() handler to handlePress call to prevent unhandled promise rejections - Ensures navigation errors are properly caught and logged --- apps/client/components/navigation/TabBar.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/client/components/navigation/TabBar.tsx b/apps/client/components/navigation/TabBar.tsx index 5119ee1a..617aca27 100644 --- a/apps/client/components/navigation/TabBar.tsx +++ b/apps/client/components/navigation/TabBar.tsx @@ -54,7 +54,9 @@ export function TabBar({ state, descriptors, navigation }: TabBarProps) { handlePress(tab.route)} + onPress={() => handlePress(tab.route).catch((error) => { + console.error('Navigation error:', error); + })} >