fix(trigger): DPLAN-0112 Option C — strip dispatch from error_logged, route through Medic v2

error_logged.py handler stripped to monitor-only (log event, no email/dispatch).
Centralized watcher (watchers/log_watcher.py) now calls registry_report() +
fires error_detected for ERROR-level lines, routing through full Medic v2
pipeline (count threshold, circuit breaker, fingerprint backoff). Falls back
to error_logged (monitor-only) if registry unavailable. 370 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AIOSAI
2026-04-10 14:28:11 -07:00
co-authored by Claude Opus 4.6
parent 64c74a5a2f
commit 0c290a95d1
2 changed files with 33 additions and 67 deletions
@@ -270,70 +270,13 @@ def handle_error_logged(
# Resolve source_module from either parameter name
effective_module = source_module or module_name or "unknown"
# --- Medic gating (FPLAN-0371 Phase 1) ---
# Gate 1: Global medic toggle
if not _is_medic_enabled():
_log_suppression("Medic OFF", branch, effective_module, message)
return
# Gate 2: Per-branch mute
if _is_branch_muted(branch):
_log_suppression("Branch muted", branch, effective_module, message)
return
# Gate 3: Convert branch name to email format
recipient = f"@{branch.lower()}"
# devpulse is protected from auto-triggering
if recipient == '@devpulse':
return
# Gate 4: Validate target branch exists in registry
registered_emails = _get_registered_emails()
if recipient not in registered_emails:
_log_suppression("Unknown branch skipped", branch, effective_module, message)
return
# Gate 5: Rate limiting (3 dispatches per 10 minutes per branch)
if _is_rate_limited(recipient):
_log_suppression("Rate limited", branch, effective_module, message)
return
# --- Dispatch ---
try:
from aipass.ai_mail.apps.modules.email import deliver_email_to_branch
except ImportError:
return
effective_timestamp = timestamp or datetime.now().strftime("%Y-%m-%d %H:%M:%S")
effective_log_file = log_file or "unknown"
email_subject = f"[ERROR] {effective_module} - investigation needed"
notification_message = _build_notification_message(
error_hash=error_hash,
source_module=effective_module,
message=message,
timestamp=effective_timestamp,
log_file=effective_log_file
)
email_data = {
"from": "@trigger",
"from_name": "TRIGGER",
"to": recipient,
"subject": email_subject,
"message": f"⚡ DISPATCH TASK - READ THIS FIRST ⚡\n\n{notification_message}",
"timestamp": effective_timestamp,
}
deliver_email_to_branch(recipient, email_data)
# Record dispatch for rate limiting
_record_dispatch(recipient)
json_handler.log_operation("error_logged_event", {"success": True})
# 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,
"error_hash": error_hash,
})
except Exception as exc:
_log_warning(f"handle_error_logged failed: {exc}")
@@ -13,9 +13,9 @@ Watches system_logs/ for log file changes.
Detects ERROR/WARNING/INFO entries and fires appropriate events.
Events fired:
- error_logged: When ERROR level log detected
- error_detected: When ERROR level log detected (Medic v2 pipeline via registry_report)
- error_logged: Monitoring-only event (no dispatch)
- warning_logged: When WARNING level log detected
- log_entry: All log entries (for monitoring systems)
Architecture:
- Trigger OWNS all file watching (filesystem events)
@@ -257,7 +257,30 @@ class LogFileWatcher(WatchdogFileSystemEventHandler if WATCHDOG_AVAILABLE else o
}
if level == 'error':
trigger.fire('error_logged', **event_data)
# Route through Medic v2: registry_report() for dedup/count, then error_detected
try:
from aipass.trigger.apps.handlers.error_registry import report as registry_report
result = registry_report(
error_type='ERROR',
message=message,
component=branch,
log_path=log_file,
severity='medium'
)
error_count = result.get('count', 1)
trigger.fire('error_detected',
branch=branch, module=module_name, message=message,
log_path=log_file, error_hash=result.get('id', error_hash),
timestamp=timestamp,
fingerprint=result.get('fingerprint', ''),
registry_id=result.get('id', ''),
first_seen=result.get('first_seen', ''),
last_seen=result.get('last_seen', ''),
count=error_count,
)
except Exception:
# Registry unavailable — fire error_logged as monitoring-only fallback
trigger.fire('error_logged', **event_data)
json_handler.log_operation("system_log_event", {"level": level, "module": module_name})
elif level == 'warning':
trigger.fire('warning_logged', **event_data)