From 42fd727a4c301aa9a889b9ea3e07ab1fef726977 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Wed, 30 Sep 2026 00:14:43 +0200 Subject: [PATCH] fix: approving a tool call twice at once runs the tool twice Double-clicking Approve on a tool call that waits for approval, or approving it from two open tabs, could run the tool twice while the chat only showed one result. Both approvals were accepted because neither saw the other. Now only the first approval goes through and the other gets the "already resolved" error, also across workers when Redis is configured. Approving two different tool calls in the same reply at the same time could also lose one approval, so one tool ran twice and the other never ran; both now run once. --- backend/open_webui/main.py | 12 +++++++++++- backend/open_webui/utils/tool_approval.py | 8 +++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index ecba6407f3..0f9d455136 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1888,6 +1888,9 @@ generate_chat_completions = chat_completion generate_chat_completion = chat_completion +_tool_call_resolve_locks: dict[str, asyncio.Lock] = {} + + @app.post('/api/v1/chats/{id}/messages/{message_id}/resolve') async def resolve_chat_message_tool_call( request: Request, @@ -1897,7 +1900,14 @@ async def resolve_chat_message_tool_call( user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): - resolution = await resolve_tool_call_output(id, message_id, form_data, user, db=db) + redis = request.app.state.redis + if redis: + resolve_lock = redis.lock(f'{REDIS_KEY_PREFIX}:tool_call:resolve_lock:{message_id}', timeout=60) + else: + resolve_lock = _tool_call_resolve_locks.setdefault(message_id, asyncio.Lock()) + + async with resolve_lock: + resolution = await resolve_tool_call_output(id, message_id, form_data, user, db=db) payload = await build_tool_approval_resume_payload(id, message_id, chat=resolution['chat']) result = await chat_completion(request, payload, user) return { diff --git a/backend/open_webui/utils/tool_approval.py b/backend/open_webui/utils/tool_approval.py index 7d5376bb01..d59999e665 100644 --- a/backend/open_webui/utils/tool_approval.py +++ b/backend/open_webui/utils/tool_approval.py @@ -55,9 +55,11 @@ async def resolve_tool_call_output( function_call.setdefault('call_id', form_data.call_id) tool_name = function_call.get('name') - if any( - item.get('type') == 'function_call_output' and item.get('call_id') == form_data.call_id for item in output - ) or function_call.get('status') not in {'pending', 'queued', 'requires_approval'}: + if ( + any(item.get('type') == 'function_call_output' and item.get('call_id') == form_data.call_id for item in output) + or function_call.get('status') not in {'pending', 'queued', 'requires_approval'} + or function_call.get('approved') is True + ): raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail='Tool call has already been resolved.') if form_data.action == 'approve':