hot-fix: chat conversation reaching max depth with update (#442)

This commit is contained in:
MaheshtheDev 2025-09-29 05:38:32 +00:00
parent 9f548a477d
commit a9bd64c106

View file

@ -1,43 +1,73 @@
import type { UIMessage } from "@ai-sdk/react";
import { create } from "zustand";
import { persist } from "zustand/middleware";
import type { UIMessage } from "@ai-sdk/react"
import { create } from "zustand"
import { persist } from "zustand/middleware"
/**
* Deep equality check for UIMessage arrays to prevent unnecessary state updates
*/
function areUIMessageArraysEqual(a: UIMessage[], b: UIMessage[]): boolean {
if (a === b) return true
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) {
const msgA = a[i]
const msgB = b[i]
// Both messages should exist at this index
if (!msgA || !msgB) return false
if (msgA === msgB) continue
if (msgA.id !== msgB.id || msgA.role !== msgB.role) {
return false
}
// Compare the entire message using JSON serialization as a fallback
// This handles all properties including parts, toolInvocations, etc.
if (JSON.stringify(msgA) !== JSON.stringify(msgB)) {
return false
}
}
return true
}
export interface ConversationSummary {
id: string;
title?: string;
lastUpdated: string;
id: string
title?: string
lastUpdated: string
}
interface ConversationRecord {
messages: UIMessage[];
title?: string;
lastUpdated: string;
messages: UIMessage[]
title?: string
lastUpdated: string
}
interface ProjectConversationsState {
currentChatId: string | null;
conversations: Record<string, ConversationRecord>;
currentChatId: string | null
conversations: Record<string, ConversationRecord>
}
interface ConversationsStoreState {
byProject: Record<string, ProjectConversationsState>;
setCurrentChatId: (projectId: string, chatId: string | null) => void;
byProject: Record<string, ProjectConversationsState>
setCurrentChatId: (projectId: string, chatId: string | null) => void
setConversation: (
projectId: string,
chatId: string,
messages: UIMessage[],
) => void;
deleteConversation: (projectId: string, chatId: string) => void;
) => void
deleteConversation: (projectId: string, chatId: string) => void
setConversationTitle: (
projectId: string,
chatId: string,
title: string | undefined,
) => void;
) => void
}
export const usePersistentChatStore = create<ConversationsStoreState>()(
persist(
(set, get) => ({
(set, _get) => ({
byProject: {},
setCurrentChatId(projectId, chatId) {
@ -45,29 +75,38 @@ export const usePersistentChatStore = create<ConversationsStoreState>()(
const project = state.byProject[projectId] ?? {
currentChatId: null,
conversations: {},
};
}
return {
byProject: {
...state.byProject,
[projectId]: { ...project, currentChatId: chatId },
},
};
});
}
})
},
setConversation(projectId, chatId, messages) {
const now = new Date().toISOString();
const now = new Date().toISOString()
set((state) => {
const project = state.byProject[projectId] ?? {
currentChatId: null,
conversations: {},
};
const existing = project.conversations[chatId];
}
const existing = project.conversations[chatId]
// Check if messages are actually different to prevent unnecessary updates
if (
existing &&
areUIMessageArraysEqual(existing.messages, messages)
) {
return state // No change needed
}
const shouldTouchLastUpdated = (() => {
if (!existing) return messages.length > 0;
const previousLength = existing.messages?.length ?? 0;
return messages.length > previousLength;
})();
if (!existing) return messages.length > 0
const previousLength = existing.messages?.length ?? 0
return messages.length > previousLength
})()
const record: ConversationRecord = {
messages,
@ -75,7 +114,7 @@ export const usePersistentChatStore = create<ConversationsStoreState>()(
lastUpdated: shouldTouchLastUpdated
? now
: (existing?.lastUpdated ?? now),
};
}
return {
byProject: {
...state.byProject,
@ -87,8 +126,8 @@ export const usePersistentChatStore = create<ConversationsStoreState>()(
},
},
},
};
});
}
})
},
deleteConversation(projectId, chatId) {
@ -96,28 +135,28 @@ export const usePersistentChatStore = create<ConversationsStoreState>()(
const project = state.byProject[projectId] ?? {
currentChatId: null,
conversations: {},
};
const { [chatId]: _, ...rest } = project.conversations;
}
const { [chatId]: _, ...rest } = project.conversations
const nextCurrent =
project.currentChatId === chatId ? null : project.currentChatId;
project.currentChatId === chatId ? null : project.currentChatId
return {
byProject: {
...state.byProject,
[projectId]: { currentChatId: nextCurrent, conversations: rest },
},
};
});
}
})
},
setConversationTitle(projectId, chatId, title) {
const now = new Date().toISOString();
const now = new Date().toISOString()
set((state) => {
const project = state.byProject[projectId] ?? {
currentChatId: null,
conversations: {},
};
const existing = project.conversations[chatId];
if (!existing) return { byProject: state.byProject };
}
const existing = project.conversations[chatId]
if (!existing) return { byProject: state.byProject }
return {
byProject: {
...state.byProject,
@ -129,76 +168,76 @@ export const usePersistentChatStore = create<ConversationsStoreState>()(
},
},
},
};
});
}
})
},
}),
{
name: "supermemory-chats",
},
),
);
)
// Always scoped to the current project via useProject
import { useProject } from ".";
import { useProject } from "."
export function usePersistentChat() {
const { selectedProject } = useProject();
const projectId = selectedProject;
const { selectedProject } = useProject()
const projectId = selectedProject
const projectState = usePersistentChatStore((s) => s.byProject[projectId]);
const setCurrentChatIdRaw = usePersistentChatStore((s) => s.setCurrentChatId);
const setConversationRaw = usePersistentChatStore((s) => s.setConversation);
const projectState = usePersistentChatStore((s) => s.byProject[projectId])
const setCurrentChatIdRaw = usePersistentChatStore((s) => s.setCurrentChatId)
const setConversationRaw = usePersistentChatStore((s) => s.setConversation)
const deleteConversationRaw = usePersistentChatStore(
(s) => s.deleteConversation,
);
)
const setConversationTitleRaw = usePersistentChatStore(
(s) => s.setConversationTitle,
);
)
const conversations: ConversationSummary[] = (() => {
const convs = projectState?.conversations ?? {};
const convs = projectState?.conversations ?? {}
return Object.entries(convs).map(([id, rec]) => ({
id,
title: rec.title,
lastUpdated: rec.lastUpdated,
}));
})();
}))
})()
const currentChatId = projectState?.currentChatId ?? null;
const currentChatId = projectState?.currentChatId ?? null
function setCurrentChatId(chatId: string | null): void {
setCurrentChatIdRaw(projectId, chatId);
setCurrentChatIdRaw(projectId, chatId)
}
function setConversation(chatId: string, messages: UIMessage[]): void {
setConversationRaw(projectId, chatId, messages);
setConversationRaw(projectId, chatId, messages)
}
function deleteConversation(chatId: string): void {
deleteConversationRaw(projectId, chatId);
deleteConversationRaw(projectId, chatId)
}
function setConversationTitle(
chatId: string,
title: string | undefined,
): void {
setConversationTitleRaw(projectId, chatId, title);
setConversationTitleRaw(projectId, chatId, title)
}
function getCurrentConversation(): UIMessage[] | undefined {
const convs = projectState?.conversations ?? {};
const id = currentChatId;
if (!id) return undefined;
return convs[id]?.messages;
const convs = projectState?.conversations ?? {}
const id = currentChatId
if (!id) return undefined
return convs[id]?.messages
}
function getCurrentChat(): ConversationSummary | undefined {
const id = currentChatId;
if (!id) return undefined;
const rec = projectState?.conversations?.[id];
if (!rec) return undefined;
return { id, title: rec.title, lastUpdated: rec.lastUpdated };
const id = currentChatId
if (!id) return undefined
const rec = projectState?.conversations?.[id]
if (!rec) return undefined
return { id, title: rec.title, lastUpdated: rec.lastUpdated }
}
return {
@ -210,5 +249,5 @@ export function usePersistentChat() {
setConversationTitle,
getCurrentConversation,
getCurrentChat,
};
}
}