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.
This commit is contained in:
Lukas May
2026-03-04 11:25:43 +01:00
parent 84dcb0193d
commit 354950bb8a
3 changed files with 33 additions and 17 deletions

View File

@@ -26,6 +26,7 @@ export function chatSessionProcedures(publicProcedure: ProcedureBuilder) {
targetId: z.string().min(1), targetId: z.string().min(1),
initiativeId: z.string().min(1), initiativeId: z.string().min(1),
message: z.string().min(1), message: z.string().min(1),
retry: z.boolean().optional(),
provider: z.string().optional(), provider: z.string().optional(),
})) }))
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
@@ -49,18 +50,20 @@ export function chatSessionProcedures(publicProcedure: ProcedureBuilder) {
}); });
} }
// Store user message // Store user message (skip on retry — message already exists)
await chatRepo.createMessage({ if (!input.retry) {
chatSessionId: session.id, await chatRepo.createMessage({
role: 'user', chatSessionId: session.id,
content: input.message, role: 'user',
}); content: input.message,
});
ctx.eventBus.emit({ ctx.eventBus.emit({
type: 'chat:message_created' as const, type: 'chat:message_created' as const,
timestamp: new Date(), timestamp: new Date(),
payload: { chatSessionId: session.id, role: 'user' as const }, payload: { chatSessionId: session.id, role: 'user' as const },
}); });
}
// Check if agent exists and is waiting for input // Check if agent exists and is waiting for input
if (session.agentId) { if (session.agentId) {

View File

@@ -42,7 +42,7 @@ function ChatSlideOverInner({
initiativeId: string; initiativeId: string;
onClose: () => void; onClose: () => void;
}) { }) {
const { messages, agentStatus, agentError, sendMessage, closeSession, isSending } = const { messages, agentStatus, agentError, sendMessage, retryLastMessage, closeSession, isSending } =
useChatSession(target.type, target.id, initiativeId); useChatSession(target.type, target.id, initiativeId);
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
@@ -145,11 +145,7 @@ function ChatSlideOverInner({
variant="outline" variant="outline"
size="sm" size="sm"
className="h-6 gap-1.5 text-xs" className="h-6 gap-1.5 text-xs"
onClick={() => { onClick={retryLastMessage}
// Re-send the last user message to retry
const lastUserMsg = [...messages].reverse().find(m => m.role === 'user');
if (lastUserMsg) sendMessage(lastUserMsg.content);
}}
> >
<RotateCcw className="h-3 w-3" /> <RotateCcw className="h-3 w-3" />
Retry Retry

View File

@@ -30,6 +30,7 @@ export interface UseChatSessionResult {
agentStatus: ChatAgentStatus; agentStatus: ChatAgentStatus;
agentError: string | null; agentError: string | null;
sendMessage: (message: string) => void; sendMessage: (message: string) => void;
retryLastMessage: () => void;
closeSession: () => void; closeSession: () => void;
isSending: boolean; isSending: boolean;
isLoading: boolean; isLoading: boolean;
@@ -163,6 +164,21 @@ export function useChatSession(
[targetType, targetId, initiativeId], [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 closeSession = useCallback(() => {
const s = sessionRef.current; const s = sessionRef.current;
if (s) { if (s) {
@@ -179,6 +195,7 @@ export function useChatSession(
agentStatus, agentStatus,
agentError, agentError,
sendMessage, sendMessage,
retryLastMessage,
closeSession, closeSession,
isSending, isSending,
isLoading, isLoading,