From b0a32b7d6d79d5cf0e50fafaffdd181647b60adf Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 14 Feb 2026 13:39:03 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=91=BB=20fix:=20Prevent=20Async=20Title?= =?UTF-8?q?=20Generation=20From=20Recreating=20Deleted=20Conversations=20(?= =?UTF-8?q?#11797)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 fix: Prevent deleted conversations from being recreated by async title generation When a user deletes a chat while auto-generated title is still in progress, `saveConvo` with `upsert: true` recreates the deleted conversation as a ghost entry with only a title and no messages. This adds a `noUpsert` metadata option to `saveConvo` and uses it in both agent and assistant title generation paths, so the title save is skipped if the conversation no longer exists. * test: conversation creation logic with noUpsert option Added new tests to validate the behavior of the `saveConvo` function with the `noUpsert` option. This includes scenarios where a conversation should not be created if it doesn't exist, updating an existing conversation when `noUpsert` is true, and ensuring that upsert behavior remains the default when `noUpsert` is not provided. These changes improve the flexibility and reliability of conversation management. * test: Clean up Conversation.spec.js by removing commented-out code Removed unnecessary comments from the Conversation.spec.js test file to improve readability and maintainability. This includes comments related to database verification and temporary conversation handling, streamlining the test cases for better clarity. --- api/models/Conversation.js | 7 ++- api/models/Conversation.spec.js | 45 +++++++++++++++++-- api/server/services/Endpoints/agents/title.js | 2 +- .../services/Endpoints/assistants/title.js | 4 +- 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/api/models/Conversation.js b/api/models/Conversation.js index a8f5f9a36c..32eac1a764 100644 --- a/api/models/Conversation.js +++ b/api/models/Conversation.js @@ -124,10 +124,15 @@ module.exports = { updateOperation, { new: true, - upsert: true, + upsert: metadata?.noUpsert !== true, }, ); + if (!conversation) { + logger.debug('[saveConvo] Conversation not found, skipping update'); + return null; + } + return conversation.toObject(); } catch (error) { logger.error('[saveConvo] Error saving conversation', error); diff --git a/api/models/Conversation.spec.js b/api/models/Conversation.spec.js index b6237d5f15..bd415b4165 100644 --- a/api/models/Conversation.spec.js +++ b/api/models/Conversation.spec.js @@ -106,6 +106,47 @@ describe('Conversation Operations', () => { expect(result.conversationId).toBe(newConversationId); }); + it('should not create a conversation when noUpsert is true and conversation does not exist', async () => { + const nonExistentId = uuidv4(); + const result = await saveConvo( + mockReq, + { conversationId: nonExistentId, title: 'Ghost Title' }, + { noUpsert: true }, + ); + + expect(result).toBeNull(); + + const dbConvo = await Conversation.findOne({ conversationId: nonExistentId }); + expect(dbConvo).toBeNull(); + }); + + it('should update an existing conversation when noUpsert is true', async () => { + await saveConvo(mockReq, mockConversationData); + + const result = await saveConvo( + mockReq, + { conversationId: mockConversationData.conversationId, title: 'Updated Title' }, + { noUpsert: true }, + ); + + expect(result).not.toBeNull(); + expect(result.title).toBe('Updated Title'); + expect(result.conversationId).toBe(mockConversationData.conversationId); + }); + + it('should still upsert by default when noUpsert is not provided', async () => { + const newId = uuidv4(); + const result = await saveConvo(mockReq, { + conversationId: newId, + title: 'New Conversation', + endpoint: EModelEndpoint.openAI, + }); + + expect(result).not.toBeNull(); + expect(result.conversationId).toBe(newId); + expect(result.title).toBe('New Conversation'); + }); + it('should handle unsetFields metadata', async () => { const metadata = { unsetFields: { someField: 1 }, @@ -122,7 +163,6 @@ describe('Conversation Operations', () => { describe('isTemporary conversation handling', () => { it('should save a conversation with expiredAt when isTemporary is true', async () => { - // Mock app config with 24 hour retention mockReq.config.interfaceConfig.temporaryChatRetention = 24; mockReq.body = { isTemporary: true }; @@ -135,7 +175,6 @@ describe('Conversation Operations', () => { expect(result.expiredAt).toBeDefined(); expect(result.expiredAt).toBeInstanceOf(Date); - // Verify expiredAt is approximately 24 hours in the future const expectedExpirationTime = new Date(beforeSave.getTime() + 24 * 60 * 60 * 1000); const actualExpirationTime = new Date(result.expiredAt); @@ -157,7 +196,6 @@ describe('Conversation Operations', () => { }); it('should save a conversation without expiredAt when isTemporary is not provided', async () => { - // No isTemporary in body mockReq.body = {}; const result = await saveConvo(mockReq, mockConversationData); @@ -167,7 +205,6 @@ describe('Conversation Operations', () => { }); it('should use custom retention period from config', async () => { - // Mock app config with 48 hour retention mockReq.config.interfaceConfig.temporaryChatRetention = 48; mockReq.body = { isTemporary: true }; diff --git a/api/server/services/Endpoints/agents/title.js b/api/server/services/Endpoints/agents/title.js index 1d6d359bd6..e31cdeea11 100644 --- a/api/server/services/Endpoints/agents/title.js +++ b/api/server/services/Endpoints/agents/title.js @@ -71,7 +71,7 @@ const addTitle = async (req, { text, response, client }) => { conversationId: response.conversationId, title, }, - { context: 'api/server/services/Endpoints/agents/title.js' }, + { context: 'api/server/services/Endpoints/agents/title.js', noUpsert: true }, ); } catch (error) { logger.error('Error generating title:', error); diff --git a/api/server/services/Endpoints/assistants/title.js b/api/server/services/Endpoints/assistants/title.js index a34de4d1af..1fae68cf54 100644 --- a/api/server/services/Endpoints/assistants/title.js +++ b/api/server/services/Endpoints/assistants/title.js @@ -69,7 +69,7 @@ const addTitle = async (req, { text, responseText, conversationId }) => { conversationId, title, }, - { context: 'api/server/services/Endpoints/assistants/addTitle.js' }, + { context: 'api/server/services/Endpoints/assistants/addTitle.js', noUpsert: true }, ); } catch (error) { logger.error('[addTitle] Error generating title:', error); @@ -81,7 +81,7 @@ const addTitle = async (req, { text, responseText, conversationId }) => { conversationId, title: fallbackTitle, }, - { context: 'api/server/services/Endpoints/assistants/addTitle.js' }, + { context: 'api/server/services/Endpoints/assistants/addTitle.js', noUpsert: true }, ); } };