diff --git a/src/db/repositories.js b/src/db/repositories.js index 6e9559e..7fb0578 100644 --- a/src/db/repositories.js +++ b/src/db/repositories.js @@ -709,16 +709,10 @@ export function createRepositories(db, config) { chat.scenario_prompt ?? null, chat.scenario_summary ?? null ).lastInsertRowid; - if (chat.context_summary) { - statements.updateChatContextSummary.run( - chat.context_summary, - Number(chat.context_summary_message_id || 0), - chatId, - username - ); - } + const importedMessageIds = new Map(); for (const message of chat.messages || []) { - statements.insertChatMessage.run( + const mappedRetryPromptId = importedMessageIds.get(message.retryPromptMessageId) ?? null; + const result = statements.insertChatMessage.run( chatId, message.role, message.content, @@ -726,7 +720,17 @@ export function createRepositories(db, config) { JSON.stringify(message.retryVariants || [message.content || ""]), Number(message.retryActiveIndex || 0), Number(message.retryRetriesUsed || 0), - message.retryPromptMessageId ?? null + mappedRetryPromptId + ); + if (message.sourceId != null) importedMessageIds.set(message.sourceId, Number(result.lastInsertRowid)); + } + if (chat.context_summary) { + const mappedSummaryMessageId = importedMessageIds.get(Number(chat.context_summary_message_id || 0)) || 0; + statements.updateChatContextSummary.run( + chat.context_summary, + mappedSummaryMessageId, + chatId, + username ); } for (const memory of chat.memories || []) { diff --git a/src/routes/chats.js b/src/routes/chats.js index ccd91b2..f6221a2 100644 --- a/src/routes/chats.js +++ b/src/routes/chats.js @@ -154,9 +154,13 @@ export function createChatsRouter({repositories, chatService, modelService, conf if (existingMessage.role === "bot") { const retryVariants = [...existingMessage.retryVariants]; - const activeIndex = Number.isInteger(Number(req.validatedBody.retryActiveIndex)) + const requestedIndex = Number(req.validatedBody.retryActiveIndex); + const activeIndex = Number.isInteger(requestedIndex) ? Number(req.validatedBody.retryActiveIndex) : existingMessage.retryActiveIndex; + if (activeIndex < 0 || activeIndex >= retryVariants.length) { + return res.status(400).json({error: "retryActiveIndex is out of range"}); + } retryVariants[activeIndex] = req.validatedBody.content; chatService.persistChatMessageRetryState({ chatId, @@ -186,9 +190,13 @@ export function createChatsRouter({repositories, chatService, modelService, conf if (existingMessage.role === "bot") { const retryVariants = [...existingMessage.retryVariants]; - const activeIndex = Number.isInteger(Number(req.validatedBody.retryActiveIndex)) + const requestedIndex = Number(req.validatedBody.retryActiveIndex); + const activeIndex = Number.isInteger(requestedIndex) ? Number(req.validatedBody.retryActiveIndex) : existingMessage.retryActiveIndex; + if (activeIndex < 0 || activeIndex >= retryVariants.length) { + return res.status(400).json({error: "retryActiveIndex is out of range"}); + } retryVariants[activeIndex] = req.validatedBody.content; chatService.persistChatMessageRetryState({ chatId, @@ -338,6 +346,8 @@ export function createChatsRouter({repositories, chatService, modelService, conf router.post("/chats/:id/messages/:messageId/retry", ...chatRateLimiters, async (req, res) => { const chatId = Number(req.params.id); const messageId = Number(req.params.messageId); + const chat = repositories.getChat(chatId, req.session.user); + if (!chat) return res.status(404).json({error: "Chat not found"}); const targetMessage = repositories.getChatMessage(chatId, messageId); if (!targetMessage) return res.status(404).json({error: "Message not found"}); const latestMessage = repositories.getLatestChatMessage(chatId); @@ -363,6 +373,8 @@ export function createChatsRouter({repositories, chatService, modelService, conf router.post("/chats/:id/messages/by-index/:index/retry", ...chatRateLimiters, async (req, res) => { const chatId = Number(req.params.id); + const chat = repositories.getChat(chatId, req.session.user); + if (!chat) return res.status(404).json({error: "Chat not found"}); const targetMessage = chatService.getChatMessageByIndex(chatId, req.params.index); if (!targetMessage) return res.status(404).json({error: "Message not found"}); const latestMessage = repositories.getLatestChatMessage(chatId); diff --git a/src/services/chatService.js b/src/services/chatService.js index f2b5fb7..12f85a1 100644 --- a/src/services/chatService.js +++ b/src/services/chatService.js @@ -271,8 +271,6 @@ export function createChatService(repositories, modelService, config) { const payload = this.buildMessagesPayload({user, chatId, message}); - insertChatMessage(chatId, "user", message); - repositories.touchChat(chatId, user); const activePersona = payload.activePersona; const activeUserPersona = payload.activeUserPersona; const messagesPayload = payload.messagesPayload; @@ -283,6 +281,7 @@ export function createChatService(repositories, modelService, config) { if (!fullReply) return {error: "Failed to generate a valid character reply", status: 500}; + insertChatMessage(chatId, "user", message); insertChatMessage(chatId, "bot", fullReply, { retryVariants: [fullReply], retryActiveIndex: 0, @@ -299,20 +298,19 @@ export function createChatService(repositories, modelService, config) { const payload = this.buildMessagesPayload({user, chatId, message}); - insertChatMessage(chatId, "user", message); - repositories.touchChat(chatId, user); const activePersona = payload.activePersona; const activeUserPersona = payload.activeUserPersona; const messagesPayload = payload.messagesPayload; + // Persona replies must be validated before any text is exposed to the client. + // Normal chats can retain true incremental streaming. const fullReply = activePersona - ? await modelService.streamReply(model, messagesPayload, onChunk) + ? await generateValidatedRoleplayReply({selectedModel: model, messagesPayload, userPersona: activeUserPersona}) : await modelService.streamReply(model, messagesPayload, onChunk); if (!fullReply) return {error: "Failed to generate a valid character reply", status: 500}; - if (activePersona && containsUserVoiceInRoleplayOpener(fullReply, activeUserPersona)) { - return {error: "Failed to generate a valid character reply", status: 500}; - } + if (activePersona && onChunk) onChunk(fullReply, fullReply); + insertChatMessage(chatId, "user", message); insertChatMessage(chatId, "bot", fullReply, { retryVariants: [fullReply], retryActiveIndex: 0, @@ -327,10 +325,14 @@ export function createChatService(repositories, modelService, config) { const session = repositories.getChat(chatId, user); if (!session) return {error: "Chat not found", status: 404}; + const existingMessage = formatChatMessage(targetMessage); + if (existingMessage.retryRetriesUsed >= 5) { + return {error: "Maximum of 5 retries reached", status: 400}; + } + const retryResult = await buildRetryPayload({chatId, user, session, selectedModel, targetMessage, retryStyle}); if (retryResult.error) return {error: retryResult.error, status: 400}; - const existingMessage = formatChatMessage(targetMessage); const retryVariants = [...existingMessage.retryVariants, retryResult.fullReply]; persistChatMessageRetryState({ chatId, diff --git a/src/services/modelService.js b/src/services/modelService.js index e550670..7e2e595 100644 --- a/src/services/modelService.js +++ b/src/services/modelService.js @@ -1,4 +1,4 @@ -import {exec} from "child_process"; +import {execFile} from "child_process"; class ModelServiceError extends Error { constructor(code, message, status = 500) { @@ -29,7 +29,7 @@ export function createModelService(config, overrides = {}) { if (state.timer) clearTimeout(state.timer); state.timer = setTimeout(() => { if (state.activeRequests > 0) return; - exec(`ollama stop ${model}`, () => { + execFile("ollama", ["stop", model], () => { modelState.delete(model); }); }, config.modelUnloadAfterMs); diff --git a/src/services/workspaceImportService.js b/src/services/workspaceImportService.js index 11714e4..c1fa101 100644 --- a/src/services/workspaceImportService.js +++ b/src/services/workspaceImportService.js @@ -79,13 +79,20 @@ function validateMessage(message, chatIndex, messageIndex, errors) { ? message.retry_variants.map((item) => String(item ?? "")).filter(Boolean) : [content].filter(Boolean); return { + sourceId: Number.isInteger(Number(message.id)) ? Number(message.id) : null, role: role === "assistant" ? "bot" : role, content, model_name: optionalText(message.model_name || message.modelName), retryVariants, - retryActiveIndex: Number.isInteger(Number(message.retryActiveIndex)) ? Number(message.retryActiveIndex) : 0, - retryRetriesUsed: Number.isInteger(Number(message.retryRetriesUsed)) ? Number(message.retryRetriesUsed) : 0, - retryPromptMessageId: Number.isInteger(Number(message.retryPromptMessageId)) ? Number(message.retryPromptMessageId) : null + retryActiveIndex: Number.isInteger(Number(message.retryActiveIndex ?? message.retry_active_index)) + ? Number(message.retryActiveIndex ?? message.retry_active_index) + : 0, + retryRetriesUsed: Number.isInteger(Number(message.retryRetriesUsed ?? message.retry_retries_used)) + ? Number(message.retryRetriesUsed ?? message.retry_retries_used) + : 0, + retryPromptMessageId: Number.isInteger(Number(message.retryPromptMessageId ?? message.retry_prompt_message_id)) + ? Number(message.retryPromptMessageId ?? message.retry_prompt_message_id) + : null }; }