add generation cancellation middleware and refactor chat/message workflows to support HTTP client disconnections
This commit is contained in:
+59
-32
@@ -115,7 +115,10 @@ export function createChatService(repositories, modelService, config, webSearchP
|
||||
else externalSignal?.addEventListener("abort", abortFromCaller, { once: true });
|
||||
activeGenerations.set(chatId, controller);
|
||||
try {
|
||||
return await operation(controller.signal);
|
||||
controller.signal.throwIfAborted();
|
||||
const result = await operation(controller.signal);
|
||||
controller.signal.throwIfAborted();
|
||||
return result;
|
||||
} finally {
|
||||
if (activeGenerations.get(chatId) === controller) activeGenerations.delete(chatId);
|
||||
externalSignal?.removeEventListener("abort", abortFromCaller);
|
||||
@@ -238,6 +241,7 @@ export function createChatService(repositories, modelService, config, webSearchP
|
||||
? modelService.streamReply(selectedModel, messages, onChunk, { onThinking, signal, generation, reasoning })
|
||||
: modelService.generateReply(selectedModel, messages, { onThinking, signal, generation, reasoning });
|
||||
let opener = await generate(baseMessages);
|
||||
signal?.throwIfAborted();
|
||||
if (!containsUserVoiceInRoleplayOpener(opener, userPersona)) {
|
||||
return opener;
|
||||
}
|
||||
@@ -658,10 +662,31 @@ export function createChatService(repositories, modelService, config, webSearchP
|
||||
generateRoleplayOpener,
|
||||
insertChatMessage,
|
||||
containsUserVoiceInRoleplayOpener,
|
||||
async initializeRoleplay({user, chatId, assistantPersona, userPersona, scenarioPrompt, sceneSummary, selectedModel, signal = null}) {
|
||||
if (chatGenerationQueues.has(chatId)) return {error: "A response is already being generated", status: 409};
|
||||
return withChatGenerationLock(chatId, () => withActiveGeneration(chatId, signal, async activeSignal => {
|
||||
let chat = repositories.getChat(chatId, user);
|
||||
if (!chat) return {error: "Chat not found", status: 404};
|
||||
// Check inside the same lock that protects generation and persistence.
|
||||
if (repositories.listChatMessages(chatId).length) return {chat, existing: true, generatedInitialMessage: false};
|
||||
repositories.updateChatScene(chatId, user, scenarioPrompt, sceneSummary);
|
||||
chat = repositories.getChat(chatId, user);
|
||||
const {model, generation} = getGenerationSettings(chat, selectedModel);
|
||||
const opener = await generateRoleplayOpener({selectedModel: model, generation,
|
||||
assistantPersona, userPersona, scenarioPrompt, sceneSummary, signal: activeSignal});
|
||||
activeSignal.throwIfAborted();
|
||||
if (!opener || containsUserVoiceInRoleplayOpener(opener, userPersona)) {
|
||||
return {chat, existing: false, generatedInitialMessage: false, degraded: true};
|
||||
}
|
||||
insertChatMessage(chatId, "bot", opener, null, model);
|
||||
repositories.touchChat(chatId, user);
|
||||
return {chat: repositories.getChat(chatId, user), existing: false, generatedInitialMessage: true, opener};
|
||||
}));
|
||||
},
|
||||
persistChatMessageRetryState,
|
||||
buildMessagesPayload: buildConversationContext,
|
||||
async sendChatMessage({ user, chatId, message, model, attachments = [], webSearch = false, reasoning = false, requestId = null }) {
|
||||
return generateChatTurn({ user, chatId, message, model, attachments, webSearch, reasoning, requestId });
|
||||
async sendChatMessage({ user, chatId, message, model, attachments = [], webSearch = false, reasoning = false, signal = null, requestId = null }) {
|
||||
return generateChatTurn({ user, chatId, message, model, attachments, webSearch, reasoning, signal, requestId });
|
||||
},
|
||||
async streamChatMessage({ user, chatId, message, model, attachments = [], webSearch = false, reasoning = false, onStatus, onChunk, onThinking, signal = null, requestId = null }) {
|
||||
return generateChatTurn({ user, chatId, message, model, attachments, webSearch, reasoning, onStatus, onChunk, onThinking, signal, requestId });
|
||||
@@ -706,37 +731,39 @@ export function createChatService(repositories, modelService, config, webSearchP
|
||||
throw error;
|
||||
}).finally(() => regeneratingMessages.delete(chatId));
|
||||
},
|
||||
async editAndResendMessage({ user, chatId, targetMessage, content, model, webSearch = false, onChunk = null, signal = null, requestId = null }) {
|
||||
async editAndResendMessage({ user, chatId, targetMessage, content, model, webSearch = false, reasoning = false, onChunk = null, signal = null, requestId = null }) {
|
||||
if (!targetMessage || targetMessage.role !== "user") return { error: "Only user messages can be edited and resent", status: 400 };
|
||||
const sourceChat = repositories.getChat(chatId, user);
|
||||
if (!sourceChat) return { error: "Chat not found", status: 404 };
|
||||
const cloned = cloneChatThroughMessage({
|
||||
user,
|
||||
sourceChat,
|
||||
messageId: targetMessage.parent_message_id ?? null
|
||||
});
|
||||
if (cloned.error) return cloned;
|
||||
if (chatGenerationQueues.has(chatId)) return { error: "A response is already being generated", status: 409 };
|
||||
return withChatGenerationLock(chatId, () => withActiveGeneration(chatId, signal, async activeSignal => {
|
||||
const sourceChat = repositories.getChat(chatId, user);
|
||||
if (!sourceChat) return { error: "Chat not found", status: 404 };
|
||||
const freshTarget = repositories.getChatMessage(chatId, targetMessage.id);
|
||||
if (!freshTarget) return { error: "Message not found", status: 404 };
|
||||
const cloned = cloneChatThroughMessage({user, sourceChat, messageId: freshTarget.parent_message_id ?? null});
|
||||
if (cloned.error) return cloned;
|
||||
|
||||
const generated = await generateChatTurn({
|
||||
user,
|
||||
chatId: cloned.chat.id,
|
||||
attachments: attachmentService?.list(targetMessage.id).map(item => item.id) || [],
|
||||
reuseChatId: chatId,
|
||||
message: content,
|
||||
model,
|
||||
webSearch,
|
||||
onChunk,
|
||||
signal,
|
||||
requestId
|
||||
});
|
||||
if (generated.error) return { ...generated, chat: cloned.chat };
|
||||
return {
|
||||
...generated,
|
||||
chat: repositories.getChat(cloned.chat.id, user),
|
||||
messages: listActiveBranchMessages(cloned.chat.id),
|
||||
sourceChatId: chatId,
|
||||
branchedFromMessageId: targetMessage.id
|
||||
};
|
||||
const generated = await generateChatTurn({
|
||||
user,
|
||||
chatId: cloned.chat.id,
|
||||
attachments: attachmentService?.list(freshTarget.id).map(item => item.id) || [],
|
||||
reuseChatId: chatId,
|
||||
message: content,
|
||||
model,
|
||||
webSearch,
|
||||
reasoning,
|
||||
onChunk,
|
||||
signal: activeSignal,
|
||||
requestId
|
||||
});
|
||||
if (generated.error) return { ...generated, chat: cloned.chat };
|
||||
return {
|
||||
...generated,
|
||||
chat: repositories.getChat(cloned.chat.id, user),
|
||||
messages: listActiveBranchMessages(cloned.chat.id),
|
||||
sourceChatId: chatId,
|
||||
branchedFromMessageId: freshTarget.id
|
||||
};
|
||||
}));
|
||||
},
|
||||
createBranchedChat({ user, chatId, messageId }) {
|
||||
const sourceChat = repositories.getChat(chatId, user);
|
||||
|
||||
Reference in New Issue
Block a user