diff --git a/src/aipass/trigger/apps/handlers/events/error_logged.py b/src/aipass/trigger/apps/handlers/events/error_logged.py index b72688d7..2884f20d 100644 --- a/src/aipass/trigger/apps/handlers/events/error_logged.py +++ b/src/aipass/trigger/apps/handlers/events/error_logged.py @@ -1,37 +1,23 @@ # =================== AIPass ==================== # Name: error_logged.py -# Description: Legacy error logged event handler with medic gating (deprecated) -# Version: 2.0.0 +# Description: Legacy error logged event handler — monitor-only (no dispatch) +# Version: 3.0.0 # Created: 2026-01-31 -# Modified: 2026-02-25 +# Modified: 2026-04-10 # ============================================= """ -Error Logged Event Handler (DEPRECATED) +Error Logged Event Handler (Monitor-Only) -Legacy handler for error_logged events. The primary error dispatch pipeline -is now error_detected.py (Medic v2) which provides circuit breaker, per-fingerprint +Legacy handler for error_logged events. All dispatch now goes through +error_detected.py (Medic v2) which provides circuit breaker, per-fingerprint backoff, and registry-based deduplication. -This handler remains for backward compatibility with code that fires error_logged -events directly. It now includes full medic gating (medic_enabled, branch_muted, -rate limiting, devpulse protection) to prevent bypass. - -Event data expected: - - branch: Branch where error occurred (e.g., FLOW) - - message: Error message text - - error_hash: Unique hash for deduplication - - timestamp: When the error occurred - - log_file: Path to log file - - source_module: Module that logged the error - - level: Log level (always 'error' for this handler) +This handler logs event metadata for monitoring. No email, no wake_branch. """ -import json -import time from datetime import datetime, timezone -from pathlib import Path -from typing import Any, Dict, List +from typing import Any from aipass.trigger.apps.config import TRIGGER_ROOT from aipass.trigger.apps.handlers.json import json_handler @@ -49,229 +35,33 @@ def _log_warning(message: str) -> None: pass -def _find_repo_root() -> Path: - """Walk up from this file to find the repo root (contains AIPASS_REGISTRY.json).""" - current = Path(__file__).resolve().parent - for parent in [current] + list(current.parents): - if (parent / "AIPASS_REGISTRY.json").exists(): - return parent - return Path.cwd() - -_REPO_ROOT = _find_repo_root() - -TRIGGER_CONFIG_FILE = TRIGGER_ROOT / "trigger_json" / "trigger_config.json" -BRANCH_REGISTRY_FILE = _REPO_ROOT / "AIPASS_REGISTRY.json" -SUPPRESSED_LOG = TRIGGER_ROOT / "logs" / "medic_suppressed.log" - -# Legacy rate limiting -_dispatch_timestamps: Dict[str, List[float]] = {} -MAX_DISPATCHES_PER_WINDOW = 3 -RATE_LIMIT_WINDOW_SECONDS = 600 # 10 minutes - - -def _is_medic_enabled() -> bool: - """Check if medic dispatch is enabled globally. - - Reads medic_enabled from trigger_config.json. - Defaults to True if config is missing or unreadable. - - Returns: - True if medic dispatch is enabled - """ - try: - if TRIGGER_CONFIG_FILE.exists(): - data = json.loads(TRIGGER_CONFIG_FILE.read_text(encoding='utf-8')) - return bool(data.get('config', {}).get('medic_enabled', True)) - except Exception as exc: - _log_warning(f"_is_medic_enabled config read failed: {exc}") - return True - return True - - -def _is_branch_muted(branch_name: str) -> bool: - """Check if a specific branch is muted for medic dispatch. - - Reads muted_branches list from trigger_config.json. - - Args: - branch_name: Branch name (case-insensitive) - - Returns: - True if branch is in the muted list - """ - try: - if TRIGGER_CONFIG_FILE.exists(): - data = json.loads(TRIGGER_CONFIG_FILE.read_text(encoding='utf-8')) - muted = data.get('config', {}).get('muted_branches', []) - return branch_name.lower() in [b.lower() for b in muted] - except Exception as exc: - _log_warning(f"_is_branch_muted config read failed: {exc}") - return False - return False - - -def _get_registered_emails() -> set: - """Read registered branch emails from AIPASS_REGISTRY.json. - - Returns: - Set of registered email addresses (e.g., {'@flow', '@drone'}) - """ - try: - if BRANCH_REGISTRY_FILE.exists(): - data = json.loads(BRANCH_REGISTRY_FILE.read_text(encoding='utf-8')) - return {b["email"] for b in data.get("branches", [])} - except Exception as exc: - _log_warning(f"_get_registered_emails registry read failed: {exc}") - return set() - return set() - - -def _is_rate_limited(branch_email: str) -> bool: - """Check if a branch has exceeded the dispatch rate limit. - - Args: - branch_email: Target branch email (e.g., '@flow') - - Returns: - True if branch has hit the limit (3 dispatches in 10 minutes) - """ - now = time.time() - cutoff = now - RATE_LIMIT_WINDOW_SECONDS - - if branch_email not in _dispatch_timestamps: - _dispatch_timestamps[branch_email] = [] - - _dispatch_timestamps[branch_email] = [ - ts for ts in _dispatch_timestamps[branch_email] if ts > cutoff - ] - - return len(_dispatch_timestamps[branch_email]) >= MAX_DISPATCHES_PER_WINDOW - - -def _record_dispatch(branch_email: str) -> None: - """Record a dispatch timestamp for rate limiting. - - Args: - branch_email: Target branch email (e.g., '@flow') - """ - if branch_email not in _dispatch_timestamps: - _dispatch_timestamps[branch_email] = [] - _dispatch_timestamps[branch_email].append(time.time()) - - -def _log_suppression(reason: str, branch: str, source_module: str, message: str) -> None: - """Log a suppressed dispatch to medic_suppressed.log. - - Args: - reason: Why dispatch was suppressed - branch: Target branch name - source_module: Module that logged the error - message: Error message (truncated to 100 chars) - """ - try: - SUPPRESSED_LOG.parent.mkdir(parents=True, exist_ok=True) - with open(SUPPRESSED_LOG, 'a', encoding='utf-8') as f: - f.write( - f"{datetime.now().isoformat()} | " - f"{reason} - suppressed dispatch for {branch}: " - f"{source_module} - {message[:100]}\n" - ) - except Exception as exc: - _log_warning(f"_log_suppression write failed: {exc}") - return - - -def _build_notification_message( - error_hash: str, - source_module: str, - message: str, - timestamp: str, - log_file: str -) -> str: - """Build error notification message with investigation instructions. - - Args: - error_hash: Unique error identifier - source_module: Module that logged the error - message: Error message text - timestamp: When error occurred - log_file: Path to source log file - - Returns: - Formatted message string - """ - return f"""Error detected - investigate and respond. - -Error ID: {error_hash} -Module: {source_module} -Timestamp: {timestamp} -Log file: {log_file} - -Error message: -{message} - ---- -INVESTIGATION STEPS: -1. Check the log file for context around this error -2. Identify root cause - -DECISION TREE: -- SIMPLE FIX (typo, missing import, config issue): - -> Fix it yourself, then report what you did to @devpulse -- COMPLEX/UNCLEAR (needs research, affects multiple files): - -> Report findings only to @devpulse, recommend action, don't fix -- CRITICAL (data loss risk, security, system stability): - -> STOP immediately, escalate to @devpulse with full context - -REPORT TO @devpulse: - ai_mail email @devpulse "ERROR {error_hash[:8]} - [STATUS]" "Findings..." -""" - - def handle_error_logged( branch: str | None = None, message: str | None = None, error_hash: str | None = None, - timestamp: str | None = None, - log_file: str | None = None, source_module: str | None = None, module_name: str | None = None, - level: str | None = None, # noqa: ARG001 - **kwargs: Any # noqa: ARG001 + **_kwargs: Any ) -> None: - """Handle error_logged event with full medic gating. + """Handle error_logged event — monitor-only, no dispatch. - DEPRECATED: This is the legacy error notification handler. The primary - pipeline is error_detected.py (Medic v2). This handler remains for - backward compatibility with code that fires error_logged events. - - Gating (matches error_detected.py): - 1. medic_enabled check (global toggle) - 2. branch_muted check (per-branch suppression) - 3. devpulse protection (never auto-trigger) - 4. Branch validation (unknown branches logged + skipped) - 5. Rate limiting (3 per 10 minutes per branch) + All dispatch now goes through error_detected.py (Medic v2). + This handler logs event metadata for monitoring only. Args: - branch: Branch where error occurred - REQUIRED - message: Error message text - REQUIRED - error_hash: Unique error identifier - REQUIRED - timestamp: When error occurred (defaults to now) - log_file: Path to source log file + branch: Branch where error occurred + message: Error message text + error_hash: Unique error identifier source_module: Module that logged the error module_name: Deprecated alias for source_module - level: Log level (for reference, unused) - **kwargs: Additional event data (ignored) + **_kwargs: Additional event data (ignored) """ try: if not branch or not message or not error_hash: return - # Resolve source_module from either parameter name effective_module = source_module or module_name or "unknown" - # Monitor-only: log the event for tracking, no dispatch. - # All dispatch now goes through error_detected.py (Medic v2). json_handler.log_operation("error_logged_event", { "branch": branch, "module": effective_module,