From 8121aef83c4d15b82315936544a77d4040fc1e96 Mon Sep 17 00:00:00 2001 From: teknium1 Date: Thu, 12 Mar 2026 15:48:34 -0700 Subject: [PATCH] fix: eliminate execute_code progress spam on gateway platforms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: two issues combined to create visual spam on Telegram/Discord: 1. build_tool_preview() preserved newlines from tool arguments. A preview like 'import os\nprint("...")' rendered as 2+ visual lines per progress entry on messaging platforms. This affected execute_code most (code always has newlines), but could also hit terminal, memory, send_message, session_search, and process tools. 2. No deduplication of identical progress messages. When models iterate with execute_code using the same boilerplate code (common pattern), each call produced an identical progress line. 9 calls x 2 visual lines = 18 lines of identical spam in one message bubble. Fixes: - Added _oneline() helper to collapse all whitespace (newlines, tabs) to single spaces. Applied to ALL code paths in build_tool_preview() — both the generic path and every early-return path that touches user content (memory, session_search, send_message, process). - Added dedup in gateway progress_callback: consecutive identical messages are collapsed with a repeat counter, e.g. 'execute_code: ... (x9)' instead of 9 identical lines. The send_progress_messages async loop handles dedup tuples by updating the last progress_line in-place. --- agent/display.py | 19 ++++++++++++------- gateway/run.py | 36 ++++++++++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/agent/display.py b/agent/display.py index bd1367a3..6b8b88b5 100644 --- a/agent/display.py +++ b/agent/display.py @@ -63,6 +63,11 @@ def get_skin_tool_prefix() -> str: # Tool preview (one-line summary of a tool call's primary argument) # ========================================================================= +def _oneline(text: str) -> str: + """Collapse whitespace (including newlines) to single spaces.""" + return " ".join(text.split()) + + def build_tool_preview(tool_name: str, args: dict, max_len: int = 40) -> str: """Build a short preview of a tool call's primary argument for display.""" if not args: @@ -89,7 +94,7 @@ def build_tool_preview(tool_name: str, args: dict, max_len: int = 40) -> str: if sid: parts.append(sid[:16]) if data: - parts.append(f'"{data[:20]}"') + parts.append(f'"{_oneline(data[:20])}"') if timeout_val and action == "wait": parts.append(f"{timeout_val}s") return " ".join(parts) if parts else None @@ -105,24 +110,24 @@ def build_tool_preview(tool_name: str, args: dict, max_len: int = 40) -> str: return f"planning {len(todos_arg)} task(s)" if tool_name == "session_search": - query = args.get("query", "") + query = _oneline(args.get("query", "")) return f"recall: \"{query[:25]}{'...' if len(query) > 25 else ''}\"" if tool_name == "memory": action = args.get("action", "") target = args.get("target", "") if action == "add": - content = args.get("content", "") + content = _oneline(args.get("content", "")) return f"+{target}: \"{content[:25]}{'...' if len(content) > 25 else ''}\"" elif action == "replace": - return f"~{target}: \"{args.get('old_text', '')[:20]}\"" + return f"~{target}: \"{_oneline(args.get('old_text', '')[:20])}\"" elif action == "remove": - return f"-{target}: \"{args.get('old_text', '')[:20]}\"" + return f"-{target}: \"{_oneline(args.get('old_text', '')[:20])}\"" return action if tool_name == "send_message": target = args.get("target", "?") - msg = args.get("message", "") + msg = _oneline(args.get("message", "")) if len(msg) > 20: msg = msg[:17] + "..." return f"to {target}: \"{msg}\"" @@ -156,7 +161,7 @@ def build_tool_preview(tool_name: str, args: dict, max_len: int = 40) -> str: if isinstance(value, list): value = value[0] if value else "" - preview = str(value).strip() + preview = _oneline(str(value)) if not preview: return None if len(preview) > max_len: diff --git a/gateway/run.py b/gateway/run.py index dfd1e4c2..43697f3b 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -2916,6 +2916,8 @@ class GatewayRunner: # Queue for progress messages (thread-safe) progress_queue = queue.Queue() if tool_progress_enabled else None last_tool = [None] # Mutable container for tracking in closure + last_progress_msg = [None] # Track last message for dedup + repeat_count = [0] # How many times the same message repeated def progress_callback(tool_name: str, preview: str = None, args: dict = None): """Callback invoked by agent when a tool is called.""" @@ -2988,6 +2990,18 @@ class GatewayRunner: else: msg = f"{emoji} {tool_name}..." + # Dedup: collapse consecutive identical progress messages. + # Common with execute_code where models iterate with the same + # code (same boilerplate imports → identical previews). + if msg == last_progress_msg[0]: + repeat_count[0] += 1 + # Update the last line in progress_lines with a counter + # via a special "dedup" queue message. + progress_queue.put(("__dedup__", msg, repeat_count[0])) + return + last_progress_msg[0] = msg + repeat_count[0] = 0 + progress_queue.put(msg) # Background task to send progress messages @@ -3008,8 +3022,17 @@ class GatewayRunner: while True: try: - msg = progress_queue.get_nowait() - progress_lines.append(msg) + raw = progress_queue.get_nowait() + + # Handle dedup messages: update last line with repeat counter + if isinstance(raw, tuple) and len(raw) == 3 and raw[0] == "__dedup__": + _, base_msg, count = raw + if progress_lines: + progress_lines[-1] = f"{base_msg} (×{count + 1})" + msg = progress_lines[-1] if progress_lines else base_msg + else: + msg = raw + progress_lines.append(msg) if can_edit and progress_msg_id is not None: # Try to edit the existing progress message @@ -3045,8 +3068,13 @@ class GatewayRunner: # Drain remaining queued messages while not progress_queue.empty(): try: - msg = progress_queue.get_nowait() - progress_lines.append(msg) + raw = progress_queue.get_nowait() + if isinstance(raw, tuple) and len(raw) == 3 and raw[0] == "__dedup__": + _, base_msg, count = raw + if progress_lines: + progress_lines[-1] = f"{base_msg} (×{count + 1})" + else: + progress_lines.append(raw) except Exception: break # Final edit with all remaining tools (only if editing works)