fix: filter empty-parts messages to prevent follow-up conversation crash (#402)

* fix: filter out messages with empty parts to prevent follow-up crash

When an assistant response is interrupted or errors before producing content,
a UIMessage with empty parts remains in the chat state. On the next send, the
AI SDK validates all messages and rejects the empty-parts message with
"Message must contain at least one part". This filters them out when not
streaming and adds a safety guard in formatConversationHistory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: filter empty-parts messages before persisting to storage

Addresses race condition where the save effect could persist messages
with empty parts before the cleanup effect's state update applies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Nikhil
2026-03-04 10:20:36 -08:00
committed by GitHub
parent ad4c0af4fe
commit 92c20eef73
2 changed files with 14 additions and 5 deletions

View File

@@ -304,6 +304,15 @@ export const useChatSession = () => {
}),
})
// Remove messages with empty parts (e.g. interrupted assistant responses)
// to prevent AI SDK validation errors on subsequent sends
useEffect(() => {
if (status === 'streaming') return
if (messages.some((m) => !m.parts?.length)) {
setMessages(messages.filter((m) => m.parts?.length > 0))
}
}, [messages, status, setMessages])
useNotifyActiveTab({
messages,
status,
@@ -370,15 +379,14 @@ export const useChatSession = () => {
// biome-ignore lint/correctness/useExhaustiveDependencies: only need to run when messages change
useEffect(() => {
messagesRef.current = messages
if (messages.length > 0) {
// Local storage: save on every change (including during streaming)
// Remote: only save when not streaming to avoid partial message saves
const messagesToSave = messages.filter((m) => m.parts?.length > 0)
if (messagesToSave.length > 0) {
if (isLoggedIn) {
if (status !== 'streaming') {
saveRemoteConversation(conversationIdRef.current, messages)
saveRemoteConversation(conversationIdRef.current, messagesToSave)
}
} else {
saveLocalConversation(conversationIdRef.current, messages)
saveLocalConversation(conversationIdRef.current, messagesToSave)
}
}
}, [messages, isLoggedIn, status])

View File

@@ -17,6 +17,7 @@ export function formatConversationHistory(
return recentMessages
.map((msg) => {
if (!msg.parts?.length) return null
const role: 'user' | 'assistant' =
msg.role === 'user' ? 'user' : 'assistant'
const textContent = msg.parts