mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-10 08:13:46 +00:00
fix: refresh project stats when forced retention hides a project chat
applyForcedRetention (message edit/feedback/share/tag writes) converts a chat to isTemporary: true, which visibleProjectConversationFilter excludes, but unlike saveConvo it never recomputed the owning project's cached stats. A pre-existing project chat converted this way left the project's conversationCount and lastConversationId pointing at a chat the workspace no longer shows. After the cascade, recompute the stats of every touched chat's project (the single conversation for applyForcedRetention, all tagged chats for applyForcedRetentionToTag), matching saveConvo's retention-visibility refresh. Scoped to conversations carrying a chatProjectId, so it is a no-op for chats outside any project.
This commit is contained in:
parent
8bcfb0772c
commit
e43364982d
2 changed files with 74 additions and 1 deletions
|
|
@ -2,7 +2,7 @@ import mongoose from 'mongoose';
|
|||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { RetentionMode } from 'librechat-data-provider';
|
||||
import { MongoMemoryServer } from 'mongodb-memory-server';
|
||||
import type { IConversation, IMessage, IMongoFile, ISharedLink } from '..';
|
||||
import type { IChatProject, IConversation, IMessage, IMongoFile, ISharedLink } from '..';
|
||||
import { tenantStorage, runAsSystem } from '~/config/tenantContext';
|
||||
import { sweepForcedRetention } from '../utils/retention';
|
||||
import { createMessageMethods } from './message';
|
||||
|
|
@ -1164,6 +1164,42 @@ describe('Message Operations', () => {
|
|||
expect(message?.expiredAt?.getTime()).toBe(messageDeadline.getTime());
|
||||
});
|
||||
|
||||
it('refreshes owning project stats when forced retention hides a project chat', async () => {
|
||||
const ChatProject = mongoose.models.ChatProject as mongoose.Model<IChatProject>;
|
||||
await ChatProject.deleteMany({});
|
||||
const conversationId = uuidv4();
|
||||
const project = await ChatProject.create({
|
||||
name: 'Ephemeral Project',
|
||||
user: 'user123',
|
||||
conversationCount: 1,
|
||||
lastConversationId: conversationId,
|
||||
lastConversationAt: new Date(),
|
||||
});
|
||||
const projectId = project._id!.toString();
|
||||
await Conversation().create({
|
||||
conversationId,
|
||||
user: 'user123',
|
||||
endpoint: 'openAI',
|
||||
title: 'Project chat',
|
||||
isTemporary: false,
|
||||
chatProjectId: projectId,
|
||||
});
|
||||
await Message.create({ messageId: uuidv4(), conversationId, user: 'user123', text: 'hi' });
|
||||
|
||||
await applyForcedRetention(
|
||||
{
|
||||
userId: 'user123',
|
||||
interfaceConfig: { temporaryChatRetention: 24, retentionMode: RetentionMode.EPHEMERAL },
|
||||
},
|
||||
{ conversationId },
|
||||
{ context: 'tag' },
|
||||
);
|
||||
|
||||
const refreshed = await ChatProject.findById(projectId).lean<IChatProject>();
|
||||
expect(refreshed?.conversationCount).toBe(0);
|
||||
expect(refreshed?.lastConversationId ?? null).toBeNull();
|
||||
});
|
||||
|
||||
it('converts a permanent conversation and its messages without a messageId (tag write)', async () => {
|
||||
const conversationId = uuidv4();
|
||||
await Conversation().create({
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
createFallbackRetentionDate,
|
||||
} from '~/utils/retention';
|
||||
import { createTempChatExpirationDate } from '~/utils/tempChatRetention';
|
||||
import { refreshChatProjectStatsForUser } from './chatProject';
|
||||
import { tenantSafeBulkWrite } from '~/utils/tenantBulkWrite';
|
||||
import logger from '~/config/winston';
|
||||
|
||||
|
|
@ -377,6 +378,38 @@ export function createMessageMethods(mongoose: typeof import('mongoose')): Messa
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recomputes the owning project's cached stats after forced retention hides project chats.
|
||||
* A conversion flips the conversation to `isTemporary: true`, which
|
||||
* `visibleProjectConversationFilter` excludes, so a stale count/`lastConversationId` would keep
|
||||
* pointing at a chat the project workspace no longer shows — matching `saveConvo`, which already
|
||||
* refreshes project stats on a retention-visibility change. Scoped to conversations carrying a
|
||||
* `chatProjectId`; a no-op when none of the touched chats belong to a project.
|
||||
*/
|
||||
async function refreshForcedRetentionProjectStats(
|
||||
userId: string,
|
||||
conversationFilter: FilterQuery<IConversation>,
|
||||
): Promise<void> {
|
||||
const Conversation = mongoose.models.Conversation as Model<IConversation>;
|
||||
const projectChats = await Conversation.find(
|
||||
{ user: userId, ...conversationFilter, chatProjectId: { $exists: true, $ne: null } },
|
||||
'chatProjectId',
|
||||
).lean<Array<{ chatProjectId?: string | null }>>();
|
||||
if (projectChats.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const projectIds = new Set<string>();
|
||||
for (const chat of projectChats) {
|
||||
if (typeof chat.chatProjectId === 'string' && chat.chatProjectId.length > 0) {
|
||||
projectIds.add(chat.chatProjectId);
|
||||
}
|
||||
}
|
||||
for (const projectId of projectIds) {
|
||||
await refreshChatProjectStatsForUser(mongoose, userId, projectId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforces forced (ephemeral) retention on a conversation (and optionally a specific
|
||||
* message) that was touched outside `saveMessage`/`saveConvo` — message edits, feedback,
|
||||
|
|
@ -443,6 +476,9 @@ export function createMessageMethods(mongoose: typeof import('mongoose')): Messa
|
|||
conversationId,
|
||||
forcedExpiredAt,
|
||||
);
|
||||
await refreshForcedRetentionProjectStats(userId, {
|
||||
conversationId,
|
||||
} as FilterQuery<IConversation>);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -490,6 +526,7 @@ export function createMessageMethods(mongoose: typeof import('mongoose')): Messa
|
|||
tag,
|
||||
forcedExpiredAt,
|
||||
);
|
||||
await refreshForcedRetentionProjectStats(userId, { tags: tag } as FilterQuery<IConversation>);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue