mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-07 06:42:30 +00:00
fix: preserve earlier parent expiry on message-only forced saves
A message-only forced save (branch/artifact/abort/edit) to a parent carrying an active expiry sooner than the freshly computed ephemeral window only capped when the parent was already temporary. An all-mode parent (isTemporary: false) with an earlier active deadline skipped the cap, so the touched message took the later window and the cascade then rewrote the parent and its messages to that later date, extending data that was meant to expire sooner. Cap on any active parent expiredAt earlier than the forced window, regardless of isTemporary, and feed the capped deadline into the conversation cascade so it converts the parent without extending it.
This commit is contained in:
parent
b9602214e2
commit
34dfd71e3a
3 changed files with 52 additions and 11 deletions
|
|
@ -929,6 +929,47 @@ describe('Message Operations', () => {
|
|||
expect(saved?.expiredAt?.getTime()).toBeLessThanOrEqual(convo?.expiredAt?.getTime() ?? 0);
|
||||
});
|
||||
|
||||
it('caps a message-only save to an all-mode parent expiring sooner without extending it', async () => {
|
||||
const conversationId = uuidv4();
|
||||
const parentExpiry = new Date(Date.now() + 60 * 60 * 1000);
|
||||
await Conversation().create({
|
||||
conversationId,
|
||||
user: 'user123',
|
||||
endpoint: 'openAI',
|
||||
title: 'All-mode chat expiring soon',
|
||||
isTemporary: false,
|
||||
expiredAt: parentExpiry,
|
||||
});
|
||||
const olderMessageId = uuidv4();
|
||||
await Message.create({
|
||||
messageId: olderMessageId,
|
||||
conversationId,
|
||||
user: 'user123',
|
||||
text: 'older retained message',
|
||||
isTemporary: false,
|
||||
expiredAt: parentExpiry,
|
||||
});
|
||||
|
||||
const saved = await saveMessage(
|
||||
{
|
||||
userId: 'user123',
|
||||
interfaceConfig: { temporaryChatRetention: 24, retentionMode: RetentionMode.EPHEMERAL },
|
||||
},
|
||||
{ messageId: uuidv4(), conversationId, text: 'branch', user: 'user123' },
|
||||
{ context: 'branch', capExpiryToConversation: true },
|
||||
);
|
||||
|
||||
expect(saved?.expiredAt?.getTime()).toBe(parentExpiry.getTime());
|
||||
|
||||
const convo = await Conversation().findOne({ conversationId }).lean();
|
||||
expect(convo?.isTemporary).toBe(true);
|
||||
expect(convo?.expiredAt?.getTime()).toBe(parentExpiry.getTime());
|
||||
|
||||
const older = await Message.findOne({ messageId: olderMessageId }).lean();
|
||||
expect(older?.isTemporary).toBe(true);
|
||||
expect(older?.expiredAt?.getTime()).toBe(parentExpiry.getTime());
|
||||
});
|
||||
|
||||
it('backfills lagging messages when the parent already expires sooner', async () => {
|
||||
const conversationId = uuidv4();
|
||||
const parentExpiry = new Date(Date.now() + 60 * 60 * 1000);
|
||||
|
|
|
|||
|
|
@ -214,7 +214,8 @@ export function createMessageMethods(mongoose: typeof import('mongoose')): Messa
|
|||
message.isTemporary = false;
|
||||
}
|
||||
|
||||
if (isForcedRetention && forcedExpiredAt instanceof Date) {
|
||||
const cascadeExpiredAt = update.expiredAt;
|
||||
if (isForcedRetention && cascadeExpiredAt instanceof Date) {
|
||||
const Conversation = mongoose.models.Conversation as Model<IConversation>;
|
||||
const SharedLink = mongoose.models.SharedLink as Model<ISharedLink>;
|
||||
await cascadeForcedConversationRetention(
|
||||
|
|
@ -223,7 +224,7 @@ export function createMessageMethods(mongoose: typeof import('mongoose')): Messa
|
|||
SharedLink,
|
||||
userId,
|
||||
conversationId,
|
||||
forcedExpiredAt,
|
||||
cascadeExpiredAt,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -121,6 +121,10 @@ export const capConversationSharedLinks = async (
|
|||
* an already-conforming parent untouched, so older `expiredAt: null`/later messages would
|
||||
* otherwise survive the parent's TTL. Returns the forced window unchanged when no earlier
|
||||
* parent deadline applies.
|
||||
*
|
||||
* Any active earlier deadline is honored regardless of `isTemporary`: an `all`-mode parent
|
||||
* carried over with a sooner `expiredAt` must not be extended to the fresh window just
|
||||
* because it is not yet temporary — the cascade converts it afterward using this deadline.
|
||||
*/
|
||||
export const capForcedRetentionToParent = async (
|
||||
Conversation: Model<IConversation>,
|
||||
|
|
@ -130,15 +134,10 @@ export const capForcedRetentionToParent = async (
|
|||
conversationId: string,
|
||||
forcedExpiredAt: Date,
|
||||
): Promise<Date> => {
|
||||
const parent = await Conversation.findOne(
|
||||
{ conversationId, user: userId },
|
||||
'isTemporary expiredAt',
|
||||
).lean<{ isTemporary?: boolean | null; expiredAt?: Date | null } | null>();
|
||||
if (
|
||||
parent?.isTemporary === true &&
|
||||
parent.expiredAt instanceof Date &&
|
||||
parent.expiredAt.getTime() < forcedExpiredAt.getTime()
|
||||
) {
|
||||
const parent = await Conversation.findOne({ conversationId, user: userId }, 'expiredAt').lean<{
|
||||
expiredAt?: Date | null;
|
||||
} | null>();
|
||||
if (parent?.expiredAt instanceof Date && parent.expiredAt.getTime() < forcedExpiredAt.getTime()) {
|
||||
await forceConversationMessagesTemporary(Message, userId, conversationId, parent.expiredAt);
|
||||
await capConversationSharedLinks(SharedLink, userId, conversationId, parent.expiredAt);
|
||||
return parent.expiredAt;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue