From 2cd5076f501bb745aac9a7cf786aaac4ce16896e Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 31 Aug 2026 08:43:10 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20Retain=20auto-routed=20co?= =?UTF-8?q?ntext=20uploads=20and=20isolate=20vector=20temp=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- api/server/services/Files/process.js | 4 ++-- api/server/services/Files/process.spec.js | 17 +++++++++++++++++ api/server/services/Files/provision.js | 9 ++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/api/server/services/Files/process.js b/api/server/services/Files/process.js index ba35f4584f..b0e03d8b16 100644 --- a/api/server/services/Files/process.js +++ b/api/server/services/Files/process.js @@ -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({ diff --git a/api/server/services/Files/process.spec.js b/api/server/services/Files/process.spec.js index 8c8e8def3e..0b544aad44 100644 --- a/api/server/services/Files/process.spec.js +++ b/api/server/services/Files/process.spec.js @@ -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()); diff --git a/api/server/services/Files/provision.js b/api/server/services/Files/provision.js index f89d6e293b..00cd455237 100644 --- a/api/server/services/Files/provision.js +++ b/api/server/services/Files/provision.js @@ -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;