diff --git a/gateway/run.py b/gateway/run.py index 545b9c05b31..2bd014fb74d 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -17611,9 +17611,14 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew ) if not final_response: - error_msg = f"⚠️ {result['error']}" if result.get("error") else "" + final_response = _normalize_empty_agent_response( + result, final_response or "", history_len=len(agent_history), + ) + final_response = _sanitize_gateway_final_response(source.platform, final_response) + if not final_response: + final_response = f"⚠️ {result['error']}" if result.get("error") else "" return { - "final_response": error_msg, + "final_response": final_response, "messages": result.get("messages", []), "api_calls": result.get("api_calls", 0), "failed": result.get("failed", False), diff --git a/scripts/release.py b/scripts/release.py index c42ee55e789..1f1e56add73 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -46,6 +46,7 @@ ACP_REGISTRY_MANIFEST = REPO_ROOT / "acp_registry" / "agent.json" # Auto-extracted from noreply emails + manual overrides AUTHOR_MAP = { "mac-studio@Fabios-Mac-Studio.local": "valenteff", # PR #53277 salvage (macOS launchd reload: retry bootstrap via _launchctl_bootstrap until launchctl-list confirms registration or the restart-drain window elapses; retry TimeoutExpired not just CalledProcessError; log persistent orphans) + "info@djimit.nl": "djimit", # PR #48034 salvage (normalize empty agent responses in the proxy/inner runner's not-final_response early return so gateways surface "⚠️ Processing stopped: … Try again." instead of a raw truncation error) "gary@bitcryptic.com": "bitcryptic-gw", # PR #53997 salvage (Matrix E2EE: resolve device_id via query_keys({mxid: []}) when whoami returns none; guard verification call sites so query_keys is never sent [null]; reset _device_id_unverified at connect() start; disconnect before reconnect) "7698789+abchiaravalle@users.noreply.github.com": "abchiaravalle", # PR #46997 salvage (recover resume_pending sessions: dual freshness signal + empty-turn safety net so restart auto-resume never sends a blank user turn) "swissly@users.noreply.github.com": "swissly", # PR #47167 salvage (wrap cron delivery thread-pool fallback in its own try/except so a per-target failure can't escape the except-RuntimeError block and crash the multi-target delivery loop; #47163) diff --git a/tests/gateway/test_run_progress_interrupt.py b/tests/gateway/test_run_progress_interrupt.py index de047e0fe29..b3cbe10438d 100644 --- a/tests/gateway/test_run_progress_interrupt.py +++ b/tests/gateway/test_run_progress_interrupt.py @@ -106,6 +106,29 @@ class InterruptedAgent: return {"final_response": "interrupted", "messages": [], "api_calls": 1} +class PartialTruncationAgent: + """Returns an incomplete turn with no visible assistant text.""" + + def __init__(self, **kwargs): + self.tool_progress_callback = kwargs.get("tool_progress_callback") + self.tools = [] + self._interrupt_requested = False + + @property + def is_interrupted(self) -> bool: + return self._interrupt_requested + + def run_conversation(self, message, conversation_history=None, task_id=None): + return { + "final_response": None, + "messages": [], + "api_calls": 2, + "completed": False, + "partial": True, + "error": "Response truncated due to output length limit", + } + + def _make_runner(adapter): gateway_run = importlib.import_module("gateway.run") GatewayRunner = gateway_run.GatewayRunner @@ -181,6 +204,19 @@ async def test_baseline_non_interrupted_agent_renders_progress(monkeypatch, tmp_ ) +@pytest.mark.asyncio +async def test_partial_empty_agent_response_is_normalized(monkeypatch, tmp_path): + """Messaging gateways should not echo raw truncation errors as final text.""" + adapter, result = await _run_once( + monkeypatch, tmp_path, PartialTruncationAgent, "sess-partial-empty" + ) + + assert result["final_response"].startswith("⚠️ Processing stopped:") + assert "Response truncated due to output length limit" in result["final_response"] + assert result["final_response"] != "⚠️ Response truncated due to output length limit" + assert result["partial"] is True + + @pytest.mark.asyncio async def test_progress_suppressed_when_agent_is_interrupted(monkeypatch, tmp_path): """Post-interrupt tool.started events must not render as bubbles.