From 30109e90b04c52a7a033986b72fb054b045accf1 Mon Sep 17 00:00:00 2001 From: WhammyLeaf <233105313+WhammyLeaf@users.noreply.github.com> Date: Sat, 15 Nov 2025 21:26:05 +0000 Subject: [PATCH 1/2] feat: support data retention for normal chats Add retentionMode config variable supporting "all" and "temporary" values. When "all" is set, data retention applies to all chats, not just temporary ones. Adds isTemporary field to conversations for proper filtering. Adapted to new TS method files in packages/data-schemas since upstream moved models out of api/models/. Based on danny-avila/LibreChat#10532 Co-Authored-By: WhammyLeaf <233105313+WhammyLeaf@users.noreply.github.com> --- .../components/Chat/Menus/BookmarkMenu.tsx | 5 +- client/src/routes/ChatRoute.tsx | 5 +- librechat.example.yaml | 1 + packages/data-provider/src/config.ts | 7 ++ packages/data-provider/src/schemas.ts | 1 + packages/data-schemas/src/app/interface.ts | 2 + .../src/methods/conversation.spec.ts | 74 +++++++++++++------ .../data-schemas/src/methods/conversation.ts | 14 +++- packages/data-schemas/src/methods/message.ts | 6 +- packages/data-schemas/src/schema/convo.ts | 4 + packages/data-schemas/src/types/convo.ts | 1 + 11 files changed, 93 insertions(+), 27 deletions(-) diff --git a/client/src/components/Chat/Menus/BookmarkMenu.tsx b/client/src/components/Chat/Menus/BookmarkMenu.tsx index d66fccd24b..7fc106e57b 100644 --- a/client/src/components/Chat/Menus/BookmarkMenu.tsx +++ b/client/src/components/Chat/Menus/BookmarkMenu.tsx @@ -26,8 +26,9 @@ const BookmarkMenu: FC = () => { const conversationId = conversation?.conversationId ?? ''; const updateConvoTags = useBookmarkSuccess(conversationId); const tags = conversation?.tags; - const isTemporary = conversation?.expiredAt != null; - + const isTemporary = + conversation.isTemporary || + (conversation.isTemporary === undefined && conversation.expiredAt != null); const menuId = useId(); const [isMenuOpen, setIsMenuOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false); diff --git a/client/src/routes/ChatRoute.tsx b/client/src/routes/ChatRoute.tsx index a17d349037..10a05ee56c 100644 --- a/client/src/routes/ChatRoute.tsx +++ b/client/src/routes/ChatRoute.tsx @@ -62,7 +62,10 @@ export default function ChatRoute() { const endpointsQuery = useGetEndpointsQuery({ enabled: isAuthenticated }); const assistantListMap = useAssistantListMap(); - const isTemporaryChat = conversation && conversation.expiredAt ? true : false; + const isTemporaryChat = + conversation && + (conversation.isTemporary || + (conversation.isTemporary === undefined && conversation.expiredAt != null)); useEffect(() => { if (conversationId === Constants.NEW_CONVO) { diff --git a/librechat.example.yaml b/librechat.example.yaml index 03bb5f5bc2..b841f812ad 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -133,6 +133,7 @@ interface: # Temporary chat retention period in hours (default: 720, min: 1, max: 8760) # temporaryChatRetention: 1 + retentionMode: "temporary" # Example Cloudflare turnstile (optional) #turnstile: diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index ae3f5b9560..98ac38499e 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -44,6 +44,7 @@ export const excludedKeys = new Set([ 'createdAt', 'updatedAt', 'expiredAt', + 'isTemporary', 'messages', 'isArchived', 'tags', @@ -640,6 +641,11 @@ const mcpServersSchema = z export type TMcpServersConfig = z.infer; +export enum RetentionMode { + ALL = 'all', + TEMPORARY = 'temporary', +} + export const interfaceSchema = z .object({ privacyPolicy: z @@ -683,6 +689,7 @@ export const interfaceSchema = z .optional(), temporaryChat: z.boolean().optional(), temporaryChatRetention: z.number().min(1).max(8760).optional(), + retentionMode: z.nativeEnum(RetentionMode).default(RetentionMode.TEMPORARY), runCode: z.boolean().optional(), webSearch: z.boolean().optional(), peoplePicker: z diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index 084f74af86..b677222c11 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -797,6 +797,7 @@ export const tConversationSchema = z.object({ iconURL: z.string().nullable().optional(), /* temporary chat */ expiredAt: z.string().nullable().optional(), + isTemporary: z.boolean().optional(), /* file token limits */ fileTokenLimit: coerceNumber.optional(), /** @deprecated */ diff --git a/packages/data-schemas/src/app/interface.ts b/packages/data-schemas/src/app/interface.ts index 1701a22fad..f07b3b8d4d 100644 --- a/packages/data-schemas/src/app/interface.ts +++ b/packages/data-schemas/src/app/interface.ts @@ -49,6 +49,8 @@ export async function loadDefaultInterface({ multiConvo: interfaceConfig?.multiConvo, agents: interfaceConfig?.agents, temporaryChat: interfaceConfig?.temporaryChat, + temporaryChatRetention: interfaceConfig?.temporaryChatRetention, + retentionMode: interfaceConfig?.retentionMode, runCode: interfaceConfig?.runCode, webSearch: interfaceConfig?.webSearch, fileSearch: interfaceConfig?.fileSearch, diff --git a/packages/data-schemas/src/methods/conversation.spec.ts b/packages/data-schemas/src/methods/conversation.spec.ts index 9e4c2d2f5d..b045486291 100644 --- a/packages/data-schemas/src/methods/conversation.spec.ts +++ b/packages/data-schemas/src/methods/conversation.spec.ts @@ -398,13 +398,24 @@ describe('Conversation Operations', () => { expect(secondSave?.expiredAt).toBeNull(); }); - it('should filter out expired conversations in getConvosByCursor', async () => { + it('should filter out temporary conversations in getConvosByCursor', async () => { // Create some test conversations - const nonExpiredConvo = await Conversation.create({ + const newNonTemporaryConvo = await Conversation.create({ conversationId: uuidv4(), user: 'user123', - title: 'Non-expired', + title: 'New Non-temporary Conversation', endpoint: EModelEndpoint.openAI, + isTemporary: false, + expiredAt: new Date(Date.now() + 24 * 60 * 60 * 1000), + updatedAt: new Date(), + }); + + const oldNonTemporaryConvo = await Conversation.create({ + conversationId: uuidv4(), + user: 'user123', + title: 'Old Non-Temporary Conversation', + endpoint: EModelEndpoint.openAI, + isTemporary: undefined, expiredAt: null, updatedAt: new Date(), }); @@ -412,9 +423,10 @@ describe('Conversation Operations', () => { await Conversation.create({ conversationId: uuidv4(), user: 'user123', - title: 'Future expired', + title: 'Temporary conversation', endpoint: EModelEndpoint.openAI, - expiredAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours from now + isTemporary: true, + expiredAt: new Date(Date.now() + 24 * 60 * 60 * 1000), updatedAt: new Date(), }); @@ -423,41 +435,61 @@ describe('Conversation Operations', () => { const result = await getConvosByCursor('user123'); - // Should only return conversations with null or non-existent expiredAt - expect(result?.conversations).toHaveLength(1); - expect(result?.conversations[0]?.conversationId).toBe(nonExpiredConvo.conversationId); + // Should return both non-temporary conversations, not the temporary one + expect(result?.conversations).toHaveLength(2); + const convoIds = result?.conversations.map((c) => c.conversationId); + expect(convoIds).toContain(newNonTemporaryConvo.conversationId); + expect(convoIds).toContain(oldNonTemporaryConvo.conversationId); }); it('should filter out expired conversations in getConvosQueried', async () => { - // Create test conversations - const nonExpiredConvo = await Conversation.create({ + const newNonTemporaryConvo = await Conversation.create({ conversationId: uuidv4(), user: 'user123', - title: 'Non-expired', + title: 'New Non-temporary Conversation', endpoint: EModelEndpoint.openAI, - expiredAt: null, + isTemporary: false, + expiredAt: new Date(Date.now() + 24 * 60 * 60 * 1000), + updatedAt: new Date(), }); - const expiredConvo = await Conversation.create({ + const oldNonTemporaryConvo = await Conversation.create({ conversationId: uuidv4(), user: 'user123', - title: 'Expired', + title: 'Old Non-Temporary Conversation', endpoint: EModelEndpoint.openAI, + isTemporary: undefined, + expiredAt: null, + updatedAt: new Date(), + }); + + const tempConvo = await Conversation.create({ + conversationId: uuidv4(), + user: 'user123', + title: 'Temporary conversation', + endpoint: EModelEndpoint.openAI, + isTemporary: true, expiredAt: new Date(Date.now() + 24 * 60 * 60 * 1000), + updatedAt: new Date(), }); const convoIds = [ - { conversationId: nonExpiredConvo.conversationId }, - { conversationId: expiredConvo.conversationId }, + { conversationId: newNonTemporaryConvo.conversationId }, + { conversationId: oldNonTemporaryConvo.conversationId }, + { conversationId: tempConvo.conversationId }, ]; const result = await getConvosQueried('user123', convoIds); - // Should only return the non-expired conversation - expect(result?.conversations).toHaveLength(1); - expect(result?.conversations[0].conversationId).toBe(nonExpiredConvo.conversationId); - expect(result?.convoMap[nonExpiredConvo.conversationId]).toBeDefined(); - expect(result?.convoMap[expiredConvo.conversationId]).toBeUndefined(); + // Should only return the non-temporary conversations + expect(result?.conversations).toHaveLength(2); + + const resultIds = result?.conversations.map((c) => c.conversationId); + expect(resultIds).toContain(newNonTemporaryConvo.conversationId); + expect(resultIds).toContain(oldNonTemporaryConvo.conversationId); + expect(result?.convoMap[newNonTemporaryConvo.conversationId]).toBeDefined(); + expect(result?.convoMap[oldNonTemporaryConvo.conversationId]).toBeDefined(); + expect(result?.convoMap[tempConvo.conversationId]).toBeUndefined(); }); }); diff --git a/packages/data-schemas/src/methods/conversation.ts b/packages/data-schemas/src/methods/conversation.ts index 00b5cfee7a..eb2136affc 100644 --- a/packages/data-schemas/src/methods/conversation.ts +++ b/packages/data-schemas/src/methods/conversation.ts @@ -1,4 +1,5 @@ import type { FilterQuery, Model, SortOrder } from 'mongoose'; +import { RetentionMode } from 'librechat-data-provider'; import { createTempChatExpirationDate } from '~/utils/tempChatRetention'; import { tenantSafeBulkWrite } from '~/utils/tenantBulkWrite'; import logger from '~/config/winston'; @@ -174,6 +175,15 @@ export function createConversationMethods( } if (isTemporary) { + update.isTemporary = true; + } else { + update.isTemporary = false; + } + + if ( + isTemporary || + interfaceConfig?.retentionMode === RetentionMode.ALL + ) { try { update.expiredAt = createTempChatExpirationDate(interfaceConfig); } catch (err) { @@ -278,7 +288,7 @@ export function createConversationMethods( } filters.push({ - $or: [{ expiredAt: null }, { expiredAt: { $exists: false } }], + $or: [{ isTemporary: false }, { isTemporary: { $exists: false } }], } as FilterQuery); if (search) { @@ -399,7 +409,7 @@ export function createConversationMethods( const results = await Conversation.find({ user, conversationId: { $in: conversationIds }, - $or: [{ expiredAt: { $exists: false } }, { expiredAt: null }], + $or: [{ isTemporary: false }, { isTemporary: { $exists: false } }], }).lean(); results.sort( diff --git a/packages/data-schemas/src/methods/message.ts b/packages/data-schemas/src/methods/message.ts index 2e638b6bfb..4eb73a7054 100644 --- a/packages/data-schemas/src/methods/message.ts +++ b/packages/data-schemas/src/methods/message.ts @@ -1,4 +1,5 @@ import type { DeleteResult, FilterQuery, Model } from 'mongoose'; +import { RetentionMode } from 'librechat-data-provider'; import logger from '~/config/winston'; import { createTempChatExpirationDate } from '~/utils/tempChatRetention'; import { tenantSafeBulkWrite } from '~/utils/tenantBulkWrite'; @@ -91,7 +92,10 @@ export function createMessageMethods(mongoose: typeof import('mongoose')): Messa messageId: params.newMessageId || params.messageId, }; - if (isTemporary) { + if ( + isTemporary || + interfaceConfig?.retentionMode === RetentionMode.ALL + ) { try { update.expiredAt = createTempChatExpirationDate(interfaceConfig); } catch (err) { diff --git a/packages/data-schemas/src/schema/convo.ts b/packages/data-schemas/src/schema/convo.ts index c8f394935a..6b845d6ce9 100644 --- a/packages/data-schemas/src/schema/convo.ts +++ b/packages/data-schemas/src/schema/convo.ts @@ -21,6 +21,10 @@ const convoSchema: Schema = new Schema( meiliIndex: true, }, messages: [{ type: Schema.Types.ObjectId, ref: 'Message' }], + isTemporary: { + type: Boolean, + default: false, + }, ...conversationPreset, agent_id: { type: String, diff --git a/packages/data-schemas/src/types/convo.ts b/packages/data-schemas/src/types/convo.ts index c7888efba2..5cd828e9d6 100644 --- a/packages/data-schemas/src/types/convo.ts +++ b/packages/data-schemas/src/types/convo.ts @@ -6,6 +6,7 @@ export interface IConversation extends Document { title?: string; user?: string; messages?: Types.ObjectId[]; + isTemporary?: boolean; // Fields provided by conversationPreset (adjust types as needed) endpoint?: string; endpointType?: string; From 48973752d353fcef5c68dee9d17344afd54aee9c Mon Sep 17 00:00:00 2001 From: Aron Gates Date: Fri, 3 Apr 2026 12:13:03 +0100 Subject: [PATCH 2/2] feat: extend data retention to files, tool calls, and shared links Add expiredAt field and TTL indexes to file, toolCall, and share schemas. Set expiredAt on tool calls, shared links, and file uploads when retentionMode is "all" or chat is temporary. Co-Authored-By: Claude Opus 4.6 (1M context) --- api/server/controllers/tools.js | 12 ++++++- api/server/routes/share.js | 28 +++++++++++++-- api/server/services/Files/Code/process.js | 3 ++ api/server/services/Files/process.js | 34 ++++++++++++++++++- .../components/Chat/Menus/BookmarkMenu.tsx | 6 ++-- .../data-schemas/src/methods/conversation.ts | 5 +-- packages/data-schemas/src/methods/message.ts | 5 +-- packages/data-schemas/src/methods/share.ts | 9 ++++- packages/data-schemas/src/schema/file.ts | 4 +++ packages/data-schemas/src/schema/share.ts | 5 +++ packages/data-schemas/src/schema/toolCall.ts | 5 +++ packages/data-schemas/src/types/file.ts | 1 + packages/data-schemas/src/types/share.ts | 1 + 13 files changed, 101 insertions(+), 17 deletions(-) diff --git a/api/server/controllers/tools.js b/api/server/controllers/tools.js index 1df11b1059..437cf44cae 100644 --- a/api/server/controllers/tools.js +++ b/api/server/controllers/tools.js @@ -1,12 +1,13 @@ const { nanoid } = require('nanoid'); const { EnvVar } = require('@librechat/agents'); const { logger } = require('@librechat/data-schemas'); -const { checkAccess, loadWebSearchAuth } = require('@librechat/api'); +const { checkAccess, loadWebSearchAuth, createTempChatExpirationDate } = require('@librechat/api'); const { Tools, AuthType, Permissions, ToolCallTypes, + RetentionMode, PermissionTypes, } = require('librechat-data-provider'); const { getRoleByName, createToolCall, getToolCallsByConvo, getMessage } = require('~/models'); @@ -181,6 +182,15 @@ const callTool = async (req, res) => { user: req.user.id, }; + if (req?.body?.isTemporary || appConfig?.interfaceConfig?.retentionMode === RetentionMode.ALL) { + try { + toolCallData.expiredAt = createTempChatExpirationDate(appConfig?.interfaceConfig); + } catch (err) { + logger.error('Error creating tool call expiration date:', err); + toolCallData.expiredAt = null; + } + } + if (!artifact || !artifact.files || toolId !== Tools.execute_code) { createToolCall(toolCallData).catch((error) => { logger.error(`Error creating tool call: ${error.message}`); diff --git a/api/server/routes/share.js b/api/server/routes/share.js index 296644afde..d2c992b27b 100644 --- a/api/server/routes/share.js +++ b/api/server/routes/share.js @@ -1,6 +1,7 @@ const express = require('express'); -const { isEnabled } = require('@librechat/api'); +const { isEnabled, createTempChatExpirationDate } = require('@librechat/api'); const { logger } = require('@librechat/data-schemas'); +const { RetentionMode } = require('librechat-data-provider'); const { getSharedMessages, createSharedLink, @@ -98,7 +99,20 @@ router.get('/link/:conversationId', requireJwtAuth, async (req, res) => { router.post('/:conversationId', requireJwtAuth, async (req, res) => { try { const { targetMessageId } = req.body; - const created = await createSharedLink(req.user.id, req.params.conversationId, targetMessageId); + let expiredAt; + if (req?.config?.interfaceConfig?.retentionMode === RetentionMode.ALL) { + try { + expiredAt = createTempChatExpirationDate(req.config?.interfaceConfig); + } catch (err) { + logger.error('Error creating shared link expiration date:', err); + } + } + const created = await createSharedLink( + req.user.id, + req.params.conversationId, + targetMessageId, + expiredAt, + ); if (created) { res.status(200).json(created); } else { @@ -112,7 +126,15 @@ router.post('/:conversationId', requireJwtAuth, async (req, res) => { router.patch('/:shareId', requireJwtAuth, async (req, res) => { try { - const updatedShare = await updateSharedLink(req.user.id, req.params.shareId); + let expiredAt; + if (req?.config?.interfaceConfig?.retentionMode === RetentionMode.ALL) { + try { + expiredAt = createTempChatExpirationDate(req.config?.interfaceConfig); + } catch (err) { + logger.error('Error creating shared link expiration date:', err); + } + } + const updatedShare = await updateSharedLink(req.user.id, req.params.shareId, expiredAt); if (updatedShare) { res.status(200).json(updatedShare); } else { diff --git a/api/server/services/Files/Code/process.js b/api/server/services/Files/Code/process.js index 7cdebeb202..55eb56d15a 100644 --- a/api/server/services/Files/Code/process.js +++ b/api/server/services/Files/Code/process.js @@ -27,6 +27,7 @@ const { filterFilesByAgentAccess } = require('~/server/services/Files/permission const { createFile, getFiles, updateFile, claimCodeFile } = require('~/models'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { convertImage } = require('~/server/services/Files/images/convert'); +const { getRetentionExpiry } = require('~/server/services/Files/process'); const { determineFileType } = require('~/server/utils'); const axios = createAxiosInstance(); @@ -182,6 +183,7 @@ const processCodeOutput = async ({ source: appConfig.fileStrategy, context: FileContext.execute_code, metadata: { fileIdentifier }, + ...getRetentionExpiry(req), }; await createFile(file, true); return Object.assign(file, { messageId, toolCallId }); @@ -241,6 +243,7 @@ const processCodeOutput = async ({ context: FileContext.execute_code, usage: isUpdate ? (claimed.usage ?? 0) + 1 : 1, createdAt: isUpdate ? claimed.createdAt : formattedDate, + ...getRetentionExpiry(req), }; await createFile(file, true); diff --git a/api/server/services/Files/process.js b/api/server/services/Files/process.js index f7d7731975..ce3eff643a 100644 --- a/api/server/services/Files/process.js +++ b/api/server/services/Files/process.js @@ -8,6 +8,7 @@ const { FileContext, FileSources, imageExtRegex, + RetentionMode, EModelEndpoint, EToolResources, mergeFileConfig, @@ -20,7 +21,12 @@ const { } = require('librechat-data-provider'); const { EnvVar } = require('@librechat/agents'); const { logger } = require('@librechat/data-schemas'); -const { sanitizeFilename, parseText, processAudioFile } = require('@librechat/api'); +const { + sanitizeFilename, + parseText, + processAudioFile, + createTempChatExpirationDate, +} = require('@librechat/api'); const { convertImage, resizeAndConvert, @@ -37,6 +43,23 @@ const { determineFileType } = require('~/server/utils'); const { STTService } = require('./Audio/STTService'); const db = require('~/models'); +/** + * Returns `{ expiredAt }` when the request indicates data retention applies, otherwise `{}`. + * Spread into file data objects before calling createFile. + * @param {ServerRequest} req + * @returns {{ expiredAt?: Date }} + */ +function getRetentionExpiry(req) { + if (req?.body?.isTemporary || req?.config?.interfaceConfig?.retentionMode === RetentionMode.ALL) { + try { + return { expiredAt: createTempChatExpirationDate(req.config?.interfaceConfig) }; + } catch (_err) { + return {}; + } + } + return {}; +} + /** * Creates a modular file upload wrapper that ensures filename sanitization * across all storage strategies. This prevents storage-specific implementations @@ -307,6 +330,7 @@ const processImageFile = async ({ req, res, metadata, returnFile = false }) => { context: FileContext.message_attachment, source, type: `image/${appConfig.imageOutputType}`, + ...getRetentionExpiry(req), width, height, }, @@ -359,6 +383,7 @@ const uploadImageBuffer = async ({ req, context, metadata = {}, resize = true }) source, type, width, + ...getRetentionExpiry(req), height, }, true, @@ -445,6 +470,7 @@ const processFileUpload = async ({ req, res, metadata }) => { context: isAssistantUpload ? FileContext.assistants : FileContext.message_attachment, model: isAssistantUpload ? req.body.model : undefined, type: file.mimetype, + ...getRetentionExpiry(req), embedded, source, height, @@ -541,6 +567,7 @@ const processAgentFileUpload = async ({ req, res, metadata }) => { filename: file.originalname, model: messageAttachment ? undefined : req.body.model, context: messageAttachment ? FileContext.message_attachment : FileContext.agents, + ...getRetentionExpiry(req), }); if (!messageAttachment && tool_resource) { @@ -717,6 +744,7 @@ const processAgentFileUpload = async ({ req, res, metadata }) => { source, height, width, + ...getRetentionExpiry(req), }); const result = await db.createFile(fileInfo, true); @@ -762,6 +790,7 @@ const processOpenAIFile = async ({ source, model: openai.req.body.model, filename: originalName ?? file_id, + ...getRetentionExpiry(openai.req), }; if (saveFile) { @@ -805,6 +834,7 @@ const processOpenAIImageOutput = async ({ req, buffer, file_id, filename, fileEx context: FileContext.assistants_output, file_id, filename, + ...getRetentionExpiry(req), }; db.createFile(file, true); return file; @@ -961,6 +991,7 @@ async function saveBase64Image( user: req.user.id, bytes: image.bytes, width: image.width, + ...getRetentionExpiry(req), height: image.height, }, true, @@ -1047,6 +1078,7 @@ function filterFile({ req, image, isAvatar }) { module.exports = { filterFile, + getRetentionExpiry, processFileURL, saveBase64Image, processImageFile, diff --git a/client/src/components/Chat/Menus/BookmarkMenu.tsx b/client/src/components/Chat/Menus/BookmarkMenu.tsx index 7fc106e57b..d7567b2019 100644 --- a/client/src/components/Chat/Menus/BookmarkMenu.tsx +++ b/client/src/components/Chat/Menus/BookmarkMenu.tsx @@ -61,9 +61,9 @@ const BookmarkMenu: FC = () => { const isActiveConvo = Boolean( conversation && - conversationId && - conversationId !== Constants.NEW_CONVO && - conversationId !== 'search', + conversationId && + conversationId !== Constants.NEW_CONVO && + conversationId !== 'search', ); const handleSubmit = useCallback( diff --git a/packages/data-schemas/src/methods/conversation.ts b/packages/data-schemas/src/methods/conversation.ts index eb2136affc..057828bb40 100644 --- a/packages/data-schemas/src/methods/conversation.ts +++ b/packages/data-schemas/src/methods/conversation.ts @@ -180,10 +180,7 @@ export function createConversationMethods( update.isTemporary = false; } - if ( - isTemporary || - interfaceConfig?.retentionMode === RetentionMode.ALL - ) { + if (isTemporary || interfaceConfig?.retentionMode === RetentionMode.ALL) { try { update.expiredAt = createTempChatExpirationDate(interfaceConfig); } catch (err) { diff --git a/packages/data-schemas/src/methods/message.ts b/packages/data-schemas/src/methods/message.ts index 4eb73a7054..6dc3966330 100644 --- a/packages/data-schemas/src/methods/message.ts +++ b/packages/data-schemas/src/methods/message.ts @@ -92,10 +92,7 @@ export function createMessageMethods(mongoose: typeof import('mongoose')): Messa messageId: params.newMessageId || params.messageId, }; - if ( - isTemporary || - interfaceConfig?.retentionMode === RetentionMode.ALL - ) { + if (isTemporary || interfaceConfig?.retentionMode === RetentionMode.ALL) { try { update.expiredAt = createTempChatExpirationDate(interfaceConfig); } catch (err) { diff --git a/packages/data-schemas/src/methods/share.ts b/packages/data-schemas/src/methods/share.ts index 2a0d2bc3bd..fe3a0c8607 100644 --- a/packages/data-schemas/src/methods/share.ts +++ b/packages/data-schemas/src/methods/share.ts @@ -345,6 +345,7 @@ export function createShareMethods(mongoose: typeof import('mongoose')) { user: string, conversationId: string, targetMessageId?: string, + expiredAt?: Date, ): Promise { if (!user || !conversationId) { throw new ShareServiceError('Missing required parameters', 'INVALID_PARAMS'); @@ -408,6 +409,7 @@ export function createShareMethods(mongoose: typeof import('mongoose')) { title, user, ...(targetMessageId && { targetMessageId }), + ...(expiredAt && { expiredAt }), }); return { shareId, conversationId }; @@ -460,7 +462,11 @@ export function createShareMethods(mongoose: typeof import('mongoose')) { /** * Update a shared link with new messages */ - async function updateSharedLink(user: string, shareId: string): Promise { + async function updateSharedLink( + user: string, + shareId: string, + expiredAt?: Date, + ): Promise { if (!user || !shareId) { throw new ShareServiceError('Missing required parameters', 'INVALID_PARAMS'); } @@ -485,6 +491,7 @@ export function createShareMethods(mongoose: typeof import('mongoose')) { messages: updatedMessages, user, shareId: newShareId, + ...(expiredAt && { expiredAt }), }; const updatedShare = (await SharedLink.findOneAndUpdate({ shareId, user }, update, { diff --git a/packages/data-schemas/src/schema/file.ts b/packages/data-schemas/src/schema/file.ts index e483541bdb..c8c9dbbc15 100644 --- a/packages/data-schemas/src/schema/file.ts +++ b/packages/data-schemas/src/schema/file.ts @@ -82,12 +82,16 @@ const file: Schema = new Schema( type: String, index: true, }, + expiredAt: { + type: Date, + }, }, { timestamps: true, }, ); +file.index({ expiredAt: 1 }, { expireAfterSeconds: 0 }); file.index({ createdAt: 1, updatedAt: 1 }); file.index( { filename: 1, conversationId: 1, context: 1, tenantId: 1 }, diff --git a/packages/data-schemas/src/schema/share.ts b/packages/data-schemas/src/schema/share.ts index 3238084889..616536fe52 100644 --- a/packages/data-schemas/src/schema/share.ts +++ b/packages/data-schemas/src/schema/share.ts @@ -8,6 +8,7 @@ export interface ISharedLink extends Document { shareId?: string; targetMessageId?: string; isPublic: boolean; + expiredAt?: Date; createdAt?: Date; updatedAt?: Date; tenantId?: string; @@ -45,10 +46,14 @@ const shareSchema: Schema = new Schema( type: String, index: true, }, + expiredAt: { + type: Date, + }, }, { timestamps: true }, ); +shareSchema.index({ expiredAt: 1 }, { expireAfterSeconds: 0 }); shareSchema.index({ conversationId: 1, user: 1, targetMessageId: 1, tenantId: 1 }); export default shareSchema; diff --git a/packages/data-schemas/src/schema/toolCall.ts b/packages/data-schemas/src/schema/toolCall.ts index d36d6b758a..cde2163ca3 100644 --- a/packages/data-schemas/src/schema/toolCall.ts +++ b/packages/data-schemas/src/schema/toolCall.ts @@ -10,6 +10,7 @@ export interface IToolCallData extends Document { attachments?: TAttachment[]; blockIndex?: number; partIndex?: number; + expiredAt?: Date; createdAt?: Date; updatedAt?: Date; tenantId?: string; @@ -50,10 +51,14 @@ const toolCallSchema: Schema = new Schema( type: String, index: true, }, + expiredAt: { + type: Date, + }, }, { timestamps: true }, ); +toolCallSchema.index({ expiredAt: 1 }, { expireAfterSeconds: 0 }); toolCallSchema.index({ messageId: 1, user: 1, tenantId: 1 }); toolCallSchema.index({ conversationId: 1, user: 1, tenantId: 1 }); diff --git a/packages/data-schemas/src/types/file.ts b/packages/data-schemas/src/types/file.ts index bbf9de3d3d..57131fdb98 100644 --- a/packages/data-schemas/src/types/file.ts +++ b/packages/data-schemas/src/types/file.ts @@ -23,6 +23,7 @@ export interface IMongoFile extends Omit { fileIdentifier?: string; }; expiresAt?: Date; + expiredAt?: Date; createdAt?: Date; updatedAt?: Date; tenantId?: string; diff --git a/packages/data-schemas/src/types/share.ts b/packages/data-schemas/src/types/share.ts index 8b54990cf4..dc0203cb93 100644 --- a/packages/data-schemas/src/types/share.ts +++ b/packages/data-schemas/src/types/share.ts @@ -10,6 +10,7 @@ export interface ISharedLink { shareId?: string; targetMessageId?: string; isPublic: boolean; + expiredAt?: Date; createdAt?: Date; updatedAt?: Date; }