From 5176d156dee966d7cb4144db4b1ef3969cb68096 Mon Sep 17 00:00:00 2001 From: TomasPalsson Date: Fri, 2 Oct 2026 13:51:22 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=B9=20fix:=20Let=20File=20Deletes=20Su?= =?UTF-8?q?cceed=20When=20the=20Code=20Environment=20Refuses=20the=20Sandb?= =?UTF-8?q?ox=20Delete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A file with a code environment reference also deletes its sandbox copy. When the Code API refused that call (409 execution_profile_mismatch after the switch to a stateful-only self-hosted Code API), the whole delete was marked failed, so the record and its agent references stayed and every later code turn aborted with 'Failed to provision N file(s) to the code environment'. The sandbox copy is a cache that expires on its own, so its delete failure is now logged and ignored. --- api/server/services/Files/process.js | 13 ++++++-- api/server/services/Files/process.spec.js | 39 +++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/api/server/services/Files/process.js b/api/server/services/Files/process.js index 35117ab248e..61b4554266a 100644 --- a/api/server/services/Files/process.js +++ b/api/server/services/Files/process.js @@ -196,8 +196,17 @@ const createDeleteFileWithSecondaryStorage = ({ source, deleteFile, deletionMeth ); } if (hasCodeEnvRef(file) && source !== FileSources.execute_code) { - secondaryDeleteMethods.push( - getDeleteMethod({ source: FileSources.execute_code, deletionMethods }), + const deleteCodeEnvCopy = getDeleteMethod({ + source: FileSources.execute_code, + deletionMethods, + }); + /* The sandbox copy is a cache of the primary bytes and expires on its own. A Code API + * that refuses the delete (wrong profile, unreachable route) must not leave the user + * with a record they can never remove. */ + secondaryDeleteMethods.push((req, file) => + deleteCodeEnvCopy(req, file).catch((err) => { + logger.warn('Could not delete code environment copy; removing the file anyway', err); + }), ); } diff --git a/api/server/services/Files/process.spec.js b/api/server/services/Files/process.spec.js index 33ec48b7861..ef9ff7cb48a 100644 --- a/api/server/services/Files/process.spec.js +++ b/api/server/services/Files/process.spec.js @@ -2786,6 +2786,45 @@ describe('processDeleteRequest', () => { expect(db.deleteFiles).toHaveBeenCalledWith(['code-resource-file']); expect(result).toEqual({ deletedFileIds: ['code-resource-file'], failedFileIds: [] }); }); + + it('removes metadata and agent references when the code environment rejects the delete', async () => { + const primaryDelete = jest.fn().mockRejectedValue( + Object.assign(new Error('no such file'), { code: 'ENOENT' }), + ); + const codeDelete = jest.fn().mockRejectedValue( + Object.assign(new Error('Request failed with status code 409'), { + response: { status: 409, data: { error: 'execution_profile_mismatch' } }, + }), + ); + getStrategyFunctions.mockImplementation((source) => + source === FileSources.execute_code + ? { deleteFile: codeDelete } + : { deleteFile: primaryDelete }, + ); + db.deleteFiles.mockResolvedValue({ deletedCount: 1 }); + const req = { + body: {}, + config: {}, + user: { id: 'user-123', tenantId: 'tenant-a' }, + }; + const file = { + file_id: 'stale-code-file', + filepath: '/uploads/user-123/stale.pptx', + source: FileSources.local, + metadata: { + codeEnvRef: { kind: 'agent', id: 'agent-abc', storage_session_id: 's', file_id: 'f' }, + }, + }; + + const result = await processDeleteRequest({ req, files: [file] }); + + expect(codeDelete).toHaveBeenCalledWith(req, file); + expect(db.deleteFiles).toHaveBeenCalledWith(['stale-code-file']); + expect(db.removeAgentResourceFilesFromAllAgents).toHaveBeenCalledWith({ + file_ids: ['stale-code-file'], + }); + expect(result).toEqual({ deletedFileIds: ['stale-code-file'], failedFileIds: [] }); + }); }); describe('sweepExpiredFiles', () => {