Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
streamText,
smoothStream,
consumeStream,
convertToModelMessages,
pruneMessages,
safeValidateUIMessages,
Expand Down Expand Up @@ -38,7 +39,11 @@ import {
} from "@/lib/ai/gateway-provider-options";
import { db } from "@/lib/db/client";
import { chatThreads, chatMessages } from "@/lib/db/schema";
import { CHAT_MESSAGE_FORMAT, type ChatMessage } from "@/lib/chat/types";
import {
CHAT_MESSAGE_FORMAT,
hasMeaningfulContent,
type ChatMessage,
} from "@/lib/chat/types";
import {
CHAT_DEBUG_TAG,
summarizeMessage,
Expand Down Expand Up @@ -113,7 +118,8 @@ async function loadThreadHistory(threadId: string): Promise<ChatMessage[]> {
),
)
.orderBy(asc(chatMessages.createdAt));
return rows.map((r) => r.content) as ChatMessage[];
const messages = rows.map((r) => r.content) as ChatMessage[];
return messages.filter((m) => hasMeaningfulContent(m));
}

async function getThreadById(threadId: string) {
Expand Down Expand Up @@ -344,6 +350,7 @@ async function handlePOST(req: Request) {
try {
convertedMessages = await convertToModelMessages(validatedMessages, {
tools,
ignoreIncompleteToolCalls: true,
});
} catch (convertError) {
logger.error("❌ [CHAT-API] convertToModelMessages FAILED:", {
Expand Down Expand Up @@ -473,6 +480,7 @@ async function handlePOST(req: Request) {
tools,
providerOptions,
headers: getGatewayAttributionHeaders(),
abortSignal: req.signal,
experimental_telemetry: {
isEnabled: true,
metadata: {
Expand Down Expand Up @@ -546,10 +554,20 @@ async function handlePOST(req: Request) {
}
},
onFinish: async ({ responseMessage, isAborted }) => {
if (isAborted || !userId) {
const meaningful = hasMeaningfulContent(responseMessage);
if (isAborted) {
logger.debug(`${CHAT_DEBUG_TAG} onFinish skipped (aborted)`, {
hasUserId: !!userId,
hasMeaningfulContent: meaningful,
partCount: responseMessage.parts?.length ?? 0,
});
return;
}
if (!userId || !meaningful) {
logger.warn(`${CHAT_DEBUG_TAG} onFinish skipped`, {
isAborted,
hasUserId: !!userId,
hasMeaningfulContent: meaningful,
partCount: responseMessage.parts?.length ?? 0,
});
return;
}
Expand Down Expand Up @@ -599,7 +617,10 @@ async function handlePOST(req: Request) {
},
});

return createUIMessageStreamResponse({ stream });
return createUIMessageStreamResponse({
stream,
consumeSseStream: consumeStream,
});
} catch (error) {
if (error instanceof Response) {
return error;
Expand Down
9 changes: 7 additions & 2 deletions src/app/api/threads/[id]/messages/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
summarizeMessage,
summarizeRoster,
} from "@/lib/chat/debug";
import { CHAT_MESSAGE_FORMAT } from "@/lib/chat/types";
import { CHAT_MESSAGE_FORMAT, hasMeaningfulContent } from "@/lib/chat/types";

async function getThreadAndVerify(id: string, userId: string) {
const [thread] = await db
Expand Down Expand Up @@ -70,7 +70,12 @@ export const GET = withServerObservability(
.orderBy(asc(chatMessages.createdAt));

// content is a stored UIMessage object; no reshaping required.
const messages = rows.map((r) => r.content);
// Drop any pre-existing rows whose stored content has no meaningful
// parts (empty or `step-start`-only) so corrupted history from before
// the persistence guard never poisons hydrated `useChat` state.
const messages = rows
.map((r) => r.content)
.filter((content) => hasMeaningfulContent(content));

// [chat-debug] Snapshot what's actually in the DB for this thread so
// we can correlate against what the client receives. If the assistant
Expand Down
16 changes: 13 additions & 3 deletions src/components/chat/ChatProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from "@/lib/chat/queries";
import { createChatTransport } from "@/lib/chat/transport";
import type { ChatMessage } from "@/lib/chat/types";
import { hasMeaningfulContent } from "@/lib/chat/types";
import { useUIStore } from "@/lib/stores/ui-store";
import {
selectCurrentThreadId,
Expand Down Expand Up @@ -302,7 +303,11 @@ export function ChatProvider({ workspaceId, children }: ChatProviderProps) {
const chat = useChat<ChatMessage>({
id: threadId,
transport,
messages: shouldHydrateHistory ? persistedMessages : undefined,
messages: shouldHydrateHistory
? (persistedMessages as ChatMessage[]).filter((m) =>
hasMeaningfulContent(m),
)
: undefined,
onError: handleError,
// Live title updates: the chat route writes a `data-chat-title` part to
// the SSE stream once `generateThreadTitle` resolves. We patch the
Expand Down Expand Up @@ -366,20 +371,25 @@ export function ChatProvider({ workspaceId, children }: ChatProviderProps) {
useEffect(() => {
if (seededRef.current) return;
if (persistedMessages.length === 0) return;
const cleanPersisted = (persistedMessages as ChatMessage[]).filter((m) =>
hasMeaningfulContent(m),
);
if (messages.length > 0) {
chatDebug("seed: skipped, useChat already has messages", {
threadId,
useChatCount: messages.length,
persistedCount: persistedMessages.length,
cleanCount: cleanPersisted.length,
});
seededRef.current = true;
return;
}
chatDebug("seed: pushing persisted messages into useChat", {
threadId,
...summarizeRoster(persistedMessages as unknown[]),
droppedEmpty: persistedMessages.length - cleanPersisted.length,
...summarizeRoster(cleanPersisted as unknown[]),
});
setMessages(persistedMessages);
setMessages(cleanPersisted);
seededRef.current = true;
}, [persistedMessages, messages.length, setMessages, threadId]);

Expand Down
Loading
Loading