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.
This commit is contained in:
Zoe
2026-07-27 17:37:40 -05:00
parent 166052012d
commit 5aebd30808
+69 -6
View File
@@ -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<string, string>();
const generationIdMap = new Map<string, string>();
const toolCallIdMap = new Map<string, string>();
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,
});
}
}