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 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 08:23:31 +00:00
co-authored by Claude Opus 4.6
parent 70442ae277
commit b896515b2c
4 changed files with 20 additions and 15 deletions
@@ -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<string, unknown>,
);
@@ -41,6 +41,7 @@ const ChatPanelInner = ({
const savedIdRef = useRef(savedId);
savedIdRef.current = savedId;
const chatSessionRef = useRef<string | null>(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],
@@ -95,12 +95,14 @@ function NewChat({ resumeSummary, initialMessages, savedId: initialSavedId }: Ne
const savedIdRef = useRef(savedId);
savedIdRef.current = savedId;
const chatSessionRef = useRef<string | null>(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]);
+2 -2
View File
@@ -88,8 +88,8 @@ export function useSavedSessions() {
return client.post<ResumeResult>(`/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 {