diff --git a/src/aipass/trigger/.aipass/aipass_local_prompt.md b/src/aipass/trigger/.aipass/aipass_local_prompt.md index 86c6789d..f14dbff9 100644 --- a/src/aipass/trigger/.aipass/aipass_local_prompt.md +++ b/src/aipass/trigger/.aipass/aipass_local_prompt.md @@ -6,7 +6,7 @@ Event bus and error dispatch for AIPass. I detect errors, fingerprint them, gate ## Architecture ``` trigger.py (entry point — auto-discovers modules/) -├── core.py → Event bus: fire/on/off/status (12 events, 12 handlers) +├── core.py → Event bus: fire/on/off/status (14 events, 14 handlers) ├── errors.py → Error registry CLI: list/detail/suppress/resolve/stats/circuit-breaker ├── medic.py → Medic toggle: on/off/status/mute/unmute ├── log_events.py → Centralized log watcher (system_logs/) diff --git a/src/aipass/trigger/apps/handlers/events/error_detected.py b/src/aipass/trigger/apps/handlers/events/error_detected.py index 3e3c4d29..c798cd27 100644 --- a/src/aipass/trigger/apps/handlers/events/error_detected.py +++ b/src/aipass/trigger/apps/handlers/events/error_detected.py @@ -350,7 +350,7 @@ def _write_suppression_log(reason: str, branch: str, module: str, message: str) try: suppressed_log = TRIGGER_ROOT / "logs" / "medic_suppressed.log" suppressed_log.parent.mkdir(parents=True, exist_ok=True) - with open(suppressed_log, 'a') as f: + with open(suppressed_log, 'a', encoding='utf-8') as f: f.write( f"{datetime.now().isoformat()} | " f"{reason} - {branch}: {module} - {message[:100]}\n" @@ -364,7 +364,7 @@ def _write_rate_log(reason: str, detail: str) -> None: try: rate_log = TRIGGER_ROOT / "logs" / "rate_limited.log" rate_log.parent.mkdir(parents=True, exist_ok=True) - with open(rate_log, 'a') as f: + with open(rate_log, 'a', encoding='utf-8') as f: f.write(f"{datetime.now().isoformat()} | {reason}: {detail}\n") except Exception as exc: _log_warning(f"rate log write failed ({reason}): {exc}") @@ -445,6 +445,10 @@ def handle_error_detected( _write_suppression_log(f"First occurrence (count={count}) - waiting for pattern", branch, module, message) return + # Record error for circuit breaker at detection time (not dispatch time) + if _REGISTRY_DISPATCH_AVAILABLE: + circuit_breaker_record_error() + # Callback must be set by module layer before events fire if _send_email is None: return @@ -531,9 +535,8 @@ def handle_error_detected( # Record dispatch for tracking if _REGISTRY_DISPATCH_AVAILABLE and fingerprint: - # Medic v2: per-fingerprint dispatch tracking + circuit breaker + # Medic v2: per-fingerprint dispatch tracking registry_record_dispatch(fingerprint) - circuit_breaker_record_error() else: # Legacy: per-branch rate limiting _record_dispatch(recipient) diff --git a/src/aipass/trigger/apps/handlers/events/error_logged.py b/src/aipass/trigger/apps/handlers/events/error_logged.py index a2ba1d85..210fd948 100644 --- a/src/aipass/trigger/apps/handlers/events/error_logged.py +++ b/src/aipass/trigger/apps/handlers/events/error_logged.py @@ -170,7 +170,7 @@ def _log_suppression(reason: str, branch: str, source_module: str, message: str) """ try: SUPPRESSED_LOG.parent.mkdir(parents=True, exist_ok=True) - with open(SUPPRESSED_LOG, 'a') as f: + with open(SUPPRESSED_LOG, 'a', encoding='utf-8') as f: f.write( f"{datetime.now().isoformat()} | " f"{reason} - suppressed dispatch for {branch}: " diff --git a/src/aipass/trigger/apps/handlers/events/memory_threshold_exceeded.py b/src/aipass/trigger/apps/handlers/events/memory_threshold_exceeded.py index 52882154..b8973271 100644 --- a/src/aipass/trigger/apps/handlers/events/memory_threshold_exceeded.py +++ b/src/aipass/trigger/apps/handlers/events/memory_threshold_exceeded.py @@ -134,9 +134,9 @@ def handle_memory_threshold_exceeded( if not branch or not file_name or line_count is None: return - # Import AI_Mail delivery + # Import AI_Mail delivery (modules-level API, not handler-level) try: - from aipass.ai_mail.apps.handlers.email.delivery import deliver_email_to_branch + from aipass.ai_mail.apps.modules.email import deliver_email_to_branch except ImportError: return diff --git a/src/aipass/trigger/apps/handlers/events/startup.py b/src/aipass/trigger/apps/handlers/events/startup.py index da1ed012..529b914b 100644 --- a/src/aipass/trigger/apps/handlers/events/startup.py +++ b/src/aipass/trigger/apps/handlers/events/startup.py @@ -56,7 +56,7 @@ def _load_trigger_data() -> Dict[str, Any]: """Load trigger_data.json with error_catchup section.""" try: if TRIGGER_DATA_FILE.exists(): - with open(TRIGGER_DATA_FILE, 'r') as f: + with open(TRIGGER_DATA_FILE, 'r', encoding='utf-8') as f: data = json.load(f) if 'error_catchup' not in data: data['error_catchup'] = { @@ -103,7 +103,7 @@ def _log_suppression(reason: str) -> None: """ try: SUPPRESSED_LOG.parent.mkdir(parents=True, exist_ok=True) - with open(SUPPRESSED_LOG, 'a') as f: + with open(SUPPRESSED_LOG, 'a', encoding='utf-8') as f: f.write(f"{datetime.now().isoformat()} | error_catchup: {reason}\n") except Exception as exc: _log_warning(f"log suppression write failed: {exc}") @@ -349,7 +349,7 @@ def _run_error_catchup(fire_event: Optional[Callable[..., None]] = None) -> None if errors and fire_event is not None: for error in errors: - fire_event('error_logged', **error) + fire_event('error_detected', **error) hash_list = list(processed_hashes) max_h = catchup.get('max_hashes', MAX_HASHES) diff --git a/src/aipass/trigger/apps/handlers/log_watcher.py b/src/aipass/trigger/apps/handlers/log_watcher.py index de63bb24..dab8d2fa 100644 --- a/src/aipass/trigger/apps/handlers/log_watcher.py +++ b/src/aipass/trigger/apps/handlers/log_watcher.py @@ -112,9 +112,9 @@ SYSTEM_LOGS_DIR = AIPASS_PKG_ROOT.parent.parent / "system_logs" # Known branch prefixes that appear in system_logs filenames (_.log). # Sorted longest-first so longer prefixes match before shorter ones. _SYSTEM_LOGS_BRANCH_PREFIXES: list = sorted([ - 'ai_mail', 'api', 'backup', 'cli', 'drone', 'flow', - 'prax', 'trigger', 'seedgo', 'memory', 'The_Commons', - 'aipass_os', 'aipass_business', + 'ai_mail', 'api', 'cli', 'drone', 'flow', + 'prax', 'trigger', 'seedgo', 'memory', + 'spawn', 'devpulse', ], key=len, reverse=True) # Event fire callback (set by module, avoids handler importing from modules) diff --git a/src/aipass/trigger/apps/modules/core.py b/src/aipass/trigger/apps/modules/core.py index ec643d29..cf9c6618 100644 --- a/src/aipass/trigger/apps/modules/core.py +++ b/src/aipass/trigger/apps/modules/core.py @@ -96,6 +96,7 @@ class Trigger: def _fire_to_handlers(cls, event: str, data: dict) -> None: """Fire a single event to its registered handlers.""" handlers = cls._handlers.get(event, []) + data = dict(data) # Copy to avoid mutating caller's dict data['fire_event'] = cls.fire for handler in handlers: try: