From 60b67e2b476ef8b4f70e9fa1b3447fff73a95045 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:23:07 -0700 Subject: [PATCH] fix(gateway): cap interrupt recursion depth to prevent resource exhaustion (#816) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: prevent infinite 400 failure loop on context overflow (#1630) When a gateway session exceeds the model's context window, Anthropic may return a generic 400 invalid_request_error with just 'Error' as the message. This bypassed the phrase-based context-length detection, causing the agent to treat it as a non-retryable client error. Worse, the failed user message was still persisted to the transcript, making the session even larger on each attempt — creating an infinite loop. Three-layer fix: 1. run_agent.py — Fallback heuristic: when a 400 error has a very short generic message AND the session is large (>40% of context or >80 messages), treat it as a probable context overflow and trigger compression instead of aborting. 2. run_agent.py + gateway/run.py — Don't persist failed messages: when the agent returns failed=True before generating any response, skip writing the user's message to the transcript/DB. This prevents the session from growing on each failure. 3. gateway/run.py — Smarter error messages: detect context-overflow failures and suggest /compact or /reset specifically, instead of a generic 'try again' that will fail identically. * fix(skills): detect prompt injection patterns and block cache file reads Adds two security layers to prevent prompt injection via skills hub cache files (#1558): 1. read_file: blocks direct reads of ~/.hermes/skills/.hub/ directory (index-cache, catalog files). The 3.5MB clawhub_catalog_v1.json was the original injection vector — untrusted skill descriptions in the catalog contained adversarial text that the model executed. 2. skill_view: warns when skills are loaded from outside the trusted ~/.hermes/skills/ directory, and detects common injection patterns in skill content ("ignore previous instructions", "", etc.). Cherry-picked from PR #1562 by ygd58. * fix(tools): chunk long messages in send_message_tool before dispatch (#1552) Long messages sent via send_message tool or cron delivery silently failed when exceeding platform limits. Gateway adapters handle this via truncate_message(), but the standalone senders in send_message_tool bypassed that entirely. - Apply truncate_message() chunking in _send_to_platform() before dispatching to individual platform senders - Remove naive message[i:i+2000] character split in _send_discord() in favor of centralized smart splitting - Attach media files to last chunk only for Telegram - Add regression tests for chunking and media placement Cherry-picked from PR #1557 by llbn. * fix(approval): show full command in dangerous command approval (#1553) Previously the command was truncated to 80 chars in CLI (with a [v]iew full option), 500 chars in Discord embeds, and missing entirely in Telegram/Slack approval messages. Now the full command is always displayed everywhere: - CLI: removed 80-char truncation and [v]iew full menu option - Gateway (TG/Slack): approval_required message includes full command in a code block - Discord: embed shows full command up to 4096-char limit - Windows: skip SIGALRM-based test timeout (Unix-only) - Updated tests: replaced view-flow tests with direct approval tests Cherry-picked from PR #1566 by crazywriter1. * fix(cli): flush stdout during agent loop to prevent macOS display freeze (#1624) The interrupt polling loop in chat() waited on the queue without invalidating the prompt_toolkit renderer. On macOS, the StdoutProxy buffer only flushed on input events, causing the CLI to appear frozen during tool execution until the user typed a key. Fix: call _invalidate() on each queue timeout (every ~100ms, throttled to 150ms) to force the renderer to flush buffered agent output. * fix(claw): warn when API keys are skipped during OpenClaw migration (#1580) When --migrate-secrets is not passed (the default), API keys like OPENROUTER_API_KEY are silently skipped with no warning. Users don't realize their keys weren't migrated until the agent fails to connect. Add a post-migration warning with actionable instructions: either re-run with --migrate-secrets or add the key manually via hermes config set. Cherry-picked from PR #1593 by ygd58. * fix(security): block sandbox backend creds from subprocess env (#1264) Add Modal and Daytona sandbox credentials to the subprocess env blocklist so they're not leaked to agent terminal sessions via printenv/env. Cherry-picked from PR #1571 by ygd58. * fix(gateway): cap interrupt recursion depth to prevent resource exhaustion (#816) When a user sends multiple messages while the agent keeps failing, _run_agent() calls itself recursively with no depth limit. This can exhaust stack/memory if the agent is in a failure loop. Add _MAX_INTERRUPT_DEPTH = 3. When exceeded, the pending message is logged and the current result is returned instead of recursing deeper. The log handler duplication bug described in #816 was already fixed separately (AIAgent.__init__ deduplicates handlers). --------- Co-authored-by: buray Co-authored-by: lbn Co-authored-by: crazywriter1 <53251494+crazywriter1@users.noreply.github.com> --- gateway/run.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/gateway/run.py b/gateway/run.py index 6b3a586e7..8e702c714 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -3956,6 +3956,8 @@ class GatewayRunner: logger.debug("Process watcher ended: %s", session_id) + _MAX_INTERRUPT_DEPTH = 3 # Cap recursive interrupt handling (#816) + async def _run_agent( self, message: str, @@ -3963,7 +3965,8 @@ class GatewayRunner: history: List[Dict[str, Any]], source: SessionSource, session_id: str, - session_key: str = None + session_key: str = None, + _interrupt_depth: int = 0, ) -> Dict[str, Any]: """ Run the agent with the given message and context. @@ -4552,6 +4555,20 @@ class GatewayRunner: if adapter and hasattr(adapter, '_active_sessions') and session_key and session_key in adapter._active_sessions: adapter._active_sessions[session_key].clear() + # Cap recursion depth to prevent resource exhaustion when the + # user sends multiple messages while the agent keeps failing. (#816) + if _interrupt_depth >= self._MAX_INTERRUPT_DEPTH: + logger.warning( + "Interrupt recursion depth %d reached for session %s — " + "queueing message instead of recursing.", + _interrupt_depth, session_key, + ) + # Queue the pending message for normal processing on next turn + adapter = self.adapters.get(source.platform) + if adapter and hasattr(adapter, 'queue_message'): + adapter.queue_message(session_key, pending) + return result_holder[0] or {"final_response": response, "messages": history} + # Don't send the interrupted response to the user — it's just noise # like "Operation interrupted." They already know they sent a new # message, so go straight to processing it. @@ -4564,7 +4581,8 @@ class GatewayRunner: history=updated_history, source=source, session_id=session_id, - session_key=session_key + session_key=session_key, + _interrupt_depth=_interrupt_depth + 1, ) finally: # Stop progress sender and interrupt monitor