From 4404b60305591ec254740d330c235229d4c0819a Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Fri, 10 Jul 2026 18:24:06 -0700 Subject: [PATCH] #673 offenders adopt prax append_jsonl: @backup (1 .jsonl site), @hooks (2 .jsonl sites), @trigger (11 raw .log appenders across 8 files -> .jsonl + downstream medic_state/medic/log_watcher readers + 5 tests). Zero raw open('a') log appenders remain fleet-wide. Verified: backup 18 / hooks 114 / trigger 189 tests green, no recursion regression, append_jsonl wired at every site. --- .../backup/apps/handlers/json/json_handler.py | 9 ++-- .../hooks/apps/handlers/config/diagnostics.py | 6 +-- .../notification/telegram_response.py | 5 +-- src/aipass/trigger/apps/config.py | 19 ++++---- .../apps/handlers/events/error_detected.py | 43 ++++++++++++------- .../apps/handlers/events/memory_pool.py | 19 ++++---- .../trigger/apps/handlers/events/plan_file.py | 19 ++++---- .../apps/handlers/events/pr_status_sync.py | 19 ++++---- .../trigger/apps/handlers/events/registry.py | 20 +++++---- .../trigger/apps/handlers/events/startup.py | 35 ++++++++------- .../apps/handlers/json/json_handler.py | 21 +++++---- .../trigger/apps/handlers/log_watcher.py | 4 +- .../trigger/apps/handlers/medic_state.py | 12 +++--- src/aipass/trigger/apps/modules/medic.py | 10 ++--- src/aipass/trigger/tests/test_log_watcher.py | 4 +- src/aipass/trigger/tests/test_medic.py | 2 +- src/aipass/trigger/tests/test_medic_state.py | 34 +++++++-------- .../trigger/tests/test_memory_pool_handler.py | 2 +- 18 files changed, 153 insertions(+), 130 deletions(-) diff --git a/src/aipass/backup/apps/handlers/json/json_handler.py b/src/aipass/backup/apps/handlers/json/json_handler.py index b95631ca..e47ca9df 100644 --- a/src/aipass/backup/apps/handlers/json/json_handler.py +++ b/src/aipass/backup/apps/handlers/json/json_handler.py @@ -14,7 +14,7 @@ import tempfile from datetime import datetime, timezone from pathlib import Path -from aipass.prax import logger +from aipass.prax import append_jsonl, logger def log_operation(operation: str, data: dict) -> None: @@ -24,12 +24,9 @@ def log_operation(operation: str, data: dict) -> None: "operation": operation, **data, } - log_dir = Path(__file__).resolve().parents[3] / "logs" - log_dir.mkdir(exist_ok=True) - log_file = log_dir / "operations.jsonl" + log_file = Path(__file__).resolve().parents[3] / "logs" / "operations.jsonl" try: - with open(log_file, "a", encoding="utf-8") as f: - f.write(json.dumps(entry, default=str) + "\n") + append_jsonl(log_file, entry) except OSError as e: logger.warning(f"Failed to write operation log: {e}") diff --git a/src/aipass/hooks/apps/handlers/config/diagnostics.py b/src/aipass/hooks/apps/handlers/config/diagnostics.py index 01db6a65..640241d4 100644 --- a/src/aipass/hooks/apps/handlers/config/diagnostics.py +++ b/src/aipass/hooks/apps/handlers/config/diagnostics.py @@ -10,9 +10,9 @@ """JSONL diagnostic logging — appends structured entries for hook activity.""" -import json from pathlib import Path +from aipass.prax import append_jsonl from aipass.prax.apps.modules.logger import system_logger as logger BRANCH_ROOT = Path(__file__).resolve().parent.parent.parent.parent @@ -22,9 +22,7 @@ LOG_FILE = BRANCH_ROOT / "logs" / "engine.jsonl" def log_entry(entry: dict) -> None: """Append a JSONL log entry for detailed diagnostics.""" try: - LOG_FILE.parent.mkdir(parents=True, exist_ok=True) - with open(LOG_FILE, "a", encoding="utf-8") as f: - f.write(json.dumps(entry, ensure_ascii=False) + "\n") + append_jsonl(LOG_FILE, entry) except OSError as exc: logger.error("[HOOKS] log write failed: %s", exc) diff --git a/src/aipass/hooks/apps/handlers/notification/telegram_response.py b/src/aipass/hooks/apps/handlers/notification/telegram_response.py index 9617caec..10649022 100644 --- a/src/aipass/hooks/apps/handlers/notification/telegram_response.py +++ b/src/aipass/hooks/apps/handlers/notification/telegram_response.py @@ -28,6 +28,7 @@ from pathlib import Path from urllib.error import HTTPError, URLError from urllib.request import Request, urlopen +from aipass.prax import append_jsonl from aipass.prax.apps.modules.logger import system_logger as logger PENDING_DIR = Path.home() / ".aipass" / "telegram_pending" @@ -760,8 +761,6 @@ def _write_delivery_log(intended_text: str, chunks: list[str], chunk_results: li record["culprit"] = culprit try: - _DELIVERY_LOG.parent.mkdir(parents=True, exist_ok=True) - with open(_DELIVERY_LOG, "a", encoding="utf-8") as f: - f.write(json.dumps(record) + "\n") + append_jsonl(_DELIVERY_LOG, record) except OSError as e: logger.warning("[HOOKS] telegram: delivery log write failed: %s", e) diff --git a/src/aipass/trigger/apps/config.py b/src/aipass/trigger/apps/config.py index 239e5a3f..8cfa42df 100644 --- a/src/aipass/trigger/apps/config.py +++ b/src/aipass/trigger/apps/config.py @@ -19,23 +19,26 @@ import os import tempfile from contextlib import contextmanager from pathlib import Path -from datetime import datetime, timezone + +try: + from aipass.prax import append_jsonl as _append_jsonl +except Exception: + _append_jsonl = None # Trigger package root: .../aipass/trigger/ TRIGGER_ROOT = Path(__file__).resolve().parents[1] -_CONFIG_LOG = TRIGGER_ROOT / "logs" / "config.log" +_CONFIG_LOG = TRIGGER_ROOT / "logs" / "config.jsonl" def _log_warning(message: str) -> None: - """Log warning to file (config cannot import prax logger — circular).""" + """Log warning to file (recursion-safe prax path).""" + if _append_jsonl is None: + return try: - _CONFIG_LOG.parent.mkdir(parents=True, exist_ok=True) - ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") - with open(_CONFIG_LOG, "a", encoding="utf-8") as f: - f.write(f"{ts} | WARNING | {message}\n") + _append_jsonl(_CONFIG_LOG, {"level": "WARNING", "msg": message}) except Exception: - pass # Meta-logging: cannot log a failure to log + pass # AIPass package root: .../aipass/ diff --git a/src/aipass/trigger/apps/handlers/events/error_detected.py b/src/aipass/trigger/apps/handlers/events/error_detected.py index 5c0c03c3..616fd0ac 100644 --- a/src/aipass/trigger/apps/handlers/events/error_detected.py +++ b/src/aipass/trigger/apps/handlers/events/error_detected.py @@ -35,24 +35,28 @@ Architecture (Medic v2): import json import time -from datetime import datetime, timezone +from datetime import datetime from pathlib import Path from typing import Any, Callable, Dict, List, Optional from aipass.trigger.apps.config import TRIGGER_ROOT from aipass.trigger.apps.handlers.json import json_handler -_HANDLER_LOG = TRIGGER_ROOT / "logs" / "error_detected_handler.log" +try: + from aipass.prax import append_jsonl as _append_jsonl +except Exception: + _append_jsonl = None + +_HANDLER_LOG = TRIGGER_ROOT / "logs" / "error_detected_handler.jsonl" def _log_warning(message: str) -> None: - """Log warning to file (event handlers cannot import Prax logger - causes recursion).""" + """Log warning to file (recursion-safe prax path).""" + if _append_jsonl is None: + return try: - _HANDLER_LOG.parent.mkdir(parents=True, exist_ok=True) - ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") - with open(_HANDLER_LOG, "a", encoding="utf-8") as f: - f.write(f"{ts} | WARNING | {message}\n") + _append_jsonl(_HANDLER_LOG, {"level": "WARNING", "msg": message}) except Exception: - pass + pass # seedgo:bypass meta-logging def _find_repo_root() -> Path: @@ -348,22 +352,29 @@ REPORT TO @devpulse: def _write_suppression_log(reason: str, branch: str, module: str, message: str) -> None: """Write a line to the medic suppression log.""" + if _append_jsonl is None: + return try: - suppressed_log = TRIGGER_ROOT / "logs" / "medic_suppressed.log" - 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()} | {reason} - {branch}: {module} - {message[:100]}\n") + suppressed_log = TRIGGER_ROOT / "logs" / "medic_suppressed.jsonl" + entry = { + "ts": datetime.now().isoformat(), + "reason": reason, + "branch": branch, + "module": module, + "msg": message[:100], + } + _append_jsonl(suppressed_log, entry) except Exception as exc: _log_warning(f"suppression log write failed ({reason}): {exc}") def _write_rate_log(reason: str, detail: str) -> None: """Write a line to the rate-limited log.""" + if _append_jsonl is None: + return try: - rate_log = TRIGGER_ROOT / "logs" / "rate_limited.log" - rate_log.parent.mkdir(parents=True, exist_ok=True) - with open(rate_log, "a", encoding="utf-8") as f: - f.write(f"{datetime.now().isoformat()} | {reason}: {detail}\n") + rate_log = TRIGGER_ROOT / "logs" / "rate_limited.jsonl" + _append_jsonl(rate_log, {"ts": datetime.now().isoformat(), "reason": reason, "detail": detail}) except Exception as exc: _log_warning(f"rate log write failed ({reason}): {exc}") diff --git a/src/aipass/trigger/apps/handlers/events/memory_pool.py b/src/aipass/trigger/apps/handlers/events/memory_pool.py index 50f5ebec..571c48b0 100644 --- a/src/aipass/trigger/apps/handlers/events/memory_pool.py +++ b/src/aipass/trigger/apps/handlers/events/memory_pool.py @@ -24,24 +24,27 @@ Event data expected: - error: str | None — error message if success=False """ -from datetime import datetime, timezone from typing import Any from aipass.trigger.apps.config import TRIGGER_ROOT from aipass.trigger.apps.handlers.json import json_handler -_HANDLER_LOG = TRIGGER_ROOT / "logs" / "memory_pool_handler.log" +try: + from aipass.prax import append_jsonl as _append_jsonl +except Exception: + _append_jsonl = None + +_HANDLER_LOG = TRIGGER_ROOT / "logs" / "memory_pool_handler.jsonl" def _log_warning(message: str) -> None: - """Log warning to file (event handlers cannot import prax logger — causes recursion).""" + """Log warning to file (recursion-safe prax path).""" + if _append_jsonl is None: + return try: - _HANDLER_LOG.parent.mkdir(parents=True, exist_ok=True) - ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") - with open(_HANDLER_LOG, "a", encoding="utf-8") as f: - f.write(f"{ts} | WARNING | {message}\n") + _append_jsonl(_HANDLER_LOG, {"level": "WARNING", "msg": message}) except Exception: - pass # Meta-logging: cannot log a failure to log + pass # seedgo:bypass meta-logging def handle_memory_pool_auto_processed( diff --git a/src/aipass/trigger/apps/handlers/events/plan_file.py b/src/aipass/trigger/apps/handlers/events/plan_file.py index e95939e0..b567d989 100644 --- a/src/aipass/trigger/apps/handlers/events/plan_file.py +++ b/src/aipass/trigger/apps/handlers/events/plan_file.py @@ -29,6 +29,11 @@ from typing import Optional from aipass.trigger.apps.config import TRIGGER_ROOT, AIPASS_PKG_ROOT, atomic_write_json from aipass.trigger.apps.handlers.json import json_handler +try: + from aipass.prax import append_jsonl as _append_jsonl +except Exception: + _append_jsonl = None + def _find_repo_root() -> Path: """Walk up from this file to find the repo root (contains AIPASS_REGISTRY.json).""" @@ -45,21 +50,19 @@ REPO_ROOT = _find_repo_root() FLOW_JSON_DIR = AIPASS_PKG_ROOT / "flow" / "flow_json" REGISTRY_FILE = FLOW_JSON_DIR / "PLAN_REGISTRY.json" -# Log file for handler errors (no Prax imports in handlers - causes recursion) -HANDLER_LOG = TRIGGER_ROOT / "logs" / "plan_file_handler.log" +HANDLER_LOG = TRIGGER_ROOT / "logs" / "plan_file_handler.jsonl" MODULE_NAME = "trigger.plan_file" def _log_error(message: str) -> None: - """Log error to file (handlers cannot import Prax logger - causes recursion)""" + """Log error to file (recursion-safe prax path).""" + if _append_jsonl is None: + return try: - HANDLER_LOG.parent.mkdir(parents=True, exist_ok=True) - timestamp = datetime.now(timezone.utc).isoformat() - with open(HANDLER_LOG, "a", encoding="utf-8") as f: - f.write(f"[{timestamp}] [{MODULE_NAME}] {message}\n") + _append_jsonl(HANDLER_LOG, {"level": "ERROR", "module": MODULE_NAME, "msg": message}) except Exception: - pass # Last resort - cannot fail on logging failure + pass # seedgo:bypass meta-logging def _load_registry() -> dict: diff --git a/src/aipass/trigger/apps/handlers/events/pr_status_sync.py b/src/aipass/trigger/apps/handlers/events/pr_status_sync.py index d9cf6c2c..762153d2 100644 --- a/src/aipass/trigger/apps/handlers/events/pr_status_sync.py +++ b/src/aipass/trigger/apps/handlers/events/pr_status_sync.py @@ -20,24 +20,27 @@ Events: """ import subprocess -from datetime import datetime, timezone from typing import Any from aipass.trigger.apps.config import TRIGGER_ROOT from aipass.trigger.apps.handlers.json import json_handler -_HANDLER_LOG = TRIGGER_ROOT / "logs" / "pr_status_sync_handler.log" +try: + from aipass.prax import append_jsonl as _append_jsonl +except Exception: + _append_jsonl = None + +_HANDLER_LOG = TRIGGER_ROOT / "logs" / "pr_status_sync_handler.jsonl" def _log_info(message: str) -> None: - """Log to file (event handlers cannot import prax logger — causes recursion).""" + """Log to file (recursion-safe prax path).""" + if _append_jsonl is None: + return try: - _HANDLER_LOG.parent.mkdir(parents=True, exist_ok=True) - ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") - with open(_HANDLER_LOG, "a", encoding="utf-8") as f: - f.write(f"{ts} | INFO | {message}\n") + _append_jsonl(_HANDLER_LOG, {"level": "INFO", "msg": message}) except Exception: - pass # Meta-logging: cannot log a failure to log + pass # seedgo:bypass meta-logging def _run_status_sync(reason: str) -> None: diff --git a/src/aipass/trigger/apps/handlers/events/registry.py b/src/aipass/trigger/apps/handlers/events/registry.py index 81aa4373..d39cb970 100644 --- a/src/aipass/trigger/apps/handlers/events/registry.py +++ b/src/aipass/trigger/apps/handlers/events/registry.py @@ -8,23 +8,25 @@ """Event Handler Registry - Setup all event handlers on startup""" -from datetime import datetime, timezone - from aipass.trigger.apps.handlers.json import json_handler from aipass.trigger.apps.config import TRIGGER_ROOT -_HANDLER_LOG = TRIGGER_ROOT / "logs" / "registry_handler.log" +try: + from aipass.prax import append_jsonl as _append_jsonl +except Exception: + _append_jsonl = None + +_HANDLER_LOG = TRIGGER_ROOT / "logs" / "registry_handler.jsonl" def _log_warning(message: str) -> None: - """Log warning to file (event handlers cannot import Prax logger — causes recursion).""" + """Log warning to file (recursion-safe prax path).""" + if _append_jsonl is None: + return try: - _HANDLER_LOG.parent.mkdir(parents=True, exist_ok=True) - ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") - with open(_HANDLER_LOG, "a", encoding="utf-8") as f: - f.write(f"{ts} | WARNING | {message}\n") + _append_jsonl(_HANDLER_LOG, {"level": "WARNING", "msg": message}) except Exception: - pass # Meta-logging: cannot log a failure to log + pass # seedgo:bypass meta-logging def setup_handlers(): diff --git a/src/aipass/trigger/apps/handlers/events/startup.py b/src/aipass/trigger/apps/handlers/events/startup.py index 5aedbb73..9dfa493a 100644 --- a/src/aipass/trigger/apps/handlers/events/startup.py +++ b/src/aipass/trigger/apps/handlers/events/startup.py @@ -21,14 +21,19 @@ import json import hashlib import time from pathlib import Path -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta from typing import Any, Callable, Dict, List, Optional, Set from aipass.trigger.apps.config import TRIGGER_ROOT, atomic_write_json from aipass.trigger.apps.handlers.json import json_handler +try: + from aipass.prax import append_jsonl as _append_jsonl +except Exception: + _append_jsonl = None + SYSTEM_LOGS_DIR = TRIGGER_ROOT.parent.parent.parent / "system_logs" TRIGGER_DATA_FILE = TRIGGER_ROOT / "trigger_json" / "trigger_data.json" -SUPPRESSED_LOG = TRIGGER_ROOT / "logs" / "medic_suppressed.log" +SUPPRESSED_LOG = TRIGGER_ROOT / "logs" / "medic_suppressed.jsonl" MAX_HASHES = 500 MAX_LOOKBACK_HOURS = 24 @@ -38,18 +43,17 @@ MAX_ERRORS_PER_SCAN = 50 # Stop after this many new errors found MAX_FILE_SIZE_BYTES = 512_000 # Skip files larger than 500KB SCAN_TIME_BUDGET_SECONDS = 5.0 # Abort entire scan after this many seconds -_HANDLER_LOG = TRIGGER_ROOT / "logs" / "startup_handler.log" +_HANDLER_LOG = TRIGGER_ROOT / "logs" / "startup_handler.jsonl" def _log_warning(message: str) -> None: - """Log warning to file (event handlers cannot import Prax logger - causes recursion).""" + """Log warning to file (recursion-safe prax path).""" + if _append_jsonl is None: + return try: - _HANDLER_LOG.parent.mkdir(parents=True, exist_ok=True) - ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") - with open(_HANDLER_LOG, "a", encoding="utf-8") as f: - f.write(f"{ts} | WARNING | {message}\n") + _append_jsonl(_HANDLER_LOG, {"level": "WARNING", "msg": message}) except Exception: - pass + pass # seedgo:bypass meta-logging def _load_trigger_data() -> Dict[str, Any]: @@ -96,18 +100,13 @@ def _save_trigger_data(data: Dict[str, Any]) -> None: def _log_suppression(reason: str) -> None: - """Log a catchup suppression event to medic_suppressed.log. - - Args: - reason: Description of why scanning was capped or skipped - """ + """Log a catchup suppression event to medic_suppressed.jsonl.""" + if _append_jsonl is None: + return 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()} | error_catchup: {reason}\n") + _append_jsonl(SUPPRESSED_LOG, {"ts": datetime.now().isoformat(), "source": "error_catchup", "reason": reason}) except Exception as exc: _log_warning(f"log suppression write failed: {exc}") - return def _generate_error_hash(source_module: str, message: str) -> str: diff --git a/src/aipass/trigger/apps/handlers/json/json_handler.py b/src/aipass/trigger/apps/handlers/json/json_handler.py index c0fe6ae4..be56d7d2 100644 --- a/src/aipass/trigger/apps/handlers/json/json_handler.py +++ b/src/aipass/trigger/apps/handlers/json/json_handler.py @@ -12,12 +12,17 @@ import json import os import sys from pathlib import Path -from datetime import datetime, timezone +from datetime import datetime from typing import Dict, Any, Optional import inspect from aipass.trigger.apps.config import atomic_write_json +try: + from aipass.prax import append_jsonl as _append_jsonl +except Exception: + _append_jsonl = None + if sys.platform == "win32": os.environ.setdefault("PYTHONUTF8", "1") for _stream in (sys.stdout, sys.stderr): @@ -28,19 +33,19 @@ if sys.platform == "win32": # Infrastructure — redirect to temp dir during tests _test_log_dir = os.environ.get("AIPASS_TEST_LOG_DIR") if _test_log_dir: - _LOG_FILE = Path(_test_log_dir) / "trigger" / "json_handler.log" + _LOG_FILE = Path(_test_log_dir) / "trigger" / "json_handler.jsonl" else: - _LOG_FILE = Path(__file__).parent.parent.parent.parent / "logs" / "json_handler.log" + _LOG_FILE = Path(__file__).parent.parent.parent.parent / "logs" / "json_handler.jsonl" def _log_warning(msg: str) -> None: - """File-based warning logger to avoid circular imports with prax.""" + """Recursion-safe warning logger via prax append_jsonl.""" + if _append_jsonl is None: + return try: - _LOG_FILE.parent.mkdir(parents=True, exist_ok=True) - with open(_LOG_FILE, "a", encoding="utf-8") as f: - f.write(f"{datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} | WARNING | {msg}\n") + _append_jsonl(_LOG_FILE, {"level": "WARNING", "msg": msg}) except Exception: - pass # Meta-logging: cannot log a failure to log + pass # seedgo:bypass meta-logging # Constants diff --git a/src/aipass/trigger/apps/handlers/log_watcher.py b/src/aipass/trigger/apps/handlers/log_watcher.py index a9dd1c0d..1d6d0031 100644 --- a/src/aipass/trigger/apps/handlers/log_watcher.py +++ b/src/aipass/trigger/apps/handlers/log_watcher.py @@ -48,8 +48,8 @@ STALE_ENTRY_THRESHOLD_SECONDS = 300 # 5 minutes # Compared case-insensitively against Path.name (see on_modified) EXCLUDED_LOG_FILES: Set[str] = { "dispatch.log", - "medic_suppressed.log", - "rate_limited.log", + "medic_suppressed.jsonl", + "rate_limited.jsonl", "error_monitor.log", "log_watcher.log", "log_watcher.log.1", diff --git a/src/aipass/trigger/apps/handlers/medic_state.py b/src/aipass/trigger/apps/handlers/medic_state.py index 7de270bb..333b59ff 100644 --- a/src/aipass/trigger/apps/handlers/medic_state.py +++ b/src/aipass/trigger/apps/handlers/medic_state.py @@ -28,8 +28,8 @@ from aipass.trigger.apps.handlers.json import json_handler logger = get_direct_logger() TRIGGER_CONFIG_FILE = TRIGGER_ROOT / "trigger_json" / "trigger_config.json" -MEDIC_SUPPRESSED_LOG = TRIGGER_ROOT / "logs" / "medic_suppressed.log" -RATE_LIMITED_LOG = TRIGGER_ROOT / "logs" / "rate_limited.log" +MEDIC_SUPPRESSED_LOG = TRIGGER_ROOT / "logs" / "medic_suppressed.jsonl" +RATE_LIMITED_LOG = TRIGGER_ROOT / "logs" / "rate_limited.jsonl" def read_config() -> dict: @@ -190,8 +190,8 @@ def get_suppression_stats() -> Dict[str, Any]: lines = MEDIC_SUPPRESSED_LOG.read_text(encoding="utf-8").strip().splitlines() suppressed_count = len(lines) if lines: - last_line = lines[-1] - last_suppressed = last_line.split(" | ")[0] if " | " in last_line else "unknown" + entry = json.loads(lines[-1]) + last_suppressed = entry.get("ts", "unknown") except Exception as exc: logger.warning("get_suppression_stats failed: %s", exc) return {"suppressed_count": 0, "last_suppressed": "error reading log"} @@ -216,8 +216,8 @@ def get_rate_limit_stats() -> Dict[str, Any]: lines = RATE_LIMITED_LOG.read_text(encoding="utf-8").strip().splitlines() dispatch_count = len(lines) if lines: - last_line = lines[-1] - last_dispatch = last_line.split(" | ")[0] if " | " in last_line else "unknown" + entry = json.loads(lines[-1]) + last_dispatch = entry.get("ts", "unknown") except Exception as exc: logger.warning("get_rate_limit_stats failed: %s", exc) return {"rate_limited_count": 0, "last_rate_limited": "error reading log"} diff --git a/src/aipass/trigger/apps/modules/medic.py b/src/aipass/trigger/apps/modules/medic.py index 72a813d4..8386a82e 100644 --- a/src/aipass/trigger/apps/modules/medic.py +++ b/src/aipass/trigger/apps/modules/medic.py @@ -198,11 +198,11 @@ def print_help() -> None: console.print() console.print(" [yellow]off[/yellow] Global kill switch. ALL error dispatch stops. No branch") console.print(" receives auto-healing emails. Errors still logged to") - console.print(" medic_suppressed.log for review.") + console.print(" medic_suppressed.jsonl for review.") console.print() console.print(" [yellow]mute[/yellow] Per-branch suppress. Only the muted branch stops receiving") console.print(" dispatch. All other branches continue normally. Muted errors") - console.print(" logged to medic_suppressed.log.") + console.print(" logged to medic_suppressed.jsonl.") console.print() console.rule("EXAMPLES") console.print() @@ -227,7 +227,7 @@ def print_help() -> None: console.print(" -> handler checks medic_enabled -> checks branch mute list") console.print(" -> dispatches fix-it email to affected branch (or suppresses)") console.print() - console.print(" Suppressed errors: trigger/logs/medic_suppressed.log") + console.print(" Suppressed errors: trigger/logs/medic_suppressed.jsonl") console.print() @@ -292,7 +292,7 @@ def _handle_status(console) -> None: console.print(f" Last rate limit: {rate_limits['last_rate_limited']}") console.print() if not enabled: - console.print(" [dim]All error dispatch suppressed. Errors logged to medic_suppressed.log[/dim]") + console.print(" [dim]All error dispatch suppressed. Errors logged to medic_suppressed.jsonl[/dim]") def _handle_on(console) -> None: @@ -342,7 +342,7 @@ def _handle_off(console) -> None: Panel( "[bold yellow]Medic DISABLED[/bold yellow]\n\n" "Error dispatch is [yellow]suppressed[/yellow]. Errors are still detected\n" - "and logged to [dim]medic_suppressed.log[/dim] for review.\n" + "and logged to [dim]medic_suppressed.jsonl[/dim] for review.\n" "Log watcher: [yellow]stopped[/yellow]", title="Medic", border_style="yellow", diff --git a/src/aipass/trigger/tests/test_log_watcher.py b/src/aipass/trigger/tests/test_log_watcher.py index a658e865..76909cee 100644 --- a/src/aipass/trigger/tests/test_log_watcher.py +++ b/src/aipass/trigger/tests/test_log_watcher.py @@ -1073,10 +1073,10 @@ class TestShouldProcessEdgeCases: assert watcher._should_process(path) is False def test_excluded_medic_suppressed(self): - """medic_suppressed.log is excluded.""" + """medic_suppressed.jsonl is excluded.""" lw = _import_log_watcher() watcher = lw.BranchLogWatcher() - path = str(Path("/src") / "aipass" / "flow" / "logs" / "medic_suppressed.log") + path = str(Path("/src") / "aipass" / "flow" / "logs" / "medic_suppressed.jsonl") assert watcher._should_process(path) is False diff --git a/src/aipass/trigger/tests/test_medic.py b/src/aipass/trigger/tests/test_medic.py index 4cb98ea9..7ec44cf0 100644 --- a/src/aipass/trigger/tests/test_medic.py +++ b/src/aipass/trigger/tests/test_medic.py @@ -307,7 +307,7 @@ def test_handle_command_status_suppression_hint_when_disabled(): console = _get_console() printed = _get_print_str_args(console) - hint = " [dim]All error dispatch suppressed. Errors logged to medic_suppressed.log[/dim]" + hint = " [dim]All error dispatch suppressed. Errors logged to medic_suppressed.jsonl[/dim]" assert hint in printed, f"Expected suppression hint '{hint}' in printed args: {printed}" diff --git a/src/aipass/trigger/tests/test_medic_state.py b/src/aipass/trigger/tests/test_medic_state.py index ffc7074c..33486750 100644 --- a/src/aipass/trigger/tests/test_medic_state.py +++ b/src/aipass/trigger/tests/test_medic_state.py @@ -63,8 +63,8 @@ def state_mod(tmp_path, monkeypatch): import aipass.trigger.apps.handlers.medic_state as mod config_file = tmp_path / "trigger_json" / "trigger_config.json" - suppressed_log = tmp_path / "logs" / "medic_suppressed.log" - rate_limited_log = tmp_path / "logs" / "rate_limited.log" + suppressed_log = tmp_path / "logs" / "medic_suppressed.jsonl" + rate_limited_log = tmp_path / "logs" / "rate_limited.jsonl" monkeypatch.setattr(mod, "TRIGGER_CONFIG_FILE", config_file) monkeypatch.setattr(mod, "MEDIC_SUPPRESSED_LOG", suppressed_log) @@ -424,26 +424,26 @@ class TestGetSuppressionStats: assert result["last_suppressed"] == "never" def test_populated_log(self, state_mod): - """get_suppression_stats parses log lines and returns correct stats.""" + """get_suppression_stats parses JSONL lines and returns correct stats.""" log_file = state_mod.MEDIC_SUPPRESSED_LOG log_file.parent.mkdir(parents=True, exist_ok=True) lines = [ - "2026-04-01 10:00:00 | ImportError | FLOW", - "2026-04-02 11:00:00 | TimeoutError | API", - "2026-04-03 12:00:00 | ValueError | DRONE", + '{"ts": "2026-04-01T10:00:00", "reason": "count<2", "branch": "FLOW"}', + '{"ts": "2026-04-02T11:00:00", "reason": "count<2", "branch": "API"}', + '{"ts": "2026-04-03T12:00:00", "reason": "count<2", "branch": "DRONE"}', ] log_file.write_text("\n".join(lines), encoding="utf-8") result = state_mod.get_suppression_stats() assert result["suppressed_count"] == 3 - assert result["last_suppressed"] == "2026-04-03 12:00:00" + assert result["last_suppressed"] == "2026-04-03T12:00:00" - def test_log_line_without_pipe_separator(self, state_mod): - """get_suppression_stats returns 'unknown' when last line has no pipe.""" + def test_log_line_without_ts_field(self, state_mod): + """get_suppression_stats returns 'unknown' when last entry has no ts.""" log_file = state_mod.MEDIC_SUPPRESSED_LOG log_file.parent.mkdir(parents=True, exist_ok=True) - log_file.write_text("malformed line without pipe", encoding="utf-8") + log_file.write_text('{"reason": "no timestamp"}', encoding="utf-8") result = state_mod.get_suppression_stats() @@ -478,25 +478,25 @@ class TestGetRateLimitStats: assert result["last_rate_limited"] == "never" def test_populated_log(self, state_mod): - """get_rate_limit_stats parses log lines and returns correct stats.""" + """get_rate_limit_stats parses JSONL lines and returns correct stats.""" log_file = state_mod.RATE_LIMITED_LOG log_file.parent.mkdir(parents=True, exist_ok=True) lines = [ - "2026-04-01 08:00:00 | ImportError | fp123", - "2026-04-02 09:00:00 | TimeoutError | fp456", + '{"ts": "2026-04-01T08:00:00", "reason": "backoff", "detail": "fp123"}', + '{"ts": "2026-04-02T09:00:00", "reason": "backoff", "detail": "fp456"}', ] log_file.write_text("\n".join(lines), encoding="utf-8") result = state_mod.get_rate_limit_stats() assert result["rate_limited_count"] == 2 - assert result["last_rate_limited"] == "2026-04-02 09:00:00" + assert result["last_rate_limited"] == "2026-04-02T09:00:00" - def test_log_line_without_pipe_separator(self, state_mod): - """get_rate_limit_stats returns 'unknown' when last line has no pipe.""" + def test_log_line_without_ts_field(self, state_mod): + """get_rate_limit_stats returns 'unknown' when last entry has no ts.""" log_file = state_mod.RATE_LIMITED_LOG log_file.parent.mkdir(parents=True, exist_ok=True) - log_file.write_text("malformed line", encoding="utf-8") + log_file.write_text('{"reason": "no timestamp"}', encoding="utf-8") result = state_mod.get_rate_limit_stats() diff --git a/src/aipass/trigger/tests/test_memory_pool_handler.py b/src/aipass/trigger/tests/test_memory_pool_handler.py index 7a9efe3d..5dcb2d7f 100644 --- a/src/aipass/trigger/tests/test_memory_pool_handler.py +++ b/src/aipass/trigger/tests/test_memory_pool_handler.py @@ -192,7 +192,7 @@ class TestHandleMemoryPoolAutoProcessedFailure: error="pool write failed", ) - log_file = tmp_path / "logs" / "memory_pool_handler.log" + log_file = tmp_path / "logs" / "memory_pool_handler.jsonl" assert log_file.exists() content = log_file.read_text() assert "pool write failed" in content