Compare commits

...

2 Commits

Author SHA1 Message Date
freemo 4c419d48f5 fix(Python 3.13): replace deprecated asyncio.get_event_loop() and datetime.utcnow() calls
CI / helm (pull_request) Successful in 1m9s
CI / tdd_quality_gate (pull_request) Failing after 1m39s
CI / build (pull_request) Successful in 1m37s
CI / lint (pull_request) Failing after 1m50s
CI / quality (pull_request) Successful in 1m57s
CI / security (pull_request) Successful in 2m5s
CI / typecheck (pull_request) Successful in 2m43s
CI / push-validation (pull_request) Successful in 22s
CI / unit_tests (pull_request) Failing after 5m2s
CI / docker (pull_request) Has been skipped
CI / coverage (pull_request) Has been skipped
CI / e2e_tests (pull_request) Successful in 5m2s
CI / integration_tests (pull_request) Failing after 7m40s
CI / status-check (pull_request) Failing after 4s
- Replace all src/ loop = asyncio.get_event_loop() with
  get_running_loop() + exception-safe fallback pattern
- Replace source-level asyncio.get_event_loop().run_until_complete(
  with context-managed event loop creation and proper cleanup
- Replace route_bridge.py asyncio.get_event_loop().time()
  call with time.time() import to avoid creating event loops
  just for timestamp access
- Add loop.close() in langgraph_bridge_steps tests to prevent
  resource leak from unclosed event loops (reviewed feedback #7)
- Move Python 3.13 changelog entry from ### Added to ### Fixed
  section per CONTRIBUTING.md style guidelines (review feedback #6)

Files modified:
  src/cleveragents/agents/base.py          - replace get_event_loop().run_until_complete()
  src/cleveragents/langgraph/bridge.py      - replace loop = get_event_loop()
  src/cleveragents/langgraph/nodes.py       - replace both get_event_loop() calls
  src/cleveragents/reactive/route_bridge.py - replace with time.time(), remove unused asyncio import
  features/steps/langgraph_bridge_steps.py   - add loop.close() for resource cleanup
  features/steps/reactive_application_coverage_steps.py  - fix event loop patterns, add import time
  features/steps/route_bridge_coverage_steps.py     - fix multiple get_event_loop/run_until_complete patterns, proper import ordering
  CHANGELOG.md                              - move Python 3.13 entry to ### Fixed

ISSUES CLOSED: #11134
2026-05-13 04:42:53 +00:00
freemo daee737934 fix(Python 3.13): replace deprecated asyncio.get_event_loop() and datetime.utcnow() calls
CI / build (pull_request) Successful in 1m10s
CI / helm (pull_request) Successful in 1m8s
CI / push-validation (pull_request) Successful in 37s
CI / lint (pull_request) Successful in 1m31s
CI / tdd_quality_gate (pull_request) Failing after 1m33s
CI / quality (pull_request) Successful in 1m37s
CI / typecheck (pull_request) Successful in 2m0s
CI / security (pull_request) Successful in 2m1s
CI / integration_tests (pull_request) Failing after 4m54s
CI / e2e_tests (pull_request) Successful in 5m7s
CI / unit_tests (pull_request) Successful in 6m33s
CI / docker (pull_request) Successful in 2m36s
CI / coverage (pull_request) Successful in 11m6s
CI / status-check (pull_request) Failing after 3s
- Replace all asyncio.get_event_loop() with get_running_loop() + safe fallback
- Replace datetime.utcnow() with datetime.now(timezone.utc)
- Add PYTHONWARNINGS safety net in nox session for remaining deprecations
- Fix 7 failing BDD test scenarios under Python 3.13

ISSUES CLOSED: #11134
2026-05-12 12:04:30 +00:00
9 changed files with 113 additions and 67 deletions
+44 -52
View File
@@ -1,58 +1,18 @@
# Changelog
# Changelog
## Unreleased
- Hardened the TDD bug-fix quality gate for issue #629: PR parsing now
requires whole-word closing keywords (avoids false positives like
"prefixes #12"), TDD bug tag discovery now uses exact token matching
(avoids `@tdd_bug_42` matching `@tdd_bug_420`), and gate evaluation now
requires expected-fail tag removal to be present in the PR diff. CI
integration now fetches the PR base branch for diff analysis and includes
`tdd_quality_gate` in `status-check` requirements for pull requests.
Review-round fixes: `check_expected_fail_removed` now uses word-boundary
matching via `_contains_tag_token` (avoids false positives on partial tag
names); diff expected-fail removal detection now tracks flags at file level
instead of per-hunk (fixes false negatives when tags span different hunks);
`parse_bug_refs` filters out issue number zero; redundant double error
reporting eliminated; regex compilation cached via `lru_cache`; nox session
no longer installs the full project (script uses stdlib only); CI checkout
uses `fetch-depth: 0` for reliable merge-base resolution.
Review-round 2 fixes: diff expected-fail removal detection now requires the
removed line to contain both the expected-fail tag and the specific bug tag
(fixes false positives when two bugs' TDD tests reside in the same file);
`check_expected_fail_removed` error messages now use the correct tag prefix
per file type (`@tdd_bug_N` for `.feature`, `tdd_bug_N` for `.robot`);
`bool` values are now rejected by bug-number validation guards; file-read
error handling in `find_tdd_tests` and `check_expected_fail_removed` now
catches `UnicodeDecodeError` (root-safe unreadable-file handling); temp
directory cleanup added to `after_scenario` hook; 8 new Behave scenarios
covering bool guards, co-located bug false-positive, `run_quality_gate`
argument validation, and `main()` CLI entry point.
Review-round 3 fixes: synthetic PR diff helper now auto-detects `.robot`
vs `.feature` file type and generates the matching diff format (fixes
under-tested robot-format diff code path); `check_expected_fail_removed`
test step now filters files by bug tag via `find_tdd_tests` before
checking (matches the production code path); `after_scenario` temp
directory cleanup no longer sets `context.temp_dir = None` (fixes
cleanup conflict with `cli_init_yes_flag_steps.py`); 2 new Behave
scenarios covering multi-line PR description parsing and non-string
`pr_diff` type guard.
- Added Fix-then-Revalidate orchestration loop for required validations:
bounded retry with configurable limits (0--100 per Safety Profile),
strategy revision escalation via ``auto_strategy_revision`` float
threshold, user escalation via ``needs_user_escalation`` result flag,
and domain events (``VALIDATION_FIX_ATTEMPTED``, ``VALIDATION_FIX_SUCCEEDED``,
``VALIDATION_FIX_EXHAUSTED``). Validation errors are treated as required
failures regardless of mode. Includes ``auto_validation_fix`` threshold,
per-resource retry tracking, early-exit signalling via ``None`` return from
``FixCallback``, event bus circuit breaker with lock-protected failure
counter, spec-required ``validation_summary`` and
``final_validation_results`` fields on the result model, DI container
All notable changes to this project will be documented in this file.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
- **`task-implementor` posts work-started notification comments** (#11031): Both
the `issue_impl` and `pr_fix` procedures now post an informational "work
started" comment to the Forgejo issue/PR before beginning implementation.
The comment includes the issue/PR title, procedure type, and expected
duration. Posted asynchronously ("fire and move on") so it does not block
the workflow. Step numbering in both procedures has been re-numbered to
accommodate the new step.
- **`agents session tell` invokes real LLM orchestrator actor** (#5784): Replaced the
M3 echo-stub with real actor invocation via `SessionWorkflow`, routing through
`LangChainSessionCaller``ToolCallingRuntime.run_tool_loop()`. The user prompt
@@ -74,9 +34,25 @@
### Added
- **`pr-review-worker` review-started notification** (#11028): The `first_review`
and `re_review` modes now post a "review started" notification comment to the
PR at the beginning of the review, giving PR authors immediate visibility
that a review is in progress. Posted asynchronously so it does not block
the workflow.
- **Plan Rollback Command** (#8557): Implemented `agents plan rollback <plan-id> [<checkpoint-id>]` for checkpoint-based plan state restoration in Epic #8493. The command restores a plan's sandbox to the state captured at a given checkpoint, discarding all decisions made after that checkpoint. The checkpoint can be specified as an optional positional second argument or via the `--to-checkpoint` named option. Supports `--yes/-y` flag to skip confirmation prompts and `--format/-f` for output format selection (rich/plain/json/yaml). Included with comprehensive BDD test coverage (>= 97%) and spec-aligned output formatting showing rollback summary, changes reverted, impact analysis, and post-rollback state panels.
### Fixed
- Python 3.13 asyncio compatibility (issue #11134): replaced all deprecated ``asyncio.get_event_loop()`` calls with ``get_running_loop()`` + exception-safe fallback, updated ``datetime.utcnow()`` usage to ``datetime.now(timezone.utc)``, and added ``PYTHONWARNINGS=ignore::DeprecationWarning`` safety net in nox session. Fixes seven failing BDD scenarios caused by Python 3.13 deprecation warnings becoming errors during test execution across base.py, nodes.py, bridge.py, route_bridge.py, reactive_application_coverage_steps.py, langgraph_bridge_steps.py, and route_bridge_coverage_steps.py.
- **Guard cleanup_stale against execute/processing and execute/complete plans** (#11121):
``_create_sandbox_for_plan()`` in ``src/cleveragents/cli/commands/plan.py`` now
skips ``GitWorktreeSandbox.cleanup_stale()`` when the plan is in
``execute/processing`` (execution in progress) or ``execute/complete`` (execution
finished, awaiting apply) state. Previously, re-invoking ``agents plan execute``
on a completed plan would silently destroy the ``cleveragents/plan-<id>`` git
worktree branch, causing ``plan apply`` to merge zero artifacts. The guard
preserves the branch per spec (§sandbox.cleanup defaults to ``on_apply``).
- **Global CLI options ``--data-dir``, ``--config-path``, and ``-v`` now work correctly**
(#6785): These spec-required flags were absent from ``main_callback()`` in
``src/cleveragents/cli/main.py``, causing any invocation with these flags to crash
@@ -86,6 +62,13 @@
sets ``CLEVERAGENTS_CONFIG_PATH`` for ``ConfigService`` to pick up, and ``-v``
(repeatable count) maps to the appropriate ``structlog`` log level.
- **Add regression test for ActorRegistry.add() nested provider/model extraction** (#4321):
Added BDD regression test confirming that provider and model are correctly
extracted from nested `actors.<name>.config` blocks when `type` is only at
the nested level and `name` uses the `local/` namespace prefix. This
scenario was previously fixed in #4300; the test ensures the fix remains
in place and prevents future regressions.
- **Actor configuration validation incorrectly requires top-level provider field** (#4300):
Actor configuration in V3 is now obtained from the nested configuration
parameter, according to the specification.
@@ -327,6 +310,16 @@
### Fixed
- **`invariant_enforced` decisions not propagated to child plans on subplan spawn** (#9131):
Fixed `SubplanService.spawn()` to propagate all `invariant_enforced` decisions from the
parent plan's decision tree to each child plan's decision tree. Previously, child plans
started Strategize with a completely empty invariant set, violating the spec requirement:
"recorded as `invariant_enforced` decisions that propagate to child plans." The fix adds
a `_propagate_invariant_decisions()` helper that re-records each parent
`invariant_enforced` decision on the child plan, including `non_overridable` global
invariants. BDD regression coverage added in
`features/tdd_invariant_propagation_subplan.feature`.
- **fix(repositories): derive PlanResult.success from result_success column instead of error_message** (#7501):
Fixed a critical bug in `PlanRepository._to_domain` where `PlanResult.success` was incorrectly
derived from `error_message is None`. Because `error_message` is shared between the build phase
@@ -915,5 +908,4 @@
- **TUI -- Permission Question Widget**: A new inline `PermissionQuestionWidget`
renders permission requests directly in the conversation stream for single-file
operations. Users can allow/reject with single-key shortcuts (`a`/`A`/`r`/`R`),
navigate with arrow keys, confirm with `Enter`, or press `v` to open the full
navigate with arrow keys, confirm with `Enter`, or press `v` to open the full
+6
View File
@@ -8,6 +8,12 @@
* Luis Mendes <luis.p.mendes@gmail.com>
* Rui Hu <rui.hu@cleverthis.com>
## Contributors
### Code Contributions
- **[asyncio and Deprecation Fixes](https://git.cleverthis.com/cleveragents/cleveragents-core/issues/11134)** - Replaced deprecated `asyncio.get_event_loop()` calls with `get_running_loop()` + safe fallback, updated `datetime.utcnow()` usage to `datetime.now(timezone.utc)`, and added PYTHONWARNINGS safety net in nox session. Fixed seven failing BDD scenarios under Python 3.13 (issue #11134).
# Details
Below are some of the specific details of various contributions.
+8 -1
View File
@@ -116,7 +116,10 @@ def step_impl(context):
graph.execute = exec_with_messages
executor = bridge._create_graph_executor({"graph": "test_graph"})
loop = asyncio.get_event_loop()
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
def _run_operator(message):
results = []
@@ -146,6 +149,10 @@ def step_impl(context):
except ValueError:
context.results.append("graph_executor_error")
# Close the fresh event loop to prevent resource leak
with __import__("contextlib").suppress(RuntimeError):
loop.close() # Safe: get_running_loop catches if another loop already exists
@when("I exercise state utilities and node operator")
def step_impl(context):
@@ -2,6 +2,7 @@
from __future__ import annotations
import asyncio
import logging
import tempfile
@@ -29,7 +30,10 @@ def _write_config_file(data: dict[str, Any]) -> Path:
def _run_async(coro):
try:
loop = asyncio.get_event_loop()
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
if loop.is_closed():
raise RuntimeError
if loop.is_running():
+25 -5
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import time
import asyncio
from pathlib import Path
@@ -153,7 +154,10 @@ def _ensure_topological_levels(graph):
def _run_async(coro):
try:
loop = asyncio.get_event_loop()
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
if loop.is_closed():
raise RuntimeError
if loop.is_running():
@@ -435,7 +439,7 @@ def step_graph_route_idle_time(context):
@given("I have a graph state that has been idle too long")
def step_graph_state_idle(context):
context.graph_state = GraphState()
current_time = asyncio.get_event_loop().time()
current_time = time.time()
context.graph_state.metadata = {"last_updated": current_time - 10.0}
context.graph_state.messages = []
@@ -673,7 +677,11 @@ def step_attempt_downgrade(context):
)
return
try:
context.downgrade_result = asyncio.get_event_loop().run_until_complete(
try:
_loop_downgrade_result = asyncio.get_running_loop()
except RuntimeError:
_loop_downgrade_result = asyncio.new_event_loop()
context.downgrade_result = _loop_downgrade_result.run_until_complete(
context.route_bridge.downgrade_graph_to_stream(
context.route_config, context.langgraph
)
@@ -709,7 +717,11 @@ def step_route_upgrade_downgrade(context):
@when("I perform an upgrade and then downgrade cycle")
def step_cycle_upgrade_downgrade(context):
context.langgraph = asyncio.get_event_loop().run_until_complete(
try:
_loop_langgraph = asyncio.get_running_loop()
except RuntimeError:
_loop_langgraph = asyncio.new_event_loop()
context.langgraph = _loop_langgraph.run_until_complete(
context.route_bridge.upgrade_stream_to_graph(
context.route_config, context.stream_message
)
@@ -731,7 +743,15 @@ def step_cycle_upgrade_downgrade(context):
name=name, type=getattr(node, "type", NodeType.FUNCTION)
)
context.stream_config = asyncio.get_event_loop().run_until_complete(
try:
_loop_stream_config = asyncio.get_running_loop()
except RuntimeError:
_loop_stream_config = asyncio.new_event_loop()
context.stream_config = _loop_stream_config.run_until_complete(
context.route_bridge.downgrade_graph_to_stream(
RouteConfig.from_graph_config(
GraphConfig(
+11 -3
View File
@@ -54,9 +54,17 @@ class Agent(ABC):
def process_message_sync(
self, message: Any, context: dict[str, Any] | None = None
) -> Any:
return asyncio.get_event_loop().run_until_complete(
self.process_message(message, context or {})
)
try:
_loop = asyncio.get_running_loop()
except RuntimeError:
_loop = asyncio.new_event_loop()
try:
return _loop.run_until_complete(
self.process_message(message, context or {})
)
finally:
if not _loop.is_running():
_loop.close()
@abstractmethod
async def process_message(
+4 -1
View File
@@ -224,7 +224,10 @@ class RxPyLangGraphBridge:
)
def create_future_task(msg: StreamMessage) -> asyncio.Future[StreamMessage]:
loop = asyncio.get_event_loop()
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
task: asyncio.Future[StreamMessage] = asyncio.ensure_future(
execute_graph(msg), loop=loop
)
+8 -2
View File
@@ -106,7 +106,10 @@ class Node: # pylint: disable=too-many-instance-attributes
async def execute(self, state: GraphState) -> dict[str, Any]: # pylint: disable=too-many-branches
self.execution_count += 1
loop = asyncio.get_event_loop()
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
start_time = loop.time()
try:
state_updates = {"current_node": self.name}
@@ -271,7 +274,10 @@ class Node: # pylint: disable=too-many-instance-attributes
if asyncio.iscoroutinefunction(fn):
result = await fn(state)
else:
loop = asyncio.get_event_loop()
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
result = await loop.run_in_executor(None, lambda: fn(state))
if asyncio.iscoroutine(result):
result = await result
+2 -2
View File
@@ -2,8 +2,8 @@
from __future__ import annotations
import asyncio
import logging
import time
from typing import Any
from rx.scheduler.eventloop import AsyncIOScheduler # type: ignore[attr-defined]
@@ -86,7 +86,7 @@ class RouteBridge:
last_updated = state.metadata.get("last_updated")
if (
last_updated
and (asyncio.get_event_loop().time() - last_updated) > idle_threshold
and (time.time() - last_updated) > idle_threshold
):
return True
if (