From 44683545c2276e7db4e9665ea0d6962b4b14af8d Mon Sep 17 00:00:00 2001 From: codesensei-tushar Date: Wed, 8 Apr 2026 19:33:53 +0530 Subject: [PATCH 1/3] fix: replace blocking time.sleep with asyncio.sleep in LLM condition time.sleep() in an async context freezes the entire event loop during LLM retry backoff, blocking all other webhook processing. --- src/rules/conditions/llm_assisted.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rules/conditions/llm_assisted.py b/src/rules/conditions/llm_assisted.py index 9dbbc00..8ab127c 100644 --- a/src/rules/conditions/llm_assisted.py +++ b/src/rules/conditions/llm_assisted.py @@ -5,6 +5,7 @@ opt-in and clearly documented as having LLM latency in the evaluation path. """ +import asyncio import logging import time from typing import Any @@ -218,7 +219,7 @@ async def evaluate(self, context: Any) -> list[Violation]: ) if attempt < max_attempts: - time.sleep(wait_time) + await asyncio.sleep(wait_time) else: # All attempts failed - gracefully degrade logger.error("All LLM retry attempts exhausted; skipping alignment check.") From 15b84c4359b9741919daab6827c08c90c40274f0 Mon Sep 17 00:00:00 2001 From: codesensei-tushar Date: Wed, 8 Apr 2026 19:59:34 +0530 Subject: [PATCH 2/3] docs: add changelog entry for async sleep fix --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aee2129..15bdf94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Fixed + +- **Blocking sleep in LLM condition** -- Replaced `time.sleep()` with + `await asyncio.sleep()` in `LLMAssisted` retry backoff to avoid + freezing the event loop during LLM retries. + ### Added - **Description-diff alignment** -- `DescriptionDiffAlignmentCondition` uses From d8d3cfdaf39bac2d4a86793c0d6963d4282dbb1f Mon Sep 17 00:00:00 2001 From: codesensei-tushar Date: Thu, 9 Apr 2026 07:02:38 +0530 Subject: [PATCH 3/3] fix: update test to mock asyncio.sleep instead of time.sleep --- tests/unit/rules/conditions/test_llm_assisted.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/rules/conditions/test_llm_assisted.py b/tests/unit/rules/conditions/test_llm_assisted.py index 70aa4ac..4ee6f90 100644 --- a/tests/unit/rules/conditions/test_llm_assisted.py +++ b/tests/unit/rules/conditions/test_llm_assisted.py @@ -196,7 +196,7 @@ async def test_graceful_degradation_on_llm_failure(self, mock_get_chat_model, co assert violations == [] @pytest.mark.asyncio - @patch("time.sleep", return_value=None) # Mock sleep to speed up test + @patch("asyncio.sleep", new_callable=AsyncMock) # Mock async sleep to speed up test @patch("src.integrations.providers.get_chat_model") async def test_retry_logic_with_exponential_backoff(self, mock_get_chat_model, mock_sleep, condition): """When structured invoke fails, retries with exponential backoff.""" @@ -213,7 +213,7 @@ async def test_retry_logic_with_exponential_backoff(self, mock_get_chat_model, m # Should have retried 3 times total assert mock_structured.ainvoke.await_count == 3 # Should have slept twice (2s, 4s) - assert mock_sleep.call_count == 2 + assert mock_sleep.await_count == 2 @pytest.mark.asyncio @patch("src.integrations.providers.get_chat_model")