mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-10 08:13:46 +00:00
fix: cap normal message saves to the preserved parent expiry
saveConvo now preserves a conversation's earlier ephemeral deadline instead of refreshing it, so the message cap can no longer be opt-in. A normal send to an existing ephemeral conversation whose parent already expires sooner saved the new message with a fresh, later TTL; saveConvo kept the parent at the earlier deadline and the cascade skipped (parent already conforms), leaving the new message to outlive the deleted conversation. Cap every forced-retention message save to the parent's deadline, not just the opt-in message-only paths. Message-only saves still backfill the parent's other messages and shares; normal saves only cap the message itself and let saveConvo and the cascade handle the rest.
This commit is contained in:
parent
98dd93d8c1
commit
acd150d4af
2 changed files with 30 additions and 20 deletions
|
|
@ -978,7 +978,7 @@ describe('Message Operations', () => {
|
|||
expect(lagging?.expiredAt?.getTime()).toBe(parentExpiry.getTime());
|
||||
});
|
||||
|
||||
it('does not cap a normal send save so a following conversation refresh stays aligned', async () => {
|
||||
it('caps a normal send to a parent expiring sooner so it cannot outlive the conversation', async () => {
|
||||
const conversationId = uuidv4();
|
||||
const parentExpiry = new Date(Date.now() + 60 * 60 * 1000);
|
||||
await Conversation().create({
|
||||
|
|
@ -998,7 +998,11 @@ describe('Message Operations', () => {
|
|||
{ messageId: uuidv4(), conversationId, text: 'follow-up', user: 'user123' },
|
||||
);
|
||||
|
||||
expect(saved?.expiredAt?.getTime()).toBeGreaterThan(parentExpiry.getTime());
|
||||
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 () => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { RetentionMode, isForcedTemporaryRetention } from 'librechat-data-provid
|
|||
import type { DeleteResult, FilterQuery, Model } from 'mongoose';
|
||||
import type { AppConfig, IConversation, IMessage, ISharedLink } from '~/types';
|
||||
import {
|
||||
capForcedRetentionExpiry,
|
||||
capForcedRetentionToParent,
|
||||
cascadeForcedConversationRetention,
|
||||
cascadeForcedRetentionByTag,
|
||||
|
|
@ -162,26 +163,31 @@ export function createMessageMethods(mongoose: typeof import('mongoose')): Messa
|
|||
const forcedExpiredAt = update.expiredAt;
|
||||
const isForcedRetention = isForcedTemporaryRetention(interfaceConfig?.retentionMode);
|
||||
/**
|
||||
* Message-only saves (branch/artifact/abort) never run `saveConvo`, so the new
|
||||
* message must not outlive a parent that already expires sooner. Callers that DO
|
||||
* refresh the conversation afterward must not opt in, otherwise the message keeps
|
||||
* the stale deadline and the TTL index deletes it before the refreshed conversation.
|
||||
* Under forced retention the new message must never outlive a parent that already
|
||||
* expires sooner, since `saveConvo` preserves that earlier deadline rather than
|
||||
* refreshing it. Message-only saves (branch/artifact/abort) additionally backfill the
|
||||
* parent's other messages and shares because no `saveConvo` follows to cascade; normal
|
||||
* saves only cap the message itself and let the conversation cascade handle the rest.
|
||||
*/
|
||||
if (
|
||||
isForcedRetention &&
|
||||
forcedExpiredAt instanceof Date &&
|
||||
metadata?.capExpiryToConversation === true
|
||||
) {
|
||||
if (isForcedRetention && forcedExpiredAt instanceof Date) {
|
||||
const Conversation = mongoose.models.Conversation as Model<IConversation>;
|
||||
const SharedLink = mongoose.models.SharedLink as Model<ISharedLink>;
|
||||
update.expiredAt = await capForcedRetentionToParent(
|
||||
Conversation,
|
||||
Message,
|
||||
SharedLink,
|
||||
userId,
|
||||
conversationId,
|
||||
forcedExpiredAt,
|
||||
);
|
||||
if (metadata?.capExpiryToConversation === true) {
|
||||
const SharedLink = mongoose.models.SharedLink as Model<ISharedLink>;
|
||||
update.expiredAt = await capForcedRetentionToParent(
|
||||
Conversation,
|
||||
Message,
|
||||
SharedLink,
|
||||
userId,
|
||||
conversationId,
|
||||
forcedExpiredAt,
|
||||
);
|
||||
} else {
|
||||
const parent = await Conversation.findOne(
|
||||
{ conversationId, user: userId },
|
||||
'expiredAt',
|
||||
).lean<{ expiredAt?: Date | null } | null>();
|
||||
update.expiredAt = capForcedRetentionExpiry(parent?.expiredAt, forcedExpiredAt);
|
||||
}
|
||||
}
|
||||
|
||||
const message = await Message.findOneAndUpdate(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue