diff --git a/gateway/run.py b/gateway/run.py index cfb4af82e..d360d453f 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -8458,6 +8458,12 @@ class GatewayRunner: cached = _cache.get(session_key) if cached and cached[1] == _sig: agent = cached[0] + # Reset activity timestamp so the inactivity timeout + # handler doesn't see stale idle time from the previous + # turn and immediately kill this agent. (#9051) + agent._last_activity_ts = time.time() + agent._last_activity_desc = "starting new turn (cached)" + agent._api_call_count = 0 logger.debug("Reusing cached agent for session %s", session_key) if agent is None: diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index 58d9f92ed..6d46bdde6 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -715,7 +715,9 @@ def _detect_venv_dir() -> Path | None: """Detect the active virtualenv directory. Checks ``sys.prefix`` first (works regardless of the directory name), - then falls back to probing common directory names under PROJECT_ROOT. + then ``VIRTUAL_ENV`` env var (covers uv-managed environments where + sys.prefix == sys.base_prefix), then falls back to probing common + directory names under PROJECT_ROOT. Returns ``None`` when no virtualenv can be found. """ # If we're running inside a virtualenv, sys.prefix points to it. @@ -724,6 +726,15 @@ def _detect_venv_dir() -> Path | None: if venv.is_dir(): return venv + # uv and some other tools set VIRTUAL_ENV without changing sys.prefix. + # This catches `uv run` where sys.prefix == sys.base_prefix but the + # environment IS a venv. (#8620) + _virtual_env = os.environ.get("VIRTUAL_ENV") + if _virtual_env: + venv = Path(_virtual_env) + if venv.is_dir(): + return venv + # Fallback: check common virtualenv directory names under the project root. for candidate in (".venv", "venv"): venv = PROJECT_ROOT / candidate diff --git a/run_agent.py b/run_agent.py index e98a6e798..55b4efaa6 100644 --- a/run_agent.py +++ b/run_agent.py @@ -7858,6 +7858,7 @@ class AIAgent: self._incomplete_scratchpad_retries = 0 self._codex_incomplete_retries = 0 self._thinking_prefill_retries = 0 + self._post_tool_empty_retried = False self._last_content_with_tools = None self._mute_post_response = False self._unicode_sanitization_passes = 0 @@ -10131,6 +10132,10 @@ class AIAgent: if _had_prefill: self._thinking_prefill_retries = 0 self._empty_content_retries = 0 + # Successful tool execution — reset the post-tool nudge + # flag so it can fire again if the model goes empty on + # a LATER tool round. + self._post_tool_empty_retried = False messages.append(assistant_msg) self._emit_interim_assistant_message(assistant_msg) @@ -10299,6 +10304,48 @@ class AIAgent: self._response_was_previewed = True break + # ── Post-tool-call empty response nudge ─────────── + # The model returned empty after executing tool calls + # but there's no prior-turn content to fall back on. + # Instead of giving up, nudge the model to continue by + # appending a user-level hint. This is the #9400 case: + # weaker models (GLM-5, etc.) sometimes return empty + # after tool results instead of continuing to the next + # step. One retry with a nudge usually fixes it. + _prior_was_tool = any( + m.get("role") == "tool" + for m in messages[-5:] # check recent messages + ) + if ( + _prior_was_tool + and not getattr(self, "_post_tool_empty_retried", False) + ): + self._post_tool_empty_retried = True + logger.info( + "Empty response after tool calls — nudging model " + "to continue processing" + ) + self._emit_status( + "⚠️ Model returned empty after tool calls — " + "nudging to continue" + ) + # Append the empty assistant message first so the + # message sequence stays valid: + # tool(result) → assistant("(empty)") → user(nudge) + # Without this, we'd have tool → user which most + # APIs reject as an invalid sequence. + assistant_msg["content"] = "(empty)" + messages.append(assistant_msg) + messages.append({ + "role": "user", + "content": ( + "You just executed tool calls but returned an " + "empty response. Please process the tool " + "results above and continue with the task." + ), + }) + continue + # ── Thinking-only prefill continuation ────────── # The model produced structured reasoning (via API # fields) but no visible text content. Rather than