From 5aebd3080864b43d014953c2668a11527ff329ce Mon Sep 17 00:00:00 2001 From: Zoe Date: Mon, 27 Jul 2026 17:37:40 -0500 Subject: [PATCH] fix: copy tool calls, generations, and timestamps when forking topics Forked topics dropped toolCall/generation relations and rewrote part lastUpdatedAt, which broke tool rendering, token stats, and reasoning durations. --- server/api/topic/[topicId]/fork.post.ts | 75 +++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/server/api/topic/[topicId]/fork.post.ts b/server/api/topic/[topicId]/fork.post.ts index b407d51..e03c103 100644 --- a/server/api/topic/[topicId]/fork.post.ts +++ b/server/api/topic/[topicId]/fork.post.ts @@ -1,5 +1,5 @@ import * as z from 'zod'; -import { messages, messageParts, attachments, topics } from '~~/drizzle/schema'; +import { messages, messageParts, attachments, topics, generations, toolCalls } from '~~/drizzle/schema'; import { db } from '~~/server/lib/db'; import { nanoid } from 'nanoid'; import { eq, and, inArray } from 'drizzle-orm'; @@ -132,6 +132,38 @@ export default defineEventHandler(async (event) => { }); const messageIdMap = new Map(); + const generationIdMap = new Map(); + const toolCallIdMap = new Map(); + + const originalMessageIds = messagesToCopy.map(m => m.id); + const originalGenerationIds = [ + ...new Set( + messagesToCopy + .map(m => m.generationId) + .filter((id): id is string => id !== null), + ), + ]; + + // Copy generations first so messages can reference them + if (originalGenerationIds.length > 0) { + const generationsToCopy = await db.select().from(generations) + .where(inArray(generations.id, originalGenerationIds)); + + for (const generation of generationsToCopy) { + const newGenerationId = nanoid(); + generationIdMap.set(generation.id, newGenerationId); + + await db.insert(generations).values({ + id: newGenerationId, + userId, + topicId: newTopicId, + modelId: generation.modelId, + status: generation.status, + tokens: generation.tokens, + error: generation.error, + }); + } + } for (const msg of messagesToCopy) { const newMessageId = nanoid(); @@ -142,13 +174,13 @@ export default defineEventHandler(async (event) => { userId, topicId: newTopicId, parentMessageId: msg.parentMessageId ? (messageIdMap.get(msg.parentMessageId) ?? null) : null, - generationId: null, + generationId: msg.generationId ? (generationIdMap.get(msg.generationId) ?? null) : null, role: msg.role, content: msg.content, activeChildId: null, deleted: msg.deleted, createdAt: msg.createdAt, - updatedAt: new Date(), + updatedAt: msg.updatedAt, }); } @@ -164,11 +196,40 @@ export default defineEventHandler(async (event) => { } } - const originalMessageIds = Array.from(messageIdMap.keys()); if (originalMessageIds.length > 0) { const parts = await db.select().from(messageParts) .where(inArray(messageParts.messageId, originalMessageIds)); + // Copy referenced tool calls before parts so FKs resolve + const originalToolCallIds = [ + ...new Set( + parts + .map(p => p.toolCallId) + .filter((id): id is string => id !== null), + ), + ]; + + if (originalToolCallIds.length > 0) { + const toolCallsToCopy = await db.select().from(toolCalls) + .where(inArray(toolCalls.id, originalToolCallIds)); + + for (const toolCall of toolCallsToCopy) { + const newToolCallId = nanoid(); + toolCallIdMap.set(toolCall.id, newToolCallId); + + await db.insert(toolCalls).values({ + id: newToolCallId, + userId, + toolName: toolCall.toolName, + status: toolCall.status, + input: toolCall.input, + output: toolCall.output, + error: toolCall.error, + createdAt: toolCall.createdAt, + }); + } + } + for (const part of parts) { const newMessageId = messageIdMap.get(part.messageId); if (!newMessageId) continue; @@ -177,12 +238,14 @@ export default defineEventHandler(async (event) => { userId, topicId: newTopicId, messageId: newMessageId, + toolCallId: part.toolCallId ? (toolCallIdMap.get(part.toolCallId) ?? null) : null, type: part.type, content: part.content, providerOptions: part.providerOptions, finished: part.finished, + // Preserve original timestamps so reasoning duration stays accurate createdAt: part.createdAt, - lastUpdatedAt: new Date(), + lastUpdatedAt: part.lastUpdatedAt, }); } @@ -198,7 +261,7 @@ export default defineEventHandler(async (event) => { topicId: newTopicId, messageId: newMessageId, fileId: attachment.fileId, - createdAt: new Date(), + createdAt: attachment.createdAt, }); } }