fix(gateway): surface retry hint instead of silently dropping turn after /stop (#31884)

After /stop, the next user message can hit a stale generation token and
return with api_calls=0, no failure, no interruption. _normalize_empty_agent_response
fell through to an empty string, so the gateway logged "response=0 chars"
and sent nothing — the message was silently lost while internal work
sometimes continued.

Add the api_calls==0 / not-failed / not-interrupted / not-partial branch
to the single normalization chokepoint so the user gets a short retry hint
instead of silence. Regression test asserts the hint surfaces.

Salvaged from #33851 (re-applied on current main; original was 1401 commits
behind and the function had moved).
This commit is contained in:
sweetcornna 2026-06-24 23:51:31 +05:30 committed by kshitijk4poor
parent 35e9c63d89
commit b41d9b845d
2 changed files with 47 additions and 0 deletions

View file

@ -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

View file

@ -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