diff --git a/gateway/run.py b/gateway/run.py index abe941fb1cc..4b285287b22 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -2332,6 +2332,12 @@ def _normalize_empty_agent_response( Consolidates the existing ``failed`` handler and adds a catch-all for the case where the agent did work (api_calls > 0) but returned no text. Fix for #18765. + + Also surfaces a retry hint when the agent never ran at all + (api_calls == 0) for a non-interrupted, non-failed turn -- this is the + silent-drop pattern observed after ``/stop`` where the next user + message hits a stale generation token and returns an empty result, + leaving the platform with nothing to send. (#31884) """ if response: return response @@ -2364,6 +2370,22 @@ def _normalize_empty_agent_response( "This may be a transient error — try sending your message again." ) + # api_calls == 0, not failed, not interrupted: the agent never ran for + # this turn. This is the post-/stop generation-race pattern where the + # gateway would otherwise silently drop the turn (response=0 chars) and + # the user sees no reply at all. Surface a short retry hint so the + # message isn't lost in silence. (#31884) + if ( + api_calls == 0 + and not agent_result.get("interrupted") + and not agent_result.get("failed") + and not agent_result.get("partial") + ): + return ( + "⚠️ Your message wasn't processed (the previous turn was still " + "being cleaned up). Please send it again." + ) + return response diff --git a/tests/test_lazy_session_regressions.py b/tests/test_lazy_session_regressions.py index 0c1ea022064..1e5a75206e1 100644 --- a/tests/test_lazy_session_regressions.py +++ b/tests/test_lazy_session_regressions.py @@ -415,6 +415,31 @@ class TestGatewaySurfacesNullResponse: assert result == "Hello!" + def test_silent_drop_after_stop_surfaces_hint(self): + """Regression for #31884: after /stop, the next user message hits a + stale generation token in _run_agent and returns with api_calls=0, + no failure, no interruption. Without normalization the gateway + silently drops the turn (response=0 chars). Surface a retry hint + so the user knows the message was lost.""" + from gateway.run import _normalize_empty_agent_response + + agent_result = { + "final_response": "", + "api_calls": 0, + "failed": False, + "interrupted": False, + "partial": False, + } + + response = agent_result.get("final_response") or "" + result = _normalize_empty_agent_response( + agent_result, response, history_len=10, + ) + + assert result, "Silent-drop turn must surface a user-facing hint" + lowered = result.lower() + assert "send it again" in lowered or "try again" in lowered + # =========================================================================== # Prune: finalize_orphaned_compression_sessions