refactor: enhance error handling with structured logging across multiple modules

- Updated various modules including cli.py, run_agent.py, gateway, and tools to replace silent exception handling with structured logging.
- Improved error messages to provide more context, aiding in debugging and monitoring.
- Ensured consistent logging practices throughout the codebase, enhancing traceability and maintainability.
This commit is contained in:
teknium1 2026-02-21 03:32:11 -08:00
parent cbff1b818c
commit 748fd3db88
14 changed files with 134 additions and 110 deletions

8
cli.py
View file

@ -823,8 +823,8 @@ class HermesCLI:
try:
from hermes_state import SessionDB
self._session_db = SessionDB()
except Exception:
pass # SQLite session store is optional
except Exception as e:
logger.debug("SQLite session store not available: %s", e)
try:
self.agent = AIAgent(
@ -2130,8 +2130,8 @@ class HermesCLI:
if hasattr(self, '_session_db') and self._session_db and self.agent:
try:
self._session_db.end_session(self.agent.session_id, "cli_close")
except Exception:
pass
except Exception as e:
logger.debug("Could not close session in DB: %s", e)
_run_cleanup()
print("\nGoodbye! ⚕")

View file

@ -8,9 +8,12 @@ Uses discord.py library for:
"""
import asyncio
import logging
import os
from typing import Dict, List, Optional, Any
logger = logging.getLogger(__name__)
try:
import discord
from discord import Message as DiscordMessage, Intents
@ -177,8 +180,8 @@ class DiscordAdapter(BasePlatformAdapter):
try:
ref_msg = await channel.fetch_message(int(reply_to))
reference = ref_msg
except Exception:
pass # Ignore if we can't find the referenced message
except Exception as e:
logger.debug("Could not fetch reply-to message: %s", e)
for i, chunk in enumerate(chunks):
msg = await channel.send(
@ -363,8 +366,8 @@ class DiscordAdapter(BasePlatformAdapter):
# Send a followup to close the interaction if needed
try:
await interaction.followup.send("Processing complete~", ephemeral=True)
except Exception:
pass
except Exception as e:
logger.debug("Discord followup failed: %s", e)
@tree.command(name="new", description="Start a new conversation")
async def slash_new(interaction: discord.Interaction):
@ -373,8 +376,8 @@ class DiscordAdapter(BasePlatformAdapter):
await self.handle_message(event)
try:
await interaction.followup.send("New conversation started~", ephemeral=True)
except Exception:
pass
except Exception as e:
logger.debug("Discord followup failed: %s", e)
@tree.command(name="reset", description="Reset your Hermes session")
async def slash_reset(interaction: discord.Interaction):
@ -383,8 +386,8 @@ class DiscordAdapter(BasePlatformAdapter):
await self.handle_message(event)
try:
await interaction.followup.send("Session reset~", ephemeral=True)
except Exception:
pass
except Exception as e:
logger.debug("Discord followup failed: %s", e)
@tree.command(name="model", description="Show or change the model")
@discord.app_commands.describe(name="Model name (e.g. anthropic/claude-sonnet-4). Leave empty to see current.")
@ -394,8 +397,8 @@ class DiscordAdapter(BasePlatformAdapter):
await self.handle_message(event)
try:
await interaction.followup.send("Done~", ephemeral=True)
except Exception:
pass
except Exception as e:
logger.debug("Discord followup failed: %s", e)
@tree.command(name="personality", description="Set a personality")
@discord.app_commands.describe(name="Personality name. Leave empty to list available.")
@ -405,8 +408,8 @@ class DiscordAdapter(BasePlatformAdapter):
await self.handle_message(event)
try:
await interaction.followup.send("Done~", ephemeral=True)
except Exception:
pass
except Exception as e:
logger.debug("Discord followup failed: %s", e)
@tree.command(name="retry", description="Retry your last message")
async def slash_retry(interaction: discord.Interaction):
@ -415,8 +418,8 @@ class DiscordAdapter(BasePlatformAdapter):
await self.handle_message(event)
try:
await interaction.followup.send("Retrying~", ephemeral=True)
except Exception:
pass
except Exception as e:
logger.debug("Discord followup failed: %s", e)
@tree.command(name="undo", description="Remove the last exchange")
async def slash_undo(interaction: discord.Interaction):
@ -425,8 +428,8 @@ class DiscordAdapter(BasePlatformAdapter):
await self.handle_message(event)
try:
await interaction.followup.send("Done~", ephemeral=True)
except Exception:
pass
except Exception as e:
logger.debug("Discord followup failed: %s", e)
@tree.command(name="status", description="Show Hermes session status")
async def slash_status(interaction: discord.Interaction):
@ -435,8 +438,8 @@ class DiscordAdapter(BasePlatformAdapter):
await self.handle_message(event)
try:
await interaction.followup.send("Status sent~", ephemeral=True)
except Exception:
pass
except Exception as e:
logger.debug("Discord followup failed: %s", e)
@tree.command(name="stop", description="Stop the running Hermes agent")
async def slash_stop(interaction: discord.Interaction):
@ -445,8 +448,8 @@ class DiscordAdapter(BasePlatformAdapter):
await self.handle_message(event)
try:
await interaction.followup.send("Stop requested~", ephemeral=True)
except Exception:
pass
except Exception as e:
logger.debug("Discord followup failed: %s", e)
def _build_slash_event(self, interaction: discord.Interaction, text: str) -> MessageEvent:
"""Build a MessageEvent from a Discord slash command interaction."""

View file

@ -17,10 +17,13 @@ with different backends via a bridge pattern.
import asyncio
import json
import logging
import subprocess
from pathlib import Path
from typing import Dict, List, Optional, Any
logger = logging.getLogger(__name__)
import sys
sys.path.insert(0, str(__file__).rsplit("/", 3)[0])
@ -246,8 +249,8 @@ class WhatsAppAdapter(BasePlatformAdapter):
"type": "group" if data.get("isGroup") else "dm",
"participants": data.get("participants", []),
}
except Exception:
pass
except Exception as e:
logger.debug("Could not get WhatsApp chat info for %s: %s", chat_id, e)
return {"name": chat_id, "type": "dm"}

View file

@ -479,8 +479,8 @@ class GatewayRunner:
self._pending_approvals[session_key] = _last_pending_approval.copy()
# Clear the global so it doesn't leak to other sessions
_last_pending_approval.clear()
except Exception:
pass
except Exception as e:
logger.debug("Failed to check pending approvals: %s", e)
# Save the full conversation to the transcript, including tool calls.
# This preserves the complete agent loop (tool_calls, tool results,
@ -973,8 +973,8 @@ class GatewayRunner:
with open(config_path, 'r') as f:
user_config = yaml.safe_load(f) or {}
platform_toolsets_config = user_config.get("platform_toolsets", {})
except Exception:
pass
except Exception as e:
logger.debug("Could not load platform_toolsets config: %s", e)
# Map platform enum to config key
platform_config_key = {

View file

@ -8,6 +8,7 @@ Handles:
- Dynamic system prompt injection (agent knows its context)
"""
import logging
import os
import json
import uuid
@ -16,6 +17,8 @@ from datetime import datetime, timedelta
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any
logger = logging.getLogger(__name__)
from .config import (
Platform,
GatewayConfig,
@ -388,8 +391,8 @@ class SessionStore:
if self._db:
try:
self._db.end_session(entry.session_id, "session_reset")
except Exception:
pass
except Exception as e:
logger.debug("Session DB operation failed: %s", e)
# Create new session
session_id = f"{now.strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}"
@ -443,8 +446,8 @@ class SessionStore:
self._db.update_token_counts(
entry.session_id, input_tokens, output_tokens
)
except Exception:
pass
except Exception as e:
logger.debug("Session DB operation failed: %s", e)
def reset_session(self, session_key: str) -> Optional[SessionEntry]:
"""Force reset a session, creating a new session ID."""
@ -459,8 +462,8 @@ class SessionStore:
if self._db:
try:
self._db.end_session(old_entry.session_id, "session_reset")
except Exception:
pass
except Exception as e:
logger.debug("Session DB operation failed: %s", e)
now = datetime.now()
session_id = f"{now.strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}"
@ -487,8 +490,8 @@ class SessionStore:
source=old_entry.platform.value if old_entry.platform else "unknown",
user_id=old_entry.origin.user_id if old_entry.origin else None,
)
except Exception:
pass
except Exception as e:
logger.debug("Session DB operation failed: %s", e)
return new_entry
@ -523,8 +526,8 @@ class SessionStore:
tool_calls=message.get("tool_calls"),
tool_call_id=message.get("tool_call_id"),
)
except Exception:
pass
except Exception as e:
logger.debug("Session DB operation failed: %s", e)
# Also write legacy JSONL (keeps existing tooling working during transition)
transcript_path = self.get_transcript_path(session_id)
@ -539,8 +542,8 @@ class SessionStore:
messages = self._db.get_messages_as_conversation(session_id)
if messages:
return messages
except Exception:
pass
except Exception as e:
logger.debug("Could not load messages from DB: %s", e)
# Fall back to legacy JSONL
transcript_path = self.get_transcript_path(session_id)

