LibreChat/api/server/services/Endpoints/agents/title.js
Danny Avila b0a32b7d6d
👻 fix: Prevent Async Title Generation From Recreating Deleted Conversations (#11797)
* 🐛 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.
2026-02-14 13:57:00 -05:00

81 lines
2.1 KiB
JavaScript

const { isEnabled } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const { CacheKeys } = require('librechat-data-provider');
const getLogStores = require('~/cache/getLogStores');
const { saveConvo } = require('~/models');
/**
* Add title to conversation in a way that avoids memory retention
*/
const addTitle = async (req, { text, response, client }) => {
const { TITLE_CONVO = true } = process.env ?? {};
if (!isEnabled(TITLE_CONVO)) {
return;
}
if (client.options.titleConvo === false) {
return;
}
// Skip title generation for temporary conversations
if (req?.body?.isTemporary) {
return;
}
const titleCache = getLogStores(CacheKeys.GEN_TITLE);
const key = `${req.user.id}-${response.conversationId}`;
/** @type {NodeJS.Timeout} */
let timeoutId;
try {
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => reject(new Error('Title generation timeout')), 45000);
}).catch((error) => {
logger.error('Title error:', error);
});
let titlePromise;
let abortController = new AbortController();
if (client && typeof client.titleConvo === 'function') {
titlePromise = Promise.race([
client
.titleConvo({
text,
abortController,
})
.catch((error) => {
logger.error('Client title error:', error);
}),
timeoutPromise,
]);
} else {
return;
}
const title = await titlePromise;
if (!abortController.signal.aborted) {
abortController.abort();
}
if (timeoutId) {
clearTimeout(timeoutId);
}
if (!title) {
logger.debug(`[${key}] No title generated`);
return;
}
await titleCache.set(key, title, 120000);
await saveConvo(
req,
{
conversationId: response.conversationId,
title,
},
{ context: 'api/server/services/Endpoints/agents/title.js', noUpsert: true },
);
} catch (error) {
logger.error('Error generating title:', error);
}
};
module.exports = addTitle;