fix: Skip Retention for Persistent Agent Resource Files (#13394)

This commit is contained in:
Max Sanna 2026-05-29 17:51:37 +02:00 committed by GitHub
parent 06a6c42435
commit f8d7d7f891
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 142 additions and 2 deletions

View file

@ -67,6 +67,17 @@ const createSanitizedUploadWrapper = (uploadFunction) => {
};
};
const isPersistentAgentResourceUpload = ({ messageAttachment, tool_resource }) =>
!messageAttachment && !!tool_resource;
const getAgentFileRetentionExpiry = async ({ req, messageAttachment, tool_resource }) => {
if (isPersistentAgentResourceUpload({ messageAttachment, tool_resource })) {
return {};
}
return await getRetentionExpiry(req);
};
const isMissingStorageError = (err) => {
const code = err?.code ?? err?.status ?? err?.statusCode ?? err?.response?.status;
if ([404, '404', 'ENOENT', 'NoSuchKey', 'NotFound', 'ResourceNotFound'].includes(code)) {
@ -727,7 +738,11 @@ const processAgentFileUpload = async ({ req, res, metadata }) => {
`Extracted text from "${file.originalname}" exceeds the 15MB storage limit (${Math.round(textBytes / megabyte)}MB). Try a shorter document.`,
);
}
const retentionExpiry = await getRetentionExpiry(req);
const retentionExpiry = await getAgentFileRetentionExpiry({
req,
messageAttachment,
tool_resource,
});
const fileInfo = {
...removeNullishValues({
text,
@ -925,7 +940,11 @@ const processAgentFileUpload = async ({ req, res, metadata }) => {
});
}
const retentionExpiry = await getRetentionExpiry(req);
const retentionExpiry = await getAgentFileRetentionExpiry({
req,
messageAttachment,
tool_resource,
});
const fileInfo = {
...removeNullishValues({
user: req.user.id,

View file

@ -21,6 +21,14 @@ jest.mock('librechat-data-provider', () => {
return {
...actual,
Providers: actual.Providers,
RetentionMode: actual.RetentionMode ?? { ALL: 'all', TEMPORARY: 'temporary' },
documentParserMimeTypes: actual.documentParserMimeTypes ?? [
/^application\/pdf$/,
/^application\/vnd\.openxmlformats-officedocument\./,
/^application\/vnd\.ms-excel$/,
/^application\/vnd\.oasis\.opendocument\./,
/^application\/(?:x-)?msexcel$/,
],
mergeFileConfig: jest.fn(),
};
});
@ -84,6 +92,16 @@ jest.mock('~/server/services/Files/strategies', () => ({
getStrategyFunctions: jest.fn(),
}));
jest.mock('./VectorDB/crud', () => ({
uploadVectors: jest.fn().mockResolvedValue({
bytes: 42,
filename: 'upload.bin',
filepath: 'vectordb',
embedded: true,
}),
deleteVectors: jest.fn(),
}));
jest.mock('~/server/utils', () => ({
determineFileType: jest.fn(),
}));
@ -107,6 +125,7 @@ const {
const { mergeFileConfig } = require('librechat-data-provider');
const { checkCapability } = require('~/server/services/Config');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { uploadVectors } = require('./VectorDB/crud');
const db = require('~/models');
const {
processAgentFileUpload,
@ -159,6 +178,17 @@ const makeFileConfig = ({ ocrSupportedMimeTypes = [] } = {}) => ({
text: { supportedMimeTypes: [] },
});
const setupStoredFileUpload = (result = {}) => {
const handleFileUpload = jest.fn().mockResolvedValue({
bytes: 42,
filename: 'upload.bin',
filepath: '/uploads/upload.bin',
...result,
});
getStrategyFunctions.mockReturnValue({ handleFileUpload });
return handleFileUpload;
};
describe('processAgentFileUpload', () => {
beforeEach(() => {
jest.clearAllMocks();
@ -382,6 +412,71 @@ describe('processAgentFileUpload', () => {
});
});
describe('retention for agent resource uploads', () => {
test('does not apply retention metadata to persistent agent context files', async () => {
const expiredAt = new Date('2030-01-01T00:00:00.000Z');
getRetentionExpiry.mockResolvedValueOnce({ expiredAt });
const req = makeReq({ mimetype: PDF_MIME, ocrConfig: null });
await processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() });
expect(getRetentionExpiry).not.toHaveBeenCalled();
expect(db.createFile).toHaveBeenCalledWith(expect.not.objectContaining({ expiredAt }), true);
expect(db.addAgentResourceFile).toHaveBeenCalledWith(
expect.objectContaining({
agent_id: 'agent-abc',
tool_resource: EToolResources.context,
}),
);
});
test('applies retention metadata to context files uploaded as message attachments', async () => {
const expiredAt = new Date('2030-01-01T00:00:00.000Z');
getRetentionExpiry.mockResolvedValueOnce({ expiredAt });
const req = makeReq({ mimetype: PDF_MIME, ocrConfig: null });
await processAgentFileUpload({
req,
res: mockRes,
metadata: { ...makeMetadata(), message_file: true },
});
expect(getRetentionExpiry).toHaveBeenCalledTimes(1);
expect(getRetentionExpiry.mock.calls[0][0]).toBe(req);
expect(db.createFile).toHaveBeenCalledWith(
expect.objectContaining({
expiredAt,
context: FileContext.message_attachment,
}),
true,
);
expect(db.addAgentResourceFile).not.toHaveBeenCalled();
});
test('does not apply retention metadata to persistent agent file-search files', async () => {
const expiredAt = new Date('2030-01-01T00:00:00.000Z');
getRetentionExpiry.mockResolvedValueOnce({ expiredAt });
setupStoredFileUpload();
const req = makeReq({ mimetype: 'text/plain', ocrConfig: null });
await processAgentFileUpload({
req,
res: mockRes,
metadata: { ...makeMetadata(), tool_resource: EToolResources.file_search },
});
expect(uploadVectors).toHaveBeenCalled();
expect(getRetentionExpiry).not.toHaveBeenCalled();
expect(db.createFile).toHaveBeenCalledWith(expect.not.objectContaining({ expiredAt }), true);
expect(db.addAgentResourceFile).toHaveBeenCalledWith(
expect.objectContaining({
agent_id: 'agent-abc',
tool_resource: EToolResources.file_search,
}),
);
});
});
/* Phase C / option α regression: the upload must persist its sandbox
* pointer under `metadata.codeEnvRef` (the post-cutover schema). The
* legacy `metadata.fileIdentifier` key is silently stripped by mongoose
@ -485,6 +580,32 @@ describe('processAgentFileUpload', () => {
);
});
it('does not apply retention metadata to persistent agent execute_code files', async () => {
const expiredAt = new Date('2030-01-01T00:00:00.000Z');
getRetentionExpiry.mockResolvedValueOnce({ expiredAt });
setupCodeEnvUpload({ storage_session_id: 'sess-4', file_id: 'fid-4' });
const req = makeReq();
await processAgentFileUpload({
req,
res: mockRes,
metadata: {
agent_id: 'agent-abc',
tool_resource: EToolResources.execute_code,
file_id: 'file-uuid',
},
});
expect(getRetentionExpiry).not.toHaveBeenCalled();
expect(db.createFile).toHaveBeenCalledWith(expect.not.objectContaining({ expiredAt }), true);
expect(db.addAgentResourceFile).toHaveBeenCalledWith(
expect.objectContaining({
agent_id: 'agent-abc',
tool_resource: EToolResources.execute_code,
}),
);
});
it('does not persist legacy fileIdentifier key (mongoose strict drops it)', async () => {
setupCodeEnvUpload({ storage_session_id: 'sess-3', file_id: 'fid-3' });
const req = makeReq();