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:
@@ -81,7 +81,7 @@ savedSessionsRouter.get('/:id', async (ctx) => {
|
|||||||
savedSessionsRouter.put('/:id', async (ctx) => {
|
savedSessionsRouter.put('/:id', async (ctx) => {
|
||||||
const user = ctx.get('user');
|
const user = ctx.get('user');
|
||||||
const id = Number(ctx.req.param('id'));
|
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)) {
|
if (Number.isNaN(id)) {
|
||||||
return ctx.json({ error: 'Invalid id' }, 400);
|
return ctx.json({ error: 'Invalid id' }, 400);
|
||||||
@@ -93,12 +93,6 @@ savedSessionsRouter.put('/:id', async (ctx) => {
|
|||||||
|
|
||||||
const homeDir = getHomeDirForRole(user.email, user.role);
|
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 meta;
|
||||||
let newMessages: Message[];
|
let newMessages: Message[];
|
||||||
try {
|
try {
|
||||||
@@ -109,14 +103,21 @@ savedSessionsRouter.put('/:id', async (ctx) => {
|
|||||||
return ctx.json({ error: 'Session not found on disk' }, 404);
|
return ctx.json({ error: 'Session not found on disk' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge: old DB messages (from before resume) + new disk messages (from this session)
|
// If resuming, merge original pre-resume messages with new disk messages
|
||||||
const oldMessages = (existing.rawMessages ?? []) as Message[];
|
let finalMessages = newMessages;
|
||||||
const mergedMessages = [...oldMessages, ...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(
|
const updated = await updateSavedSessionMessages(
|
||||||
id,
|
id,
|
||||||
user.id,
|
user.id,
|
||||||
mergedMessages,
|
finalMessages,
|
||||||
meta.cost as unknown as Record<string, unknown>,
|
meta.cost as unknown as Record<string, unknown>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const ChatPanelInner = ({
|
|||||||
const savedIdRef = useRef(savedId);
|
const savedIdRef = useRef(savedId);
|
||||||
savedIdRef.current = savedId;
|
savedIdRef.current = savedId;
|
||||||
const chatSessionRef = useRef<string | null>(null);
|
const chatSessionRef = useRef<string | null>(null);
|
||||||
|
const resumedMessageCountRef = useRef(initialMessages?.length ?? 0);
|
||||||
|
|
||||||
const handleTurnComplete = useCallback(
|
const handleTurnComplete = useCallback(
|
||||||
(hadToolCalls: boolean) => {
|
(hadToolCalls: boolean) => {
|
||||||
@@ -48,7 +49,8 @@ const ChatPanelInner = ({
|
|||||||
const id = savedIdRef.current;
|
const id = savedIdRef.current;
|
||||||
const sid = chatSessionRef.current;
|
const sid = chatSessionRef.current;
|
||||||
if (id != null && sid) {
|
if (id != null && sid) {
|
||||||
updateSessionMessages(id, sid).catch(() => {});
|
const count = resumedMessageCountRef.current;
|
||||||
|
updateSessionMessages(id, sid, count > 0 ? count : undefined).catch(() => {});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onTurnComplete, updateSessionMessages],
|
[onTurnComplete, updateSessionMessages],
|
||||||
|
|||||||
@@ -95,12 +95,14 @@ function NewChat({ resumeSummary, initialMessages, savedId: initialSavedId }: Ne
|
|||||||
const savedIdRef = useRef(savedId);
|
const savedIdRef = useRef(savedId);
|
||||||
savedIdRef.current = savedId;
|
savedIdRef.current = savedId;
|
||||||
const chatSessionRef = useRef<string | null>(null);
|
const chatSessionRef = useRef<string | null>(null);
|
||||||
|
const resumedMessageCountRef = useRef(initialMessages?.length ?? 0);
|
||||||
|
|
||||||
const onTurnComplete = useCallback(() => {
|
const onTurnComplete = useCallback(() => {
|
||||||
const id = savedIdRef.current;
|
const id = savedIdRef.current;
|
||||||
const sid = chatSessionRef.current;
|
const sid = chatSessionRef.current;
|
||||||
if (id != null && sid) {
|
if (id != null && sid) {
|
||||||
updateSessionMessages(id, sid).catch(() => {});
|
const count = resumedMessageCountRef.current;
|
||||||
|
updateSessionMessages(id, sid, count > 0 ? count : undefined).catch(() => {});
|
||||||
}
|
}
|
||||||
}, [updateSessionMessages]);
|
}, [updateSessionMessages]);
|
||||||
|
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ export function useSavedSessions() {
|
|||||||
return client.post<ResumeResult>(`/saved-sessions/${id}/resume`);
|
return client.post<ResumeResult>(`/saved-sessions/${id}/resume`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateSessionMessages(savedId: number, sessionId: string) {
|
async function updateSessionMessages(savedId: number, sessionId: string, resumedMessageCount?: number) {
|
||||||
await client.put(`/saved-sessions/${savedId}`, { sessionId });
|
await client.put(`/saved-sessions/${savedId}`, { sessionId, resumedMessageCount });
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user