From 354950bb8af42e0c9448450288423af0f8829a6f Mon Sep 17 00:00:00 2001 From: Lukas May Date: Wed, 4 Mar 2026 11:25:43 +0100 Subject: [PATCH] fix: Retry sends message with retry flag to avoid duplicate storage Added retry:true flag to sendChatMessage input. Server skips storing the user message when retry is set. Frontend uses a dedicated retryLastMessage function that skips the optimistic message add. --- apps/server/trpc/routers/chat-session.ts | 25 +++++++++++-------- .../web/src/components/chat/ChatSlideOver.tsx | 8 ++---- apps/web/src/hooks/useChatSession.ts | 17 +++++++++++++ 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/apps/server/trpc/routers/chat-session.ts b/apps/server/trpc/routers/chat-session.ts index 2d196e3..2defcdb 100644 --- a/apps/server/trpc/routers/chat-session.ts +++ b/apps/server/trpc/routers/chat-session.ts @@ -26,6 +26,7 @@ export function chatSessionProcedures(publicProcedure: ProcedureBuilder) { targetId: z.string().min(1), initiativeId: z.string().min(1), message: z.string().min(1), + retry: z.boolean().optional(), provider: z.string().optional(), })) .mutation(async ({ ctx, input }) => { @@ -49,18 +50,20 @@ export function chatSessionProcedures(publicProcedure: ProcedureBuilder) { }); } - // Store user message - await chatRepo.createMessage({ - chatSessionId: session.id, - role: 'user', - content: input.message, - }); + // Store user message (skip on retry — message already exists) + if (!input.retry) { + await chatRepo.createMessage({ + chatSessionId: session.id, + role: 'user', + content: input.message, + }); - ctx.eventBus.emit({ - type: 'chat:message_created' as const, - timestamp: new Date(), - payload: { chatSessionId: session.id, role: 'user' as const }, - }); + ctx.eventBus.emit({ + type: 'chat:message_created' as const, + timestamp: new Date(), + payload: { chatSessionId: session.id, role: 'user' as const }, + }); + } // Check if agent exists and is waiting for input if (session.agentId) { diff --git a/apps/web/src/components/chat/ChatSlideOver.tsx b/apps/web/src/components/chat/ChatSlideOver.tsx index 7d8bae4..5c514b2 100644 --- a/apps/web/src/components/chat/ChatSlideOver.tsx +++ b/apps/web/src/components/chat/ChatSlideOver.tsx @@ -42,7 +42,7 @@ function ChatSlideOverInner({ initiativeId: string; onClose: () => void; }) { - const { messages, agentStatus, agentError, sendMessage, closeSession, isSending } = + const { messages, agentStatus, agentError, sendMessage, retryLastMessage, closeSession, isSending } = useChatSession(target.type, target.id, initiativeId); const scrollRef = useRef(null); @@ -145,11 +145,7 @@ function ChatSlideOverInner({ variant="outline" size="sm" className="h-6 gap-1.5 text-xs" - onClick={() => { - // Re-send the last user message to retry - const lastUserMsg = [...messages].reverse().find(m => m.role === 'user'); - if (lastUserMsg) sendMessage(lastUserMsg.content); - }} + onClick={retryLastMessage} > Retry diff --git a/apps/web/src/hooks/useChatSession.ts b/apps/web/src/hooks/useChatSession.ts index 2ac18c7..a2d343a 100644 --- a/apps/web/src/hooks/useChatSession.ts +++ b/apps/web/src/hooks/useChatSession.ts @@ -30,6 +30,7 @@ export interface UseChatSessionResult { agentStatus: ChatAgentStatus; agentError: string | null; sendMessage: (message: string) => void; + retryLastMessage: () => void; closeSession: () => void; isSending: boolean; isLoading: boolean; @@ -163,6 +164,21 @@ export function useChatSession( [targetType, targetId, initiativeId], ); + const messagesRef = useRef(messages); + messagesRef.current = messages; + + const retryLastMessage = useCallback(() => { + const lastUserMsg = [...messagesRef.current].reverse().find(m => m.role === 'user'); + if (!lastUserMsg) return; + sendMutateRef.current({ + targetType, + targetId, + initiativeId, + message: lastUserMsg.content, + retry: true, + }); + }, [targetType, targetId, initiativeId]); + const closeSession = useCallback(() => { const s = sessionRef.current; if (s) { @@ -179,6 +195,7 @@ export function useChatSession( agentStatus, agentError, sendMessage, + retryLastMessage, closeSession, isSending, isLoading,