refactor(hermes_cli/web_server): migrate asyncio.get_event_loop() to get_running_loop()#1
refactor(hermes_cli/web_server): migrate asyncio.get_event_loop() to get_running_loop()#1Kewe63 wants to merge 1 commit into
Conversation
…get_running_loop() Single remaining deprecated call at _start_device_code_flow L6173 (MiniMax PKCE device-flow boot). Outer function is async; get_running_loop() is the direct drop-in replacement. Inner closure _do_minimax_request is correctly sync — run_in_executor bridges from the async caller to a thread-pool worker. get_running_loop() is only invoked from the async caller, so the deprecation-free replacement is safe. Note: this file already uses asyncio.get_running_loop() in 8 other places (Nous device flow was fixed separately). This PR cleans up the last deprecated site (MiniMax PKCE). Tests pin: - zero remaining asyncio.get_event_loop() sites in this file; - the migrated site sits inside async def _start_device_code_flow (walked past nested sync closures to the outer async def); - the import time is still present (used by time.time() for expires_at math). No production behaviour change. Per memory rule: per-file split. Following fix/lsp-client-time-monotonic-deprecation. Remaining files in the deprecation sweep will follow in separate PRs.
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
PR Summary by QodoReplace deprecated asyncio.get_event_loop() in MiniMax device flow Description
Diagram
High-Level Assessment
Files changed (2)
|
🔎 Lint report:
|
Code Review by Qodo
1. Source-inspection tests added
|
| REPO_ROOT = Path(__file__).resolve().parent.parent.parent | ||
| WEB_SERVER = REPO_ROOT / "hermes_cli" / "web_server.py" | ||
|
|
||
|
|
||
| def _read_web_server() -> str: | ||
| assert WEB_SERVER.is_file(), f"missing {WEB_SERVER}" | ||
| return WEB_SERVER.read_text(encoding="utf-8") | ||
|
|
||
|
|
||
| def test_get_event_loop_removed() -> None: | ||
| """No ``asyncio.get_event_loop()`` should remain in | ||
| hermes_cli/web_server.py — the deprecation must be fully resolved | ||
| in this file before siblings follow.""" | ||
| content = _read_web_server() | ||
| matches = re.findall(r"asyncio\.get_event_loop\(", content) | ||
| assert matches == [], ( | ||
| f"Found {len(matches)} remaining `asyncio.get_event_loop(` " | ||
| f"call(s) in hermes_cli/web_server.py. The 3.10+ deprecation " | ||
| f"must be fully resolved." | ||
| ) | ||
|
|
||
|
|
||
| def _find_outer_async_def( | ||
| content: str, line_idx: int, max_lookback: int = 500 | ||
| ) -> tuple[int | None, str | None]: | ||
| """Walk backwards from ``line_idx`` to find the *outer* ``async def``. | ||
| Inner ``def`` (sync closures) are skipped — they are nested | ||
| structural blocks but the loop-policy question is decided by the | ||
| nearest enclosing async def. | ||
|
|
||
| Returns (line_number_1based, function_name) or (None, None) if not | ||
| found within ``max_lookback`` lines. | ||
| """ | ||
| lines = content.split("\n") | ||
| for i in range(line_idx - 1, max(0, line_idx - max_lookback), -1): | ||
| m = re.match(r"^(\s*)(async )?def (\w+)", lines[i]) | ||
| if m: | ||
| async_kw = m.group(2) | ||
| name = m.group(3) | ||
| if async_kw: | ||
| return i + 1, name | ||
| return None, None | ||
|
|
||
|
|
||
| def test_migrated_site_in_async_def() -> None: | ||
| """The migrated ``asyncio.get_running_loop().run_in_executor(...)`` | ||
| call site at the MiniMax PKCE device-flow boot must sit inside | ||
| ``async def _start_device_code_flow``. If the call moves to a | ||
| sync caller the call would raise RuntimeError because there is | ||
| no running loop in a sync context.""" | ||
| content = _read_web_server() | ||
| lines = content.split("\n") | ||
|
|
||
| # The migrated site is multi-line: `await ... .run_in_executor(` | ||
| # spans two lines, and ``_do_minimax_request`` is the function arg | ||
| # on the next line. Walk forward from each ``run_in_executor(`` start | ||
| # and accept the call if a MiniMax marker appears in the next 5 | ||
| # lines. | ||
| migrated_sites = [] | ||
| for i, line in enumerate(lines): | ||
| if "await asyncio.get_running_loop().run_in_executor(" not in line: | ||
| continue | ||
| ctx = "\n".join(lines[i : min(i + 6, len(lines))]) | ||
| if "minimax" not in ctx.lower(): | ||
| continue | ||
| migrated_sites.append(i) | ||
| assert len(migrated_sites) >= 1, ( | ||
| "expected at least one `await asyncio.get_running_loop().run_in_executor` " | ||
| "site whose next 5 lines mention `minimax` (the MiniMax PKCE boot — " | ||
| "the only deprecation fix in this PR)." | ||
| ) | ||
|
|
||
| idx = migrated_sites[0] | ||
| owner_line, owner_name = _find_outer_async_def(content, idx) | ||
| assert owner_name is not None and owner_line is not None, ( | ||
| f"migrated MiniMax site at line {idx + 1} has no outer async def " | ||
| f"within 500 lines. _start_device_code_flow is async — this " | ||
| f"site must live inside it." | ||
| ) | ||
| assert owner_name == "_start_device_code_flow", ( | ||
| f"migrated MiniMax site at line {idx + 1} is inside `{owner_name}()`, " | ||
| f"expected `_start_device_code_flow`." | ||
| ) | ||
| decl = lines[owner_line - 1] | ||
| assert decl.lstrip().startswith("async def"), ( | ||
| f"{owner_name} at line {owner_line} is not declared async: {decl.rstrip()!r}" | ||
| ) | ||
|
|
||
|
|
||
| def test_time_module_already_imported() -> None: | ||
| """Sanity guard: ``time`` is used elsewhere in this file | ||
| (e.g. time.time() at line ~6091 in _start_device_code_flow) so | ||
| the import should already exist.""" | ||
| content = _read_web_server() | ||
| assert re.search(r"^import time\b", content, re.MULTILINE), ( | ||
| "hermes_cli/web_server.py no longer imports `time` — the " | ||
| "device-code flow uses time.time() to compute expires_at; " | ||
| "removing the import would crash the flow." | ||
| ) |
There was a problem hiding this comment.
1. Source-inspection tests added 📜 Skill insight ▣ Testability
The new regression tests validate the event-loop migration by reading and parsing hermes_cli/web_server.py as raw text and asserting on exact strings/lexical structure, which makes them brittle to harmless refactors that preserve behavior. This conflicts with the requirement that tests verify observable behavior rather than internal implementation details and can cause false CI failures and extra maintenance.
Agent Prompt
## Issue description
The current regression tests assert the event-loop deprecation migration by reading `hermes_cli/web_server.py` as text and matching exact strings/structure (including substring checks and a non-AST regex scan to determine the enclosing async function). This couples the tests to formatting and specific implementation choices, so benign refactors (e.g., storing the running loop in a local variable before calling `run_in_executor`) can break CI even when behavior is unchanged, and it also violates the requirement that tests verify observable behavior rather than internal implementation details.
## Issue Context
Compliance requires tests to assert on observable behavior (outputs, warnings/exceptions, state changes) instead of internal implementation details. The real invariants being guarded are semantic: `asyncio.get_event_loop()` should not be invoked in `hermes_cli/web_server.py`, and the `run_in_executor` usage should be driven by a *running* loop from within an async context (currently within `_start_device_code_flow`), without the test being dependent on exact source formatting.
## Fix Focus Areas
- tests/hermes_cli/test_web_server_event_loop_migration.py[34-132]
- hermes_cli/web_server.py[6045-6054]
- hermes_cli/web_server.py[6160-6176]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
…ture get_copilot_api_token now returns (api_token, base_url); the auth-remove suppression test still mocked it as a bare string, mis-unpacking into the credential-pool seed path and failing with 'No credential #1'.
…_id signature churn Two independent bugs evicted the cached gateway AIAgent on every turn, preventing the prompt cache from ever warming: 1. Model normalization mismatch: the post-run fallback-eviction check compared _agent.model (stripped in AIAgent.__init__) against the raw _resolve_gateway_model() config string. For vendor-prefixed config on native providers (e.g. 'deepseek/deepseek-v4-pro' vs 'deepseek-v4-pro') this was always unequal, so the agent was evicted after every successful run. Normalize _cfg_model the same way (skip aggregators). 2. Discord triggering message_id leaked into the cached system prompt via build_session_context_prompt()'s Discord IDs block. message_id changes every turn, so the agent-cache signature (computed from the ephemeral prompt) changed every Discord turn -> rebuild every message. The id is now injected per-turn into the user message (where per-turn content belongs and does not touch the cache signature); the cached IDs block carries a static pointer to it, preserving reply/react/pin via the discord tools. Adapted from NousResearch#28846. Bug #1 fix is the contributor's; bug NousResearch#2 reworked to be non-destructive (keeps the triggering-id capability instead of deleting it). Redundant auto-reset eviction (already on main via NousResearch#9893/NousResearch#48031) and the wrong-premise reset_context_note plumbing from the original PR were dropped. Co-authored-by: Hermes Agent <hermes@nousresearch.com>



