feat(system): S85: Trigger self-healing live + drone error handler + watchdog v1.1 snapshot + STATUS
Fix 6 bugs from DPLAN-0111 audit + cleanup: - Bug #3: memory_threshold_exceeded.py handler→modules import path - Bug #5: startup catch-up fires error_detected (Medic v2) not error_logged - Bug #7: circuit_breaker_record_error at detection time, not dispatch time - Bug #8: add encoding=utf-8 to 5 open() calls missing it - Bug #9: core.py _fire_to_handlers copies data dict before mutation - Cleanup: local prompt 12→14 events, remove stale branch prefixes (backup, The_Commons, aipass_os, aipass_business), add spawn/devpulse 370 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
f564996a8f
commit
0c9a57f912
@@ -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/)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}: "
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -112,9 +112,9 @@ SYSTEM_LOGS_DIR = AIPASS_PKG_ROOT.parent.parent / "system_logs"
|
||||
# Known branch prefixes that appear in system_logs filenames (<prefix>_<module>.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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user