View file

@ -16,6 +16,7 @@ Architecture:
from __future__ import annotations
import json
import logging
import os
import stat
import time
@ -32,6 +33,8 @@ import yaml
from hermes_cli.config import get_hermes_home, get_config_path
from hermes_constants import OPENROUTER_BASE_URL
logger = logging.getLogger(__name__)
try:
import fcntl
except Exception:
@ -314,8 +317,8 @@ def resolve_provider(
state = _load_provider_state(auth_store, active)
if state and (state.get("access_token") or state.get("refresh_token")):
return active
except Exception:
pass
except Exception as e:
logger.debug("Could not detect active auth provider: %s", e)
if os.getenv("OPENAI_API_KEY") or os.getenv("OPENROUTER_API_KEY"):
return "openrouter"
@ -578,8 +581,8 @@ def fetch_nous_models(
try:
err = response.json()
description = str(err.get("error_description") or err.get("error") or description)
except Exception:
pass
except Exception as e:
logger.debug("Could not parse error response JSON: %s", e)
raise AuthError(description, provider="nous", code="models_fetch_failed")
payload = response.json()

View file

@ -39,9 +39,13 @@ env_path = PROJECT_ROOT / '.env'
if env_path.exists():
load_dotenv(dotenv_path=env_path)
import logging
from hermes_cli import __version__
from hermes_constants import OPENROUTER_BASE_URL
logger = logging.getLogger(__name__)
def cmd_chat(args):
"""Run interactive chat CLI."""
@ -512,8 +516,8 @@ def cmd_update(args):
print(f" + {len(result['copied'])} new skill(s): {', '.join(result['copied'])}")
else:
print(" ✓ Skills are up to date")
except Exception:
pass
except Exception as e:
logger.debug("Skills sync during update failed: %s", e)
# Check for config migrations
print()

View file

@ -12,11 +12,14 @@ Guides users through:
Config files are stored in ~/.hermes/ for easy access.
"""
import logging
import os
import sys
from pathlib import Path
from typing import Optional, Dict, Any
logger = logging.getLogger(__name__)
PROJECT_ROOT = Path(__file__).parent.parent.resolve()
# Import config helpers
@ -488,8 +491,8 @@ def run_setup_wizard(args):
inference_base_url=creds.get("base_url", ""),
api_key=creds.get("api_key", ""),
)
except Exception:
pass
except Exception as e:
logger.debug("Could not fetch Nous models after login: %s", e)
except SystemExit:
print_warning("Nous Portal login was cancelled or failed.")

View file

@ -636,8 +636,8 @@ def build_skills_system_prompt() -> str:
match = re.search(r"^---\s*\n.*?description:\s*(.+?)\s*\n.*?^---", content, re.MULTILINE | re.DOTALL)
if match:
category_descriptions[category] = match.group(1).strip()
except Exception:
pass
except Exception as e:
logger.debug("Could not read skill description %s: %s", desc_file, e)
index_lines = []
for category in sorted(skills_by_category.keys()):
@ -748,8 +748,8 @@ def build_context_files_prompt(cwd: str = None) -> str:
if content:
rel_path = agents_path.relative_to(cwd_path)
total_agents_content += f"## {rel_path}\n\n{content}\n\n"
except Exception:
pass
except Exception as e:
logger.debug("Could not read %s: %s", agents_path, e)
if total_agents_content:
total_agents_content = _truncate_content(total_agents_content, "AGENTS.md")
@ -765,8 +765,8 @@ def build_context_files_prompt(cwd: str = None) -> str:
content = cursorrules_file.read_text(encoding="utf-8").strip()
if content:
cursorrules_content += f"## .cursorrules\n\n{content}\n\n"
except Exception:
pass
except Exception as e:
logger.debug("Could not read .cursorrules: %s", e)
# Check for .cursor/rules/*.mdc files
cursor_rules_dir = cwd_path / ".cursor" / "rules"
@ -777,8 +777,8 @@ def build_context_files_prompt(cwd: str = None) -> str:
content = mdc_file.read_text(encoding="utf-8").strip()
if content:
cursorrules_content += f"## .cursor/rules/{mdc_file.name}\n\n{content}\n\n"
except Exception:
pass
except Exception as e:
logger.debug("Could not read %s: %s", mdc_file, e)
if cursorrules_content:
cursorrules_content = _truncate_content(cursorrules_content, ".cursorrules")
@ -807,8 +807,8 @@ def build_context_files_prompt(cwd: str = None) -> str:
content = _truncate_content(content, "SOUL.md")
soul_content = f"## SOUL.md\n\nIf SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.\n\n{content}"
sections.append(soul_content)
except Exception:
pass
except Exception as e:
logger.debug("Could not read SOUL.md from %s: %s", soul_path, e)
# ----- Assemble -----
if not sections:
@ -1320,8 +1320,8 @@ class AIAgent:
},
user_id=None,
)
except Exception:
pass
except Exception as e:
logger.debug("Session DB create_session failed: %s", e)
# In-memory todo list for task planning (one per agent/session)
from tools.todo_tool import TodoStore
@ -1730,8 +1730,8 @@ class AIAgent:
tool_call_id=msg.get("tool_call_id"),
finish_reason=msg.get("finish_reason"),
)
except Exception:
pass
except Exception as e:
logger.debug("Session DB append_message failed: %s", e)
def _get_messages_up_to_last_assistant(self, messages: List[Dict]) -> List[Dict]:
"""
@ -2048,8 +2048,8 @@ class AIAgent:
api_key = None
try:
api_key = getattr(self.client, "api_key", None)
except Exception:
pass
except Exception as e:
logger.debug("Could not extract API key for debug dump: %s", e)
dump_payload: Dict[str, Any] = {
"timestamp": datetime.now().isoformat(),
@ -2085,8 +2085,8 @@ class AIAgent:
try:
error_info["response_status"] = getattr(response_obj, "status_code", None)
error_info["response_text"] = response_obj.text
except Exception:
pass
except Exception as e:
logger.debug("Could not extract error response details: %s", e)
dump_payload["error"] = error_info
@ -2174,8 +2174,8 @@ class AIAgent:
for child in self._active_children:
try:
child.interrupt(message)
except Exception:
pass
except Exception as e:
logger.debug("Failed to propagate interrupt to child agent: %s", e)
if not self.quiet_mode:
print(f"\n⚡ Interrupt requested" + (f": '{message[:40]}...'" if message and len(message) > 40 else f": '{message}'" if message else ""))
@ -2346,8 +2346,8 @@ class AIAgent:
if self._session_db:
try:
self._session_db.update_system_prompt(self.session_id, self._cached_system_prompt)
except Exception:
pass
except Exception as e:
logger.debug("Session DB update_system_prompt failed: %s", e)
active_system_prompt = self._cached_system_prompt
@ -2355,8 +2355,8 @@ class AIAgent:
if self._session_db:
try:
self._session_db.append_message(self.session_id, "user", user_message)
except Exception:
pass
except Exception as e:
logger.debug("Session DB append_message failed: %s", e)
# Main conversation loop
api_call_count = 0
@ -2743,8 +2743,8 @@ class AIAgent:
parent_session_id=old_session_id,
)
self._session_db.update_system_prompt(self.session_id, active_system_prompt)
except Exception:
pass
except Exception as e:
logger.debug("Session DB compression split failed: %s", e)
print(f"{self.log_prefix} 🗜️ Compressed {original_len}{len(messages)} messages, retrying...")
continue # Retry with compressed messages
else:
@ -3175,8 +3175,8 @@ class AIAgent:
parent_session_id=old_session_id,
)
self._session_db.update_system_prompt(self.session_id, active_system_prompt)
except Exception:
pass
except Exception as e:
logger.debug("Session DB compression split failed: %s", e)
# Save session log incrementally (so progress is visible even if interrupted)
self._session_messages = messages

View file

@ -31,6 +31,8 @@ import uuid
from typing import Any, Dict, List, Optional
# Availability gate: UDS requires a POSIX OS
logger = logging.getLogger(__name__)
SANDBOX_AVAILABLE = sys.platform != "win32"
# The 7 tools allowed inside the sandbox. The intersection of this list
@ -488,8 +490,8 @@ def execute_code(
try:
import shutil
shutil.rmtree(tmpdir, ignore_errors=True)
except Exception:
pass
except Exception as e:
logger.debug("Could not clean temp dir: %s", e)
try:
os.unlink(sock_path)
except OSError:
@ -503,8 +505,8 @@ def _kill_process_group(proc, escalate: bool = False):
except (ProcessLookupError, PermissionError):
try:
proc.kill()
except Exception:
pass
except Exception as e:
logger.debug("Could not kill process: %s", e)
if escalate:
# Give the process 5s to exit after SIGTERM, then SIGKILL
@ -516,8 +518,8 @@ def _kill_process_group(proc, escalate: bool = False):
except (ProcessLookupError, PermissionError):
try:
proc.kill()
except Exception:
pass
except Exception as e:
logger.debug("Could not kill process: %s", e)
def _load_config() -> dict:

View file

@ -277,14 +277,14 @@ class ProcessRegistry:
session.output_buffer += chunk
if len(session.output_buffer) > session.max_output_chars:
session.output_buffer = session.output_buffer[-session.max_output_chars:]
except Exception:
pass
except Exception as e:
logger.debug("Process stdout reader ended: %s", e)
# Process exited
try:
session.process.wait(timeout=5)
except Exception:
pass
except Exception as e:
logger.debug("Process wait timed out or failed: %s", e)
session.exited = True
session.exit_code = session.process.returncode
self._move_to_finished(session)
@ -351,14 +351,14 @@ class ProcessRegistry:
break
except Exception:
break
except Exception:
pass
except Exception as e:
logger.debug("PTY stdout reader ended: %s", e)
# Process exited
try:
pty.wait()
except Exception:
pass
except Exception as e:
logger.debug("PTY wait timed out or failed: %s", e)
session.exited = True
session.exit_code = pty.exitstatus if hasattr(pty, 'exitstatus') else -1
self._move_to_finished(session)
@ -719,8 +719,8 @@ class ProcessRegistry:
# Clear the checkpoint (will be rewritten as processes finish)
try:
CHECKPOINT_PATH.write_text("[]", encoding="utf-8")
except Exception:
pass
except Exception as e:
logger.debug("Could not write checkpoint file: %s", e)
return recovered

View file

@ -154,8 +154,8 @@ class GitHubAuth:
)
if result.returncode == 0 and result.stdout.strip():
return result.stdout.strip()
except (FileNotFoundError, subprocess.TimeoutExpired):
pass
except (FileNotFoundError, subprocess.TimeoutExpired) as e:
logger.debug("gh CLI token lookup failed: %s", e)
return None
def _try_github_app(self) -> Optional[str]:
@ -438,8 +438,8 @@ class GitHubSource(SkillSource):
)
if resp.status_code == 200:
return resp.text
except httpx.HTTPError:
pass
except httpx.HTTPError as e:
logger.debug("GitHub contents API fetch failed: %s", e)
return None
def _read_cache(self, key: str) -> Optional[list]:
@ -461,8 +461,8 @@ class GitHubSource(SkillSource):
cache_file = INDEX_CACHE_DIR / f"{key}.json"
try:
cache_file.write_text(json.dumps(data, ensure_ascii=False))
except OSError:
pass
except OSError as e:
logger.debug("Could not write cache: %s", e)
@staticmethod
def _meta_to_dict(meta: SkillMeta) -> dict:
@ -826,8 +826,8 @@ class LobeHubSource(SkillSource):
resp = httpx.get(url, timeout=15)
if resp.status_code == 200:
return resp.json()
except (httpx.HTTPError, json.JSONDecodeError):
pass
except (httpx.HTTPError, json.JSONDecodeError) as e:
logger.debug("LobeHub agent fetch failed: %s", e)
return None
@staticmethod
@ -890,8 +890,8 @@ def _write_index_cache(key: str, data: Any) -> None:
cache_file = INDEX_CACHE_DIR / f"{key}.json"
try:
cache_file.write_text(json.dumps(data, ensure_ascii=False, default=str))
except OSError:
pass
except OSError as e:
logger.debug("Could not write cache: %s", e)
def _skill_meta_to_dict(meta: SkillMeta) -> dict:
@ -1037,8 +1037,8 @@ def append_audit_log(action: str, skill_name: str, source: str,
try:
with open(AUDIT_LOG, "a") as f:
f.write(line)
except OSError:
pass
except OSError as e:
logger.debug("Could not write audit log: %s", e)
# ---------------------------------------------------------------------------

View file

@ -13,11 +13,14 @@ newline-delimited list of skill names that have been offered to the user.
"""
import json
import logging
import os
import shutil
from pathlib import Path
from typing import List, Tuple
logger = logging.getLogger(__name__)
HERMES_HOME = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
SKILLS_DIR = HERMES_HOME / "skills"
@ -131,8 +134,8 @@ def sync_skills(quiet: bool = False) -> dict:
try:
dest_desc.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(desc_md, dest_desc)
except (OSError, IOError):
pass
except (OSError, IOError) as e:
logger.debug("Could not copy %s: %s", desc_md, e)
_write_manifest(manifest)

View file

@ -122,8 +122,8 @@ def _convert_to_opus(mp3_path: str) -> Optional[str]:
)
if os.path.exists(ogg_path) and os.path.getsize(ogg_path) > 0:
return ogg_path
except Exception:
pass
except Exception as e:
logger.warning("ffmpeg OGG conversion failed: %s", e)
return None