🐛 fix: Retain auto-routed context uploads and isolate vector temp files

An agent upload with no explicit tool_resource that resolves to the text path is
promoted to a context resource, but retention still received the original
undefined tool_resource, so in retention modes that expire conversation files
while keeping persistent agent resources these files were given an expiration
and swept.

Vector provisioning also derived its temp path from file_id alone, so two
concurrent requests for the same file shared one path and could unlink or
truncate it mid-stream, producing failed or corrupted embeddings.
This commit is contained in:
Danny Avila 2026-08-31 08:43:10 -04:00
parent bc2e9c868f
commit 2cd5076f50
3 changed files with 27 additions and 3 deletions

View file

@ -899,7 +899,7 @@ const processAgentFileUpload = async ({ req, res, metadata, sseStream }) => {
const retentionExpiry = await getAgentFileRetentionExpiry({
req,
messageAttachment,
tool_resource,
tool_resource: effectiveToolResource,
});
const fileInfo = {
@ -1149,7 +1149,7 @@ const processAgentFileUpload = async ({ req, res, metadata, sseStream }) => {
const retentionExpiry = await getAgentFileRetentionExpiry({
req,
messageAttachment,
tool_resource,
tool_resource: effectiveToolResource,
});
const fileInfo = {
...removeNullishValues({

View file

@ -1530,6 +1530,23 @@ describe('processAgentFileUpload', () => {
);
});
test('retains an auto-routed context upload as an agent resource', async () => {
const { getAgentFileRetentionExpiry } = require('@librechat/api');
mergeFileConfig.mockReturnValue(makeFileConfig());
const req = makeReq({ mimetype: DOCX_MIME, ocrConfig: null });
await processAgentFileUpload({
req,
res: mockRes,
metadata: { agent_id: 'agent-abc', file_id: 'file-uuid-123' },
}).catch(() => {});
expect(getAgentFileRetentionExpiry).toHaveBeenCalledWith(
expect.objectContaining({ toolResource: EToolResources.context }),
expect.any(Object),
);
});
test('plans extraction with the promoted context resource for auto-routed text uploads', async () => {
const { getUploadExtractedTextPlan } = require('@librechat/api');
mergeFileConfig.mockReturnValue(makeFileConfig());

View file

@ -1,6 +1,7 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
const { randomUUID } = require('crypto');
const { getCodeBaseURL } = require('@librechat/agents');
const {
logAxiosError,
@ -168,7 +169,13 @@ async function provisionToVectorDB({ req, file, entity_id, existingStream }) {
return { embedded: false, fileUpdate: null };
}
const tmpPath = path.join(os.tmpdir(), `provision-${file.file_id}${path.extname(file.filename)}`);
/* Unique per attempt: two concurrent requests provisioning the same file_id would
* otherwise share one path, and the first to finish unlinks it while the second is
* still streaming into uploadVectors. */
const tmpPath = path.join(
os.tmpdir(),
`provision-${file.file_id}-${randomUUID()}${path.extname(file.filename)}`,
);
try {
let stream = existingStream;