mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-11 00:33:40 +00:00
fix: cap message expiry to parent on message-only forced saves
A message-only forced save (branch, artifact, abort) does not run saveConvo, so on an existing ephemeral conversation whose expiredAt is already sooner than the freshly computed window the parent was left untouched while the message received the later deadline. The conversation could then be TTL-deleted first, leaving the new or edited message orphaned until its later expiry. Read the parent once before saving the message and cap the message deadline to an already-temporary parent that expires sooner, so the message never outlives its conversation. The same read gates the existing conversion/re-cap cascade, which keeps extending or shortening the parent when that is the correct action.
This commit is contained in:
parent
1f19ddc714
commit
ec8eec6eaf
2 changed files with 50 additions and 3 deletions
|
|
@ -899,6 +899,33 @@ describe('Message Operations', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('caps a message to a sooner parent expiry instead of orphaning it', async () => {
|
||||
const conversationId = uuidv4();
|
||||
const parentExpiry = new Date(Date.now() + 60 * 60 * 1000);
|
||||
await Conversation().create({
|
||||
conversationId,
|
||||
user: 'user123',
|
||||
endpoint: 'openAI',
|
||||
title: 'Ephemeral chat expiring soon',
|
||||
isTemporary: true,
|
||||
expiredAt: parentExpiry,
|
||||
});
|
||||
|
||||
const saved = await saveMessage(
|
||||
{
|
||||
userId: 'user123',
|
||||
interfaceConfig: { temporaryChatRetention: 24, retentionMode: RetentionMode.EPHEMERAL },
|
||||
},
|
||||
{ messageId: uuidv4(), conversationId, text: 'branch', user: 'user123' },
|
||||
);
|
||||
|
||||
expect(saved?.expiredAt?.getTime()).toBe(parentExpiry.getTime());
|
||||
|
||||
const convo = await Conversation().findOne({ conversationId }).lean();
|
||||
expect(convo?.expiredAt?.getTime()).toBe(parentExpiry.getTime());
|
||||
expect(saved?.expiredAt?.getTime()).toBeLessThanOrEqual(convo?.expiredAt?.getTime() ?? 0);
|
||||
});
|
||||
|
||||
it('does not touch the parent conversation outside forced retention', async () => {
|
||||
const conversationId = uuidv4();
|
||||
await Conversation().create({
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { RetentionMode, isForcedTemporaryRetention } from 'librechat-data-provid
|
|||
import type { DeleteResult, FilterQuery, Model, PipelineStage } from 'mongoose';
|
||||
import type { AppConfig, IConversation, IMessage } from '~/types';
|
||||
import {
|
||||
conversationNeedsForcedRetention,
|
||||
createFallbackRetentionDate,
|
||||
forceConversationMessagesTemporary,
|
||||
forcedRetentionGapFilter,
|
||||
|
|
@ -164,6 +165,25 @@ export function createMessageMethods(mongoose: typeof import('mongoose')): Messa
|
|||
logger.info(`---\`saveMessage\` context: ${metadata?.context}`);
|
||||
update.tokenCount = 0;
|
||||
}
|
||||
|
||||
const forcedExpiredAt = update.expiredAt;
|
||||
const isForcedRetention = isForcedTemporaryRetention(interfaceConfig?.retentionMode);
|
||||
let parentRetention: { isTemporary?: boolean | null; expiredAt?: Date | null } | null = null;
|
||||
if (isForcedRetention && forcedExpiredAt instanceof Date) {
|
||||
const Conversation = mongoose.models.Conversation as Model<IConversation>;
|
||||
parentRetention = await Conversation.findOne(
|
||||
{ conversationId, user: userId },
|
||||
'isTemporary expiredAt',
|
||||
).lean<{ isTemporary?: boolean | null; expiredAt?: Date | null } | null>();
|
||||
if (
|
||||
parentRetention?.isTemporary === true &&
|
||||
parentRetention.expiredAt != null &&
|
||||
parentRetention.expiredAt.getTime() < forcedExpiredAt.getTime()
|
||||
) {
|
||||
update.expiredAt = parentRetention.expiredAt;
|
||||
}
|
||||
}
|
||||
|
||||
const message = await Message.findOneAndUpdate(
|
||||
{ messageId: params.messageId, user: userId },
|
||||
update,
|
||||
|
|
@ -183,10 +203,10 @@ export function createMessageMethods(mongoose: typeof import('mongoose')): Messa
|
|||
message.isTemporary = false;
|
||||
}
|
||||
|
||||
const forcedExpiredAt = update.expiredAt;
|
||||
if (
|
||||
isForcedTemporaryRetention(interfaceConfig?.retentionMode) &&
|
||||
forcedExpiredAt instanceof Date
|
||||
isForcedRetention &&
|
||||
forcedExpiredAt instanceof Date &&
|
||||
conversationNeedsForcedRetention(parentRetention, forcedExpiredAt)
|
||||
) {
|
||||
const Conversation = mongoose.models.Conversation as Model<IConversation>;
|
||||
const convoResult = await Conversation.updateOne(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue