From c2a7921f3bc8f96f3cd07c8777dc901655005cf9 Mon Sep 17 00:00:00 2001 From: Eris Date: Wed, 11 Mar 2026 16:00:25 +0800 Subject: [PATCH 1/2] fix: prevent logging handler accumulation in gateway mode Use exact Path comparison instead of endswith to detect existing errors.log handlers, avoiding false positives from similarly-named log files. --- run_agent.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/run_agent.py b/run_agent.py index b9bacf7d6..9b30d4f00 100644 --- a/run_agent.py +++ b/run_agent.py @@ -407,19 +407,30 @@ class AIAgent: # Persistent error log -- always writes WARNING+ to ~/.hermes/logs/errors.log # so tool failures, API errors, etc. are inspectable after the fact. - from agent.redact import RedactingFormatter - _error_log_dir = _hermes_home / "logs" - _error_log_dir.mkdir(parents=True, exist_ok=True) - _error_log_path = _error_log_dir / "errors.log" + # In gateway mode, each incoming message creates a new AIAgent instance, + # while the root logger is process-global. Re-adding the same errors.log + # handler would cause each warning/error line to be written multiple times. from logging.handlers import RotatingFileHandler - _error_file_handler = RotatingFileHandler( - _error_log_path, maxBytes=2 * 1024 * 1024, backupCount=2, + root_logger = logging.getLogger() + error_log_dir = _hermes_home / "logs" + error_log_path = error_log_dir / "errors.log" + resolved_error_log_path = error_log_path.resolve() + has_errors_log_handler = any( + isinstance(handler, RotatingFileHandler) + and Path(getattr(handler, "baseFilename", "")).resolve() == resolved_error_log_path + for handler in root_logger.handlers ) - _error_file_handler.setLevel(logging.WARNING) - _error_file_handler.setFormatter(RedactingFormatter( - '%(asctime)s %(levelname)s %(name)s: %(message)s', - )) - logging.getLogger().addHandler(_error_file_handler) + if not has_errors_log_handler: + from agent.redact import RedactingFormatter + error_log_dir.mkdir(parents=True, exist_ok=True) + error_file_handler = RotatingFileHandler( + error_log_path, maxBytes=2 * 1024 * 1024, backupCount=2, + ) + error_file_handler.setLevel(logging.WARNING) + error_file_handler.setFormatter(RedactingFormatter( + '%(asctime)s %(levelname)s %(name)s: %(message)s', + )) + root_logger.addHandler(error_file_handler) if self.verbose_logging: logging.basicConfig( From 806b79b5897b91d77c36390a5cf9bca3d55300ce Mon Sep 17 00:00:00 2001 From: teknium1 Date: Fri, 13 Mar 2026 23:56:51 -0700 Subject: [PATCH 2/2] test: cover errors.log handler reuse --- tests/test_run_agent.py | 60 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/tests/test_run_agent.py b/tests/test_run_agent.py index b20625450..8c9f7a2d5 100644 --- a/tests/test_run_agent.py +++ b/tests/test_run_agent.py @@ -6,13 +6,17 @@ are made. """ import json +import logging import re import uuid +from logging.handlers import RotatingFileHandler +from pathlib import Path from types import SimpleNamespace from unittest.mock import MagicMock, patch import pytest +import run_agent from honcho_integration.client import HonchoClientConfig from run_agent import AIAgent, _inject_honcho_turn_context from agent.prompt_builder import DEFAULT_AGENT_IDENTITY @@ -70,7 +74,7 @@ def agent_with_memory_tool(): patch("run_agent.OpenAI"), ): a = AIAgent( - api_key="test-key-1234567890", + api_key="test-k...7890", quiet_mode=True, skip_context_files=True, skip_memory=True, @@ -79,6 +83,60 @@ def agent_with_memory_tool(): return a +def test_aiagent_reuses_existing_errors_log_handler(): + """Repeated AIAgent init should not accumulate duplicate errors.log handlers.""" + root_logger = logging.getLogger() + original_handlers = list(root_logger.handlers) + error_log_path = (run_agent._hermes_home / "logs" / "errors.log").resolve() + + try: + for handler in list(root_logger.handlers): + root_logger.removeHandler(handler) + + error_log_path.parent.mkdir(parents=True, exist_ok=True) + preexisting_handler = RotatingFileHandler( + error_log_path, + maxBytes=2 * 1024 * 1024, + backupCount=2, + ) + root_logger.addHandler(preexisting_handler) + + with ( + patch( + "run_agent.get_tool_definitions", + return_value=_make_tool_defs("web_search"), + ), + patch("run_agent.check_toolset_requirements", return_value={}), + patch("run_agent.OpenAI"), + ): + AIAgent( + api_key="test-k...7890", + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + ) + AIAgent( + api_key="test-k...7890", + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + ) + + matching_handlers = [ + handler for handler in root_logger.handlers + if isinstance(handler, RotatingFileHandler) + and error_log_path == Path(handler.baseFilename).resolve() + ] + assert len(matching_handlers) == 1 + finally: + for handler in list(root_logger.handlers): + root_logger.removeHandler(handler) + if handler not in original_handlers: + handler.close() + for handler in original_handlers: + root_logger.addHandler(handler) + + # --------------------------------------------------------------------------- # Helper to build mock assistant messages (API response objects) # ---------------------------------------------------------------------------