Summary
Single remaining deprecated call at
_start_device_code_flowL6173 (MiniMax PKCE device-flow boot). Outer function is async;get_running_loop()is the direct drop-in replacement. Inner closure_do_minimax_requestis correctly sync —run_in_executorbridges from the async caller to a thread-pool worker.get_running_loop()is only invoked from the async caller, so the deprecation-free replacement is safe.Note: this file already uses
asyncio.get_running_loop()in 8 other places (Nous device flow was fixed separately). This PR cleans up the last deprecated site (MiniMax PKCE).Tests
New
tests/hermes_cli/test_web_server_event_loop_migration.py(3 tests, all passing):test_get_event_loop_removed— pins zero remaining deprecated sites in this filetest_migrated_site_in_async_def— site sits insideasync def _start_device_code_flow(walks past nested sync closures like_do_minimax_requestto find the outer async def)test_time_module_already_imported— guards againstNameErrorifimport timeis droppeduv run --frozen pytest tests/hermes_cli/test_web_server_event_loop_migration.py -v → 3 passed in 0.19s
ruff checkandruff formatclean on both files.Files Changed
hermes_cli/web_server.py | 2 +-
tests/hermes_cli/test_web_server_event_loop_migration.py | 132 +++++++++++++ (new)
Per-File Split (memory rule)
Following
fix/lsp-client-time-monotonic-deprecation. Remaining files in the deprecation sweep:These will follow in separate PRs after this one merges.
No issue filed upstream — code-hunt find (file-area coverage analysis flagged
hermes_cli/web_server.pyas a high-density deprecated-API zone).