From 7bbfbda49cd219bff61caf4894e99e79aa0b243f Mon Sep 17 00:00:00 2001 From: majdyz Date: Sat, 11 Apr 2026 23:17:29 +0000 Subject: [PATCH] fix(copilot): evict per-path edit locks after use to prevent memory leak Clean up _edit_locks entries after the edit completes when no other coroutine is waiting on the same path. Prevents unbounded growth of the lock dictionary in long-running server deployments. --- .../backend/backend/copilot/sdk/file_tools.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/autogpt_platform/backend/backend/copilot/sdk/file_tools.py b/autogpt_platform/backend/backend/copilot/sdk/file_tools.py index 5aa203fba8..321953eb30 100644 --- a/autogpt_platform/backend/backend/copilot/sdk/file_tools.py +++ b/autogpt_platform/backend/backend/copilot/sdk/file_tools.py @@ -178,7 +178,6 @@ WRITE_TOOL_SCHEMA: dict[str, Any] = { "description": "The content to write to the file.", }, }, - "required": ["file_path", "content"], } @@ -327,7 +326,6 @@ READ_TOOL_SCHEMA: dict[str, Any] = { "description": "Number of lines to read. Default: 2000.", }, }, - "required": ["file_path"], } @@ -377,7 +375,8 @@ async def _handle_edit_non_e2b(args: dict[str, Any]) -> dict[str, Any]: # the read-modify-write cycle and silently dropping changes. if resolved not in _edit_locks: _edit_locks[resolved] = asyncio.Lock() - async with _edit_locks[resolved]: + lock = _edit_locks[resolved] + async with lock: try: with open(resolved, encoding="utf-8") as f: content = f.read() @@ -410,6 +409,10 @@ async def _handle_edit_non_e2b(args: dict[str, Any]) -> dict[str, Any]: except Exception as exc: return _mcp(f"Failed to write {file_path}: {exc}", error=True) + # Evict lock when no other coroutine is waiting, preventing unbounded growth. + if not lock.locked() and _edit_locks.get(resolved) is lock: + _edit_locks.pop(resolved, None) + return _mcp(f"Edited {resolved} ({count} replacement{'s' if count > 1 else ''})")