From b896515b2cf25cdbee0c40deedfbb6f4dc44171c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 7 Mar 2026 07:46:36 +0000 Subject: [PATCH] fix message duplication on auto-save after resume The PUT endpoint was re-merging old DB messages (which already included previous merges) with disk messages on every turn, causing duplication. Now the client passes resumedMessageCount so the server always slices only the original pre-resume messages before concatenating. Co-Authored-By: Claude Opus 4.6 --- .../api/saved-sessions/saved-sessions.ts | 23 ++++++++++--------- .../src/apps/Chat/ChatPanelWrapper.tsx | 4 +++- .../src/apps/ChatHistory/ChatDetailPanel.tsx | 4 +++- src/workspaces/state/src/useSavedSessions.ts | 4 ++-- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/servers/api/saved-sessions/saved-sessions.ts b/src/servers/api/saved-sessions/saved-sessions.ts index e5c20048..78d0a339 100644 --- a/src/servers/api/saved-sessions/saved-sessions.ts +++ b/src/servers/api/saved-sessions/saved-sessions.ts @@ -81,7 +81,7 @@ savedSessionsRouter.get('/:id', async (ctx) => { savedSessionsRouter.put('/:id', async (ctx) => { const user = ctx.get('user'); const id = Number(ctx.req.param('id')); - const { sessionId } = ctx.get('body') as { sessionId: string }; + const { sessionId, resumedMessageCount } = ctx.get('body') as { sessionId: string; resumedMessageCount?: number }; if (Number.isNaN(id)) { return ctx.json({ error: 'Invalid id' }, 400); @@ -93,12 +93,6 @@ savedSessionsRouter.put('/:id', async (ctx) => { const homeDir = getHomeDirForRole(user.email, user.role); - // Load existing saved session to get previous messages - const existing = await getSavedSession(id, user.id); - if (!existing) { - return ctx.json({ error: 'Not found' }, 404); - } - let meta; let newMessages: Message[]; try { @@ -109,14 +103,21 @@ savedSessionsRouter.put('/:id', async (ctx) => { return ctx.json({ error: 'Session not found on disk' }, 404); } - // Merge: old DB messages (from before resume) + new disk messages (from this session) - const oldMessages = (existing.rawMessages ?? []) as Message[]; - const mergedMessages = [...oldMessages, ...newMessages]; + // If resuming, merge original pre-resume messages with new disk messages + let finalMessages = newMessages; + if (resumedMessageCount && resumedMessageCount > 0) { + const existing = await getSavedSession(id, user.id); + if (!existing) { + return ctx.json({ error: 'Not found' }, 404); + } + const oldMessages = ((existing.rawMessages ?? []) as Message[]).slice(0, resumedMessageCount); + finalMessages = [...oldMessages, ...newMessages]; + } const updated = await updateSavedSessionMessages( id, user.id, - mergedMessages, + finalMessages, meta.cost as unknown as Record, ); diff --git a/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx b/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx index 229f023f..742c5013 100644 --- a/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx @@ -41,6 +41,7 @@ const ChatPanelInner = ({ const savedIdRef = useRef(savedId); savedIdRef.current = savedId; const chatSessionRef = useRef(null); + const resumedMessageCountRef = useRef(initialMessages?.length ?? 0); const handleTurnComplete = useCallback( (hadToolCalls: boolean) => { @@ -48,7 +49,8 @@ const ChatPanelInner = ({ const id = savedIdRef.current; const sid = chatSessionRef.current; if (id != null && sid) { - updateSessionMessages(id, sid).catch(() => {}); + const count = resumedMessageCountRef.current; + updateSessionMessages(id, sid, count > 0 ? count : undefined).catch(() => {}); } }, [onTurnComplete, updateSessionMessages], diff --git a/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx b/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx index f1eb742f..3a2103d7 100644 --- a/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx +++ b/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx @@ -95,12 +95,14 @@ function NewChat({ resumeSummary, initialMessages, savedId: initialSavedId }: Ne const savedIdRef = useRef(savedId); savedIdRef.current = savedId; const chatSessionRef = useRef(null); + const resumedMessageCountRef = useRef(initialMessages?.length ?? 0); const onTurnComplete = useCallback(() => { const id = savedIdRef.current; const sid = chatSessionRef.current; if (id != null && sid) { - updateSessionMessages(id, sid).catch(() => {}); + const count = resumedMessageCountRef.current; + updateSessionMessages(id, sid, count > 0 ? count : undefined).catch(() => {}); } }, [updateSessionMessages]); diff --git a/src/workspaces/state/src/useSavedSessions.ts b/src/workspaces/state/src/useSavedSessions.ts index 6ab1ad01..e8c11286 100644 --- a/src/workspaces/state/src/useSavedSessions.ts +++ b/src/workspaces/state/src/useSavedSessions.ts @@ -88,8 +88,8 @@ export function useSavedSessions() { return client.post(`/saved-sessions/${id}/resume`); } - async function updateSessionMessages(savedId: number, sessionId: string) { - await client.put(`/saved-sessions/${savedId}`, { sessionId }); + async function updateSessionMessages(savedId: number, sessionId: string, resumedMessageCount?: number) { + await client.put(`/saved-sessions/${savedId}`, { sessionId, resumedMessageCount }); } return {