🗃️ feat: Retain Agent Files During All-Data Retention (#13477)

* feat: add agent file retention exemption

* refactor: centralize agent file retention policy
This commit is contained in:
Danny Avila 2026-06-02 15:04:10 -04:00 committed by GitHub
parent 571d8d8284
commit 8ba0249f1e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 306 additions and 32 deletions

View file

@ -1,6 +1,7 @@
import { RetentionMode } from 'librechat-data-provider';
import {
createMinimalRetentionRequest,
getAgentFileRetentionExpiry,
getConversationExpirationDate,
getRetentionExpiry,
getSharedLinkExpiration,
@ -191,6 +192,129 @@ describe('retention helpers', () => {
await expect(getRetentionExpiry(undefined, dependencies)).resolves.toEqual({});
});
it('skips persistent agent files in temporary retention mode when retainAgentFiles is disabled', async () => {
const result = await getAgentFileRetentionExpiry(
{
req: request({
config: {
interfaceConfig: {
retentionMode: RetentionMode.TEMPORARY,
retainAgentFiles: false,
},
},
}),
messageAttachment: false,
toolResource: 'context',
},
dependencies,
);
expect(result).toEqual({});
expect(dependencies.getConvo).not.toHaveBeenCalled();
expect(dependencies.createExpirationDate).not.toHaveBeenCalled();
});
it('skips persistent agent files in temporary retention mode when retainAgentFiles is enabled', async () => {
const result = await getAgentFileRetentionExpiry(
{
req: request({
config: {
interfaceConfig: {
retentionMode: RetentionMode.TEMPORARY,
retainAgentFiles: true,
},
},
}),
messageAttachment: false,
toolResource: 'context',
},
dependencies,
);
expect(result).toEqual({});
expect(dependencies.getConvo).not.toHaveBeenCalled();
expect(dependencies.createExpirationDate).not.toHaveBeenCalled();
});
it('applies all-data retention to persistent agent files when retainAgentFiles is disabled', async () => {
const result = await getAgentFileRetentionExpiry(
{
req: request({
config: {
interfaceConfig: {
retentionMode: RetentionMode.ALL,
retainAgentFiles: false,
},
},
}),
messageAttachment: false,
toolResource: 'context',
},
dependencies,
);
expect(result).toEqual({ expiredAt: expirationDate });
expect(dependencies.getConvo).not.toHaveBeenCalled();
expect(dependencies.createExpirationDate).toHaveBeenCalledTimes(1);
});
it('keeps current all-data retention behavior when retainAgentFiles is unset', async () => {
const result = await getAgentFileRetentionExpiry(
{
req: request({ config: { interfaceConfig: { retentionMode: RetentionMode.ALL } } }),
messageAttachment: false,
toolResource: 'context',
},
dependencies,
);
expect(result).toEqual({ expiredAt: expirationDate });
expect(dependencies.createExpirationDate).toHaveBeenCalledTimes(1);
});
it('skips all-data retention for persistent agent files when retainAgentFiles is enabled', async () => {
const result = await getAgentFileRetentionExpiry(
{
req: request({
config: {
interfaceConfig: {
retentionMode: RetentionMode.ALL,
retainAgentFiles: true,
},
},
}),
messageAttachment: false,
toolResource: 'context',
},
dependencies,
);
expect(result).toEqual({});
expect(dependencies.getConvo).not.toHaveBeenCalled();
expect(dependencies.createExpirationDate).not.toHaveBeenCalled();
});
it('still applies all-data retention to agent message attachments when retainAgentFiles is enabled', async () => {
const result = await getAgentFileRetentionExpiry(
{
req: request({
config: {
interfaceConfig: {
retentionMode: RetentionMode.ALL,
retainAgentFiles: true,
},
},
}),
messageAttachment: true,
toolResource: 'context',
},
dependencies,
);
expect(result).toEqual({ expiredAt: expirationDate });
expect(dependencies.createExpirationDate).toHaveBeenCalledTimes(1);
});
it('parses valid conversation expiration dates and ignores invalid ones', () => {
expect(getConversationExpirationDate({ expiredAt: expirationDate })).toBe(expirationDate);
expect(getConversationExpirationDate({ expiredAt: expirationDate.toISOString() })).toEqual(

View file

@ -34,6 +34,12 @@ export type RetentionExpiry = {
expiredAt?: Date | null;
};
export type AgentFileRetentionRequest = {
req: RetentionRequest | null | undefined;
messageAttachment?: boolean | null;
toolResource?: string | null;
};
export type RetentionLogger = {
error: (message: string, error?: unknown) => void;
};
@ -160,6 +166,35 @@ export async function getRetentionExpiry(
return promise;
}
const isPersistentAgentResourceUpload = ({
messageAttachment,
toolResource,
}: Omit<AgentFileRetentionRequest, 'req'>): boolean => !messageAttachment && !!toolResource;
const shouldRetainPersistentAgentFile = ({
req,
messageAttachment,
toolResource,
}: AgentFileRetentionRequest): boolean => {
const interfaceConfig = req?.config?.interfaceConfig;
return (
isPersistentAgentResourceUpload({ messageAttachment, toolResource }) &&
(interfaceConfig?.retentionMode !== RetentionMode.ALL ||
interfaceConfig?.retainAgentFiles === true)
);
};
export async function getAgentFileRetentionExpiry(
params: AgentFileRetentionRequest,
dependencies: RetentionDependencies,
): Promise<RetentionExpiry> {
if (shouldRetainPersistentAgentFile(params)) {
return {};
}
return await getRetentionExpiry(params.req, dependencies);
}
/**
* Resolves the retention deadline for a shared link derived from a conversation.
*