Merge pull request #80 from AIOSAI/feat/ai_mail-seedgo-compliance-jsonhandler-and-intros

feat(ai_mail): seedgo compliance — json_handler and introspection updates
This commit is contained in:
AIPass
2026-03-17 21:33:40 -07:00
committed by GitHub
33 changed files with 136 additions and 42 deletions
@@ -36,6 +36,8 @@ from pathlib import Path
from datetime import datetime from datetime import datetime
from typing import Dict, Any, List, Tuple from typing import Dict, Any, List, Tuple
from aipass.ai_mail.apps.handlers.json import json_handler
# ============================================================================= # =============================================================================
# CONSTANTS # CONSTANTS
@@ -290,6 +292,8 @@ def update_central() -> Dict[str, Any]:
>>> print(stats["system_totals"]["total_unread"]) >>> print(stats["system_totals"]["total_unread"])
5 5
""" """
json_handler.log_operation("update_central", {"target": str(CENTRAL_FILE)})
# Aggregate statistics from all branches # Aggregate statistics from all branches
branch_stats = aggregate_branch_stats() branch_stats = aggregate_branch_stats()
@@ -32,6 +32,7 @@ from urllib.request import Request, urlopen
from urllib.error import URLError from urllib.error import URLError
from aipass.prax.apps.modules.logger import system_logger as logger from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.ai_mail.apps.handlers.json import json_handler
def _find_repo_root() -> Path: def _find_repo_root() -> Path:
@@ -600,6 +601,8 @@ def run_daemon() -> None:
Exits gracefully on SIGTERM/SIGINT or kill switch. Exits gracefully on SIGTERM/SIGINT or kill switch.
""" """
json_handler.log_operation("run_daemon", {"pid": os.getpid()})
if not _write_pid_file(): if not _write_pid_file():
return return
@@ -28,6 +28,7 @@ import time
from pathlib import Path from pathlib import Path
from aipass.prax.apps.modules.logger import system_logger as logger from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.ai_mail.apps.handlers.json import json_handler
def _send_bounce(branch_email: str, reason: str, sender: str, def _send_bounce(branch_email: str, reason: str, sender: str,
@@ -98,6 +99,8 @@ def main():
logger.warning("[monitor] No claude command after --") logger.warning("[monitor] No claude command after --")
sys.exit(1) sys.exit(1)
json_handler.log_operation("dispatch_monitor_start", {"branch": branch_email, "sender": sender})
# Open stderr log for claude output # Open stderr log for claude output
try: try:
stderr_fh = open(stderr_log, 'a', encoding='utf-8') stderr_fh = open(stderr_log, 'a', encoding='utf-8')
@@ -19,6 +19,8 @@ from pathlib import Path
from datetime import datetime from datetime import datetime
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from aipass.ai_mail.apps.handlers.json import json_handler
# Dispatch log location (package-relative) # Dispatch log location (package-relative)
_AI_MAIL_DIR = Path(__file__).resolve().parents[3] # ai_mail/ _AI_MAIL_DIR = Path(__file__).resolve().parents[3] # ai_mail/
DISPATCH_LOG_FILE = _AI_MAIL_DIR / ".ai_mail.local" / "dispatch_log.json" DISPATCH_LOG_FILE = _AI_MAIL_DIR / ".ai_mail.local" / "dispatch_log.json"
@@ -71,6 +73,8 @@ def log_dispatch(branch: str, pid: Optional[int], status: str, error_msg: Option
Returns: Returns:
True if logged successfully True if logged successfully
""" """
json_handler.log_operation("log_dispatch", {"branch": branch, "status": status})
dispatches = load_dispatch_log() dispatches = load_dispatch_log()
entry: Dict[str, Any] = { entry: Dict[str, Any] = {
@@ -25,6 +25,7 @@ from pathlib import Path
from typing import Optional, Tuple, List from typing import Optional, Tuple, List
from aipass.prax.apps.modules.logger import system_logger as logger from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.ai_mail.apps.handlers.json import json_handler
def _find_repo_root() -> Path: def _find_repo_root() -> Path:
@@ -316,6 +317,8 @@ def wake_branch(branch_email: str, custom_message: Optional[str] = None,
Returns: Returns:
Tuple of (DispatchStatus with all steps, overall success bool) Tuple of (DispatchStatus with all steps, overall success bool)
""" """
json_handler.log_operation("wake_branch", {"branch": branch_email, "fresh": fresh, "auto": auto})
status = DispatchStatus() status = DispatchStatus()
# Step 1: Pause check (auto-dispatch only) # Step 1: Pause check (auto-dispatch only)
@@ -17,6 +17,7 @@ from pathlib import Path
from typing import List, Tuple, Callable, Optional from typing import List, Tuple, Callable, Optional
from aipass.prax import logger from aipass.prax import logger
from aipass.ai_mail.apps.handlers.json import json_handler
def batch_close( def batch_close(
@@ -36,6 +37,7 @@ def batch_close(
Tuple of (results_list, closed_count, failed_count) Tuple of (results_list, closed_count, failed_count)
results_list contains (message_id, success, message) tuples results_list contains (message_id, success, message) tuples
""" """
json_handler.log_operation("batch_close", {"branch_path": str(branch_path), "count": len(message_ids)})
batch_mode = len(message_ids) > 1 batch_mode = len(message_ids) > 1
results = [] results = []
closed_count = 0 closed_count = 0
@@ -16,9 +16,10 @@ Independent handler - no module dependencies.
import json import json
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
from typing import Dict from typing import Dict, Optional
from aipass.prax.apps.modules.logger import system_logger as logger from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.ai_mail.apps.handlers.json import json_handler
# Lazy imports # Lazy imports
_append_footer = None _append_footer = None
@@ -52,6 +53,7 @@ def create_email_file(to_branch: str, subject: str, message: str, user_info: Dic
Returns: Returns:
Path to created email file in sent folder Path to created email file in sent folder
""" """
json_handler.log_operation("create_email_file", {"to": to_branch, "subject": subject})
timestamp = datetime.now() timestamp = datetime.now()
timestamp_str = timestamp.strftime(user_info.get("timestamp_format", "%Y-%m-%d %H:%M:%S")) timestamp_str = timestamp.strftime(user_info.get("timestamp_format", "%Y-%m-%d %H:%M:%S"))
@@ -110,7 +112,7 @@ def _trigger_sent_purge(mailbox_path: Path) -> None:
logger.warning("[create] _trigger_sent_purge() failed: %s", e) logger.warning("[create] _trigger_sent_purge() failed: %s", e)
def load_email_file(email_file: Path) -> Dict | None: def load_email_file(email_file: Path) -> Optional[Dict]:
""" """
Load email data from file. Load email data from file.
@@ -158,7 +160,7 @@ if __name__ == "__main__":
c.print() c.print()
c.print("FUNCTIONS PROVIDED:") c.print("FUNCTIONS PROVIDED:")
c.print(" - create_email_file(to_branch, subject, message, user_info) -> Path") c.print(" - create_email_file(to_branch, subject, message, user_info) -> Path")
c.print(" - load_email_file(email_file) -> Dict | None") c.print(" - load_email_file(email_file) -> Optional[Dict]")
c.print(" - sanitize_subject(subject, max_length) -> str") c.print(" - sanitize_subject(subject, max_length) -> str")
c.print() c.print()
c.print("HANDLER CHARACTERISTICS:") c.print("HANDLER CHARACTERISTICS:")
@@ -22,6 +22,7 @@ from datetime import datetime
from typing import Dict, Optional from typing import Dict, Optional
from aipass.prax.apps.modules.logger import system_logger as logger from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.ai_mail.apps.handlers.json import json_handler
# Lazy-loaded write_section reference # Lazy-loaded write_section reference
_write_section = None _write_section = None
@@ -155,6 +156,7 @@ def push_dashboard_update(branch_path: Path) -> bool:
Returns: Returns:
True if update succeeded, False on any error (never raises) True if update succeeded, False on any error (never raises)
""" """
json_handler.log_operation("dashboard_sync", {"branch_path": str(branch_path)})
try: try:
branch_path = Path(branch_path) branch_path = Path(branch_path)
@@ -21,6 +21,7 @@ from typing import Dict, Tuple, List, Optional, Callable
from aipass.prax.apps.modules.logger import system_logger as logger from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.ai_mail.apps.handlers.json_utils.json_handler import load_json, save_json from aipass.ai_mail.apps.handlers.json_utils.json_handler import load_json, save_json
from aipass.ai_mail.apps.handlers.json import json_handler
def _find_repo_root() -> Path: def _find_repo_root() -> Path:
@@ -242,6 +243,8 @@ def deliver_email_to_branch(
Tuple of (success: bool, error_message: str) Tuple of (success: bool, error_message: str)
error_message is empty string if successful error_message is empty string if successful
""" """
json_handler.log_operation("deliver_email", {"to": to_branch, "subject": email_data.get("subject", "")})
# Handle path input from DRONE's @ resolution # Handle path input from DRONE's @ resolution
if to_branch.startswith('/'): if to_branch.startswith('/'):
branches_list = get_all_branches() branches_list = get_all_branches()
@@ -18,6 +18,7 @@ from datetime import datetime
from typing import Dict, Any, Callable, Optional from typing import Dict, Any, Callable, Optional
from aipass.prax import logger from aipass.prax import logger
from aipass.ai_mail.apps.handlers.json import json_handler
def build_error_report(to_branch: str, subject: str, error_msg: str) -> Dict[str, Any]: def build_error_report(to_branch: str, subject: str, error_msg: str) -> Dict[str, Any]:
@@ -75,6 +76,8 @@ def dispatch_send_error(
Returns: Returns:
True if error dispatched successfully, False otherwise. True if error dispatched successfully, False otherwise.
""" """
json_handler.log_operation("dispatch_send_error", {"to_branch": to_branch, "subject": subject})
try: try:
email_data = build_error_report(to_branch, subject, error_msg) email_data = build_error_report(to_branch, subject, error_msg)
deliver_fn("@drone", email_data) deliver_fn("@drone", email_data)
@@ -14,6 +14,8 @@ Reminds branches of process steps: Seed audit, memory update, FPLAN close, confi
Independent handler - no module dependencies. Independent handler - no module dependencies.
""" """
from aipass.ai_mail.apps.handlers.json import json_handler
# Standard footer for all outgoing emails # Standard footer for all outgoing emails
STANDARD_FOOTER = """ STANDARD_FOOTER = """
--- ---
@@ -47,6 +49,7 @@ def append_footer(message: str) -> str:
Returns: Returns:
Message with footer appended Message with footer appended
""" """
json_handler.log_operation("append_footer", {"message_length": len(message)})
return message + get_footer() return message + get_footer()
@@ -18,6 +18,8 @@ from pathlib import Path
from typing import Dict, Optional from typing import Dict, Optional
from aipass.prax.apps.modules.logger import system_logger as logger from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.ai_mail.apps.handlers.json import json_handler
def _find_repo_root() -> Path: def _find_repo_root() -> Path:
"""Walk up from this file to find AIPASS_REGISTRY.json (repo root).""" """Walk up from this file to find AIPASS_REGISTRY.json (repo root)."""
@@ -104,6 +106,7 @@ def format_email_header(email_data: Dict) -> str:
Returns: Returns:
Formatted header string Formatted header string
""" """
json_handler.log_operation("format_email_header", {"subject": email_data.get("subject", "")})
sender = format_sender_display( sender = format_sender_display(
email_data.get('from_name', 'Unknown'), email_data.get('from_name', 'Unknown'),
email_data.get('from', 'unknown') email_data.get('from', 'unknown')
@@ -8,6 +8,8 @@
from pathlib import Path from pathlib import Path
from aipass.ai_mail.apps.handlers.json import json_handler
""" """
Email Header Handler Email Header Handler
@@ -68,6 +70,7 @@ def prepend_dispatch_header(message: str, no_memory_save: bool = False) -> str:
Returns: Returns:
Message with dispatch header prepended Message with dispatch header prepended
""" """
json_handler.log_operation("prepend_dispatch_header", {"no_memory_save": no_memory_save})
return get_dispatch_header(no_memory_save=no_memory_save) + message return get_dispatch_header(no_memory_save=no_memory_save) + message
@@ -21,6 +21,7 @@ from datetime import datetime
from typing import Dict, Tuple, Optional, Any from typing import Dict, Tuple, Optional, Any
from aipass.prax.apps.modules.logger import system_logger as logger from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.ai_mail.apps.handlers.json import json_handler
# Lazy import for inbox file lock # Lazy import for inbox file lock
@@ -45,7 +46,7 @@ def _get_console() -> Any:
def _get_update_section() -> Any: def _get_update_section() -> Any:
"""Lazy import update_section.""" """Lazy import update_section."""
try: try:
from aipass.devpulse.apps.modules.dashboard import update_section from aipass.devpulse.apps.modules.dashboard import update_section # type: ignore[import-not-found]
return update_section return update_section
except ImportError: except ImportError:
return None return None
@@ -154,6 +155,7 @@ def mark_read_and_archive(branch_path: Path, message_id: str) -> Tuple[bool, str
Returns: Returns:
Tuple of (success: bool, message: str) Tuple of (success: bool, message: str)
""" """
json_handler.log_operation("inbox_cleanup", {"branch_path": str(branch_path), "message_id": message_id})
mailbox_path = branch_path / ".ai_mail.local" mailbox_path = branch_path / ".ai_mail.local"
inbox_file = mailbox_path / "inbox.json" inbox_file = mailbox_path / "inbox.json"
@@ -23,6 +23,8 @@ import sys
from pathlib import Path from pathlib import Path
from contextlib import contextmanager from contextlib import contextmanager
from aipass.ai_mail.apps.handlers.json import json_handler
# fcntl is POSIX-only (Linux/macOS). On Windows, use msvcrt for locking. # fcntl is POSIX-only (Linux/macOS). On Windows, use msvcrt for locking.
if sys.platform == "win32": if sys.platform == "win32":
import msvcrt import msvcrt
@@ -49,6 +51,7 @@ def inbox_lock(inbox_file: Path):
Raises: Raises:
OSError: If lock cannot be acquired OSError: If lock cannot be acquired
""" """
json_handler.log_operation("inbox_lock", {"inbox_file": str(inbox_file)})
lock_file = inbox_file.parent / ".inbox.lock" lock_file = inbox_file.parent / ".inbox.lock"
lock_fd = None lock_fd = None
@@ -18,6 +18,7 @@ from pathlib import Path
from typing import Dict from typing import Dict
from aipass.prax.apps.modules.logger import system_logger as logger from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.ai_mail.apps.handlers.json import json_handler
@@ -34,6 +35,7 @@ def load_inbox(inbox_file: Path) -> Dict:
Raises: Raises:
Exception: If file cannot be read or parsed Exception: If file cannot be read or parsed
""" """
json_handler.log_operation("load_inbox", {"inbox_file": str(inbox_file)})
if not inbox_file.exists(): if not inbox_file.exists():
return {"messages": []} return {"messages": []}
@@ -16,6 +16,8 @@ Independent handler - no module or display dependencies.
from pathlib import Path from pathlib import Path
from typing import Dict, Optional, Any, Callable, Tuple from typing import Dict, Optional, Any, Callable, Tuple
from aipass.ai_mail.apps.handlers.json import json_handler
def resolve_inbox_target( def resolve_inbox_target(
args_first: Optional[str], args_first: Optional[str],
@@ -40,6 +42,7 @@ def resolve_inbox_target(
target_branch: str | None (the explicit target, or None for current) target_branch: str | None (the explicit target, or None for current)
error: str | None (set when success is False) error: str | None (set when success is False)
""" """
json_handler.log_operation("resolve_inbox_target", {"target": args_first})
target_branch = None target_branch = None
if args_first and args_first.startswith("@"): if args_first and args_first.startswith("@"):
target_branch = args_first target_branch = args_first
@@ -27,6 +27,7 @@ from datetime import datetime
from typing import Dict, List, Any from typing import Dict, List, Any
from aipass.prax.apps.modules.logger import system_logger as logger from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.ai_mail.apps.handlers.json import json_handler
# Purge configuration # Purge configuration
MAX_EMAILS = 10 MAX_EMAILS = 10
@@ -275,6 +276,7 @@ def run_purge(mailbox_path: Path) -> Dict[str, Any]:
Returns: Returns:
Dict with combined results Dict with combined results
""" """
json_handler.log_operation("run_purge", {"mailbox_path": str(mailbox_path)})
sent_result = purge_sent_folder(mailbox_path) sent_result = purge_sent_folder(mailbox_path)
deleted_result = purge_deleted_folder(mailbox_path) deleted_result = purge_deleted_folder(mailbox_path)
@@ -19,6 +19,7 @@ from typing import Dict, Tuple, Optional
from datetime import datetime from datetime import datetime
from aipass.prax.apps.modules.logger import system_logger as logger from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.ai_mail.apps.handlers.json import json_handler
# Services imported in __main__ only (handlers should not display) # Services imported in __main__ only (handlers should not display)
@@ -70,6 +71,7 @@ def send_reply(
Returns: Returns:
Tuple of (success: bool, message: str, reply_id: str or None) Tuple of (success: bool, message: str, reply_id: str or None)
""" """
json_handler.log_operation("send_reply", {"from_branch": str(from_branch_path), "reply_to": original_email.get("from", "unknown")})
# Import here to avoid circular imports # Import here to avoid circular imports
from aipass.ai_mail.apps.handlers.email.delivery import deliver_email_to_branch, get_all_branches from aipass.ai_mail.apps.handlers.email.delivery import deliver_email_to_branch, get_all_branches
from aipass.ai_mail.apps.handlers.email.inbox_cleanup import mark_as_closed_and_archive from aipass.ai_mail.apps.handlers.email.inbox_cleanup import mark_as_closed_and_archive
@@ -18,7 +18,7 @@ from pathlib import Path
from typing import Optional, Tuple, List, Dict, Any from typing import Optional, Tuple, List, Dict, Any
from aipass.prax import logger from aipass.prax import logger
# logger imported from aipass.prax from aipass.ai_mail.apps.handlers.json import json_handler
def resolve_sender_info( def resolve_sender_info(
@@ -41,6 +41,7 @@ def resolve_sender_info(
Returns: Returns:
Dict with email_address, display_name, mailbox_path, timestamp_format. Dict with email_address, display_name, mailbox_path, timestamp_format.
""" """
json_handler.log_operation("resolve_sender", {"from_branch": from_branch})
if from_branch: if from_branch:
email_addr = f"@{from_branch.lstrip('@').lower()}" email_addr = f"@{from_branch.lstrip('@').lower()}"
branch_info = get_branch_by_email_fn(email_addr) branch_info = get_branch_by_email_fn(email_addr)
@@ -81,13 +82,14 @@ def send_to_broadcast(
on_delivered_callback, on_delivered_callback,
log_operation_fn, log_operation_fn,
update_central_fn, update_central_fn,
) -> Tuple[bool, int, int, Optional[str]]: ) -> Tuple[bool, int, int, Any]:
""" """
Execute broadcast send to all branches. Execute broadcast send to all branches.
Returns: Returns:
Tuple of (success, success_count, total_count, error_msg). Tuple of (success, success_count, total_count, results_or_error).
error_msg is set if the email file could not be loaded. On failure: 4th element is an error string.
On success: 4th element is a list of (branch_name, success, error_msg) tuples.
""" """
email_file = create_email_file_fn("all", subject, message, user_info, reply_to=reply_to, dispatched_to=dispatched_to) email_file = create_email_file_fn("all", subject, message, user_info, reply_to=reply_to, dispatched_to=dispatched_to)
email_data = load_email_file_fn(email_file) email_data = load_email_file_fn(email_file)
@@ -16,6 +16,8 @@ Independent handler - no module or display dependencies.
from pathlib import Path from pathlib import Path
from typing import List, Dict, Optional, Any from typing import List, Dict, Optional, Any
from aipass.ai_mail.apps.handlers.json import json_handler
def parse_send_args(args: List[str]) -> Dict[str, Any]: def parse_send_args(args: List[str]) -> Dict[str, Any]:
""" """
@@ -35,6 +37,7 @@ def parse_send_args(args: List[str]) -> Dict[str, Any]:
mode: 'direct' | 'interactive' | 'error' mode: 'direct' | 'interactive' | 'error'
error: str | None (set when mode=='error') error: str | None (set when mode=='error')
""" """
json_handler.log_operation("parse_send_args", {"arg_count": len(args)})
working_args = list(args) working_args = list(args)
# Extract --dispatch / --auto-execute # Extract --dispatch / --auto-execute
@@ -24,4 +24,5 @@ from aipass.ai_mail.apps.handlers.json_utils.json_handler import ( # noqa: F401
save_json, save_json,
ensure_json_exists, ensure_json_exists,
get_json_path, get_json_path,
log_operation,
) )
@@ -26,6 +26,8 @@ import re
from pathlib import Path from pathlib import Path
from typing import Optional, Dict, Tuple from typing import Optional, Dict, Tuple
from aipass.ai_mail.apps.handlers.json import json_handler
def _find_repo_root() -> Path: def _find_repo_root() -> Path:
"""Walk up from this file to find AIPASS_REGISTRY.json (repo root).""" """Walk up from this file to find AIPASS_REGISTRY.json (repo root)."""
@@ -63,6 +65,8 @@ def parse_error_log_line(log_line: str) -> Optional[Dict]:
Dict with timestamp, logger_name, module_name, level, message Dict with timestamp, logger_name, module_name, level, message
None if not an ERROR line or parsing fails None if not an ERROR line or parsing fails
""" """
json_handler.log_operation("parse_error_log_line", {"log_line_length": len(log_line)})
match = re.match(LOG_PATTERN, log_line.strip()) match = re.match(LOG_PATTERN, log_line.strip())
if not match: if not match:
@@ -23,6 +23,8 @@ Architecture:
# ============================================= # =============================================
from pathlib import Path from pathlib import Path
from aipass.ai_mail.apps.handlers.json import json_handler
# ============================================= # =============================================
# CONSTANTS # CONSTANTS
@@ -107,6 +109,8 @@ def get_health_info(file_path: Path | str) -> dict:
Returns: Returns:
Dict with line_count, status, needs_email Dict with line_count, status, needs_email
""" """
json_handler.log_operation("get_health_info", {"file_path": str(file_path)})
line_count = count_file_lines(file_path) line_count = count_file_lines(file_path)
return { return {
@@ -20,6 +20,7 @@ import subprocess
import sys import sys
from aipass.prax.apps.modules.logger import system_logger as logger from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.ai_mail.apps.handlers.json import json_handler
def send_notification(title: str, body: str, source: str = "ai_mail", def send_notification(title: str, body: str, source: str = "ai_mail",
@@ -36,6 +37,8 @@ def send_notification(title: str, body: str, source: str = "ai_mail",
Returns: Returns:
True if sent, False on failure True if sent, False on failure
""" """
json_handler.log_operation("send_notification", {"title": title, "source": source})
# Primary: dbus direct (bypasses Portal, supports stacking) # Primary: dbus direct (bypasses Portal, supports stacking)
if _send_via_dbus(title, body, source, icon): if _send_via_dbus(title, body, source, icon):
return True return True
@@ -14,6 +14,8 @@ import json
from pathlib import Path from pathlib import Path
from typing import Dict from typing import Dict
from aipass.ai_mail.apps.handlers.json import json_handler
def load_registry(registry_file: Path) -> Dict: def load_registry(registry_file: Path) -> Dict:
""" """
@@ -35,6 +37,8 @@ def load_registry(registry_file: Path) -> Dict:
} }
} }
""" """
json_handler.log_operation("load_registry", {"registry_file": str(registry_file)})
if not registry_file.exists(): if not registry_file.exists():
return { return {
"last_updated": "", "last_updated": "",
@@ -22,7 +22,9 @@ Handler Independence:
import json import json
from pathlib import Path from pathlib import Path
from typing import List, Dict from typing import List, Dict, Optional
from aipass.ai_mail.apps.handlers.json import json_handler
# Constants # Constants
@@ -53,6 +55,8 @@ def get_all_branches() -> List[Dict]:
Note: Note:
Returns empty list if registry not found or on error. Returns empty list if registry not found or on error.
""" """
json_handler.log_operation("get_all_branches", {"registry_path": str(BRANCH_REGISTRY_PATH)})
branches = [] branches = []
if not BRANCH_REGISTRY_PATH.exists(): if not BRANCH_REGISTRY_PATH.exists():
@@ -118,7 +122,7 @@ def _derive_email_from_branch_name(branch_name: str) -> str:
return f"@{email_part}" return f"@{email_part}"
def get_branch_by_email(email: str) -> Dict | None: def get_branch_by_email(email: str) -> Optional[Dict]:
""" """
Get branch information by email address. Get branch information by email address.
@@ -170,7 +174,7 @@ if __name__ == "__main__":
console.print() console.print()
console.print("FUNCTIONS PROVIDED:") console.print("FUNCTIONS PROVIDED:")
console.print(" - get_all_branches() -> List[Dict]") console.print(" - get_all_branches() -> List[Dict]")
console.print(" - get_branch_by_email(email) -> Dict | None") console.print(" - get_branch_by_email(email) -> Optional[Dict]")
console.print(" - get_branch_email_map() -> Dict[str, str]") console.print(" - get_branch_email_map() -> Dict[str, str]")
console.print(" - get_branch_path_map() -> Dict[str, str]") console.print(" - get_branch_path_map() -> Dict[str, str]")
console.print() console.print()
@@ -25,6 +25,8 @@ from pathlib import Path
from datetime import datetime from datetime import datetime
from typing import Dict, Tuple from typing import Dict, Tuple
from aipass.ai_mail.apps.handlers.json import json_handler
# Constants # Constants
MODULE_NAME = "registry.update" MODULE_NAME = "registry.update"
@@ -56,6 +58,8 @@ def ping_registry(
Returns: Returns:
True if registry updated successfully, False otherwise True if registry updated successfully, False otherwise
""" """
json_handler.log_operation("ping_registry", {"branch_name": branch_name, "branch_path": str(branch_path)})
try: try:
# Ensure registry directory exists # Ensure registry directory exists
REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True) REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True)
@@ -19,9 +19,10 @@ Walks up directory tree to find branch root (has .trinity/passport.json).
import os import os
import json import json
from pathlib import Path from pathlib import Path
from typing import Dict from typing import Dict, Optional
from aipass.prax.apps.modules.logger import system_logger as logger from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.ai_mail.apps.handlers.json import json_handler
# ============================================= # =============================================
# CONSTANTS # CONSTANTS
@@ -55,7 +56,7 @@ def _get_branches_list(registry: dict) -> list:
# BRANCH DETECTION FUNCTIONS # BRANCH DETECTION FUNCTIONS
# ============================================= # =============================================
def detect_branch_from_pwd() -> Dict | None: def detect_branch_from_pwd() -> Optional[Dict]:
""" """
Detect which branch is calling based on current working directory. Detect which branch is calling based on current working directory.
@@ -73,6 +74,8 @@ def detect_branch_from_pwd() -> Dict | None:
} }
None if no branch detected None if no branch detected
""" """
json_handler.log_operation("detect_branch_from_pwd", {"cwd": str(Path.cwd())})
try: try:
# Primary: use explicit branch name passed by drone (works in Docker + local) # Primary: use explicit branch name passed by drone (works in Docker + local)
caller_branch = os.environ.get("AIPASS_CALLER_BRANCH") caller_branch = os.environ.get("AIPASS_CALLER_BRANCH")
@@ -102,7 +105,7 @@ def detect_branch_from_pwd() -> Dict | None:
return None return None
def _lookup_branch_by_name(branch_name: str) -> Dict | None: def _lookup_branch_by_name(branch_name: str) -> Optional[Dict]:
""" """
Look up branch in the registry by name (case-insensitive). Look up branch in the registry by name (case-insensitive).
@@ -135,7 +138,7 @@ def _lookup_branch_by_name(branch_name: str) -> Dict | None:
return None return None
def find_branch_root(start_path: Path) -> Path | None: def find_branch_root(start_path: Path) -> Optional[Path]:
""" """
Walk up directory tree to find branch root. Walk up directory tree to find branch root.
@@ -165,7 +168,7 @@ def find_branch_root(start_path: Path) -> Path | None:
return None return None
def get_branch_info_from_registry(branch_path: Path) -> Dict | None: def get_branch_info_from_registry(branch_path: Path) -> Optional[Dict]:
""" """
Look up branch information in BRANCH_REGISTRY.json by path. Look up branch information in BRANCH_REGISTRY.json by path.
@@ -250,9 +253,9 @@ if __name__ == "__main__":
console.print(" Walks up directory tree to find branch root") console.print(" Walks up directory tree to find branch root")
console.print() console.print()
console.print("FUNCTIONS PROVIDED:") console.print("FUNCTIONS PROVIDED:")
console.print(" - detect_branch_from_pwd() -> Dict | None") console.print(" - detect_branch_from_pwd() -> Optional[Dict]")
console.print(" - find_branch_root(start_path) -> Path | None") console.print(" - find_branch_root(start_path) -> Optional[Path]")
console.print(" - get_branch_info_from_registry(branch_path) -> Dict | None") console.print(" - get_branch_info_from_registry(branch_path) -> Optional[Dict]")
console.print(" - get_branch_display_name(branch_info) -> str") console.print(" - get_branch_display_name(branch_info) -> str")
console.print(" - get_local_config_path(branch_path) -> Path") console.print(" - get_local_config_path(branch_path) -> Path")
console.print() console.print()
@@ -19,9 +19,10 @@ PHILOSOPHY: Fail hard if detection fails. Fallbacks hide bugs.
# IMPORTS # IMPORTS
# ============================================= # =============================================
from pathlib import Path from pathlib import Path
from typing import Dict from typing import Dict, Optional
from aipass.prax.apps.modules.logger import system_logger as logger from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.ai_mail.apps.handlers.json import json_handler
# Import branch detection functions # Import branch detection functions
from .branch_detection import detect_branch_from_pwd from .branch_detection import detect_branch_from_pwd
@@ -49,6 +50,8 @@ def get_current_user() -> Dict:
Raises: Raises:
RuntimeError: If branch detection fails (not called from a branch directory) RuntimeError: If branch detection fails (not called from a branch directory)
""" """
json_handler.log_operation("get_current_user", {"cwd": str(Path.cwd())})
# Detect branch from PWD # Detect branch from PWD
branch_info = detect_branch_from_pwd() branch_info = detect_branch_from_pwd()
@@ -93,7 +96,7 @@ def get_current_user() -> Dict:
} }
def get_user_by_email(email: str) -> Dict | None: def get_user_by_email(email: str) -> Optional[Dict]:
""" """
Get user information by email address from BRANCH_REGISTRY.json Get user information by email address from BRANCH_REGISTRY.json
@@ -31,7 +31,7 @@ from aipass.ai_mail.apps.handlers.registry.update import (
get_branch_context, get_branch_context,
update_json_memory_health update_json_memory_health
) )
from aipass.ai_mail.apps.handlers.json_utils.json_handler import log_operation from aipass.ai_mail.apps.handlers.json import json_handler
MODULE_NAME = "branch_ping" MODULE_NAME = "branch_ping"
THRESHOLDS = {"green": (0, 400), "yellow": (401, 550), "red": (551, float('inf'))} THRESHOLDS = {"green": (0, 400), "yellow": (401, 550), "red": (551, float('inf'))}
@@ -55,7 +55,7 @@ def handle_ping(verbose: bool = False) -> bool:
obs_status = {"line_count": obs_count, "status": obs_status_code} obs_status = {"line_count": obs_count, "status": obs_status_code}
ping_registry(branch_name, cwd, local_status, obs_status) ping_registry(branch_name, cwd, local_status, obs_status)
log_operation("ping_executed", {"branch": branch_name, "local_count": local_count, "obs_count": obs_count}) json_handler.log_operation("ping_executed", {"branch": branch_name, "local_count": local_count, "obs_count": obs_count})
if verbose: if verbose:
console.print(f"Ping successful for {branch_name}") console.print(f"Ping successful for {branch_name}")
@@ -172,6 +172,10 @@ def handle_command(command: str, args: List[str]) -> bool:
print_help() print_help()
return True return True
if not args:
print_introspection()
return True
if command == "ping": if command == "ping":
return handle_ping("--verbose" in args or "-v" in args) return handle_ping("--verbose" in args or "-v" in args)
elif command == "status": elif command == "status":
+4 -1
View File
@@ -19,6 +19,7 @@ from typing import List
from aipass.prax.apps.modules.logger import system_logger as logger from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.cli.apps.modules import console, error from aipass.cli.apps.modules import console, error
from aipass.ai_mail.apps.handlers.json import json_handler
from aipass.ai_mail.apps.handlers.dispatch.status import ( from aipass.ai_mail.apps.handlers.dispatch.status import (
load_dispatch_log, load_dispatch_log,
check_pid_status, check_pid_status,
@@ -81,11 +82,13 @@ def handle_command(command: str, args: List[str]) -> bool:
return True return True
if not args: if not args:
print_help() print_introspection()
return True return True
subcommand = args[0] subcommand = args[0]
json_handler.log_operation("dispatch_command", {"subcommand": subcommand})
if subcommand == "status": if subcommand == "status":
return _orchestrate_status() return _orchestrate_status()
elif subcommand == "daemon": elif subcommand == "daemon":
+18 -18
View File
@@ -45,7 +45,7 @@ from aipass.ai_mail.apps.handlers.email.reply import get_email_by_id, send_reply
from aipass.ai_mail.apps.handlers.email.header import prepend_dispatch_header from aipass.ai_mail.apps.handlers.email.header import prepend_dispatch_header
from aipass.ai_mail.apps.handlers.users.user import get_current_user from aipass.ai_mail.apps.handlers.users.user import get_current_user
from aipass.ai_mail.apps.handlers.registry.read import get_all_branches, get_branch_by_email from aipass.ai_mail.apps.handlers.registry.read import get_all_branches, get_branch_by_email
from aipass.ai_mail.apps.handlers.json_utils.json_handler import log_operation from aipass.ai_mail.apps.handlers.json import json_handler
from aipass.ai_mail.apps.handlers.email.send import ( from aipass.ai_mail.apps.handlers.email.send import (
resolve_sender_info, send_to_broadcast, send_to_single, collect_interactive_input resolve_sender_info, send_to_broadcast, send_to_single, collect_interactive_input
) )
@@ -116,7 +116,7 @@ def handle_command(command: str, args: List[str]) -> bool:
def handle_send(args: List[str]) -> bool: def handle_send(args: List[str]) -> bool:
"""Orchestrate email sending workflow.""" """Orchestrate email sending workflow."""
log_operation("send_email_initiated", {"args_count": len(args)}) json_handler.log_operation("send_email_initiated", {"args_count": len(args)})
parsed = parse_send_args(args) parsed = parse_send_args(args)
if parsed["mode"] == "error": if parsed["mode"] == "error":
@@ -192,7 +192,7 @@ def _send_direct(to_branch, subject, message, auto_execute=False,
success, error_msg = send_to_single( success, error_msg = send_to_single(
to_branch, subject, message, user_info, auto_execute, no_memory_save, to_branch, subject, message, user_info, auto_execute, no_memory_save,
reply_to, dispatched_to, create_email_file, load_email_file, reply_to, dispatched_to, create_email_file, load_email_file,
deliver_email_to_branch, _delivery_callback, log_operation, update_central) deliver_email_to_branch, _delivery_callback, json_handler.log_operation, update_central)
if success: if success:
label = f"\\[dispatch: queued for daemon]" if auto_execute else "" label = f"\\[dispatch: queued for daemon]" if auto_execute else ""
@@ -205,7 +205,7 @@ def _send_direct(to_branch, subject, message, auto_execute=False,
return True return True
else: else:
error(f"Failed to deliver: {error_msg}") error(f"Failed to deliver: {error_msg}")
dispatch_send_error(to_branch, subject, error_msg, deliver_email_to_branch) dispatch_send_error(to_branch, subject, error_msg or "", deliver_email_to_branch)
return False return False
except BrokenPipeError: except BrokenPipeError:
logger.info("[email] Send: broken pipe (stdout closed early)") logger.info("[email] Send: broken pipe (stdout closed early)")
@@ -224,11 +224,11 @@ def _send_broadcast(subject, message, user_info, auto_execute, no_memory_save, r
ok, success_count, total, results = send_to_broadcast( ok, success_count, total, results = send_to_broadcast(
subject, message, user_info, auto_execute, no_memory_save, reply_to, dispatched_to, subject, message, user_info, auto_execute, no_memory_save, reply_to, dispatched_to,
branches, create_email_file, load_email_file, deliver_email_to_branch, branches, create_email_file, load_email_file, deliver_email_to_branch,
_delivery_callback, log_operation, update_central) _delivery_callback, json_handler.log_operation, update_central)
if isinstance(results, str): if isinstance(results, str) or results is None:
error("Failed to load email file for broadcast") error("Failed to load email file for broadcast")
return False return False
for name, ok, err in results: for name, ok, err in results: # type: ignore[union-attr]
if ok: if ok:
console.print(f" [green]OK[/green] {name}") console.print(f" [green]OK[/green] {name}")
else: else:
@@ -239,7 +239,7 @@ def _send_broadcast(subject, message, user_info, auto_execute, no_memory_save, r
def handle_inbox(args: List[str]) -> bool: def handle_inbox(args: List[str]) -> bool:
"""Orchestrate inbox viewing.""" """Orchestrate inbox viewing."""
log_operation("inbox_viewed") json_handler.log_operation("inbox_viewed")
try: try:
first_arg = args[0] if args else None first_arg = args[0] if args else None
ok, info = resolve_inbox_target(first_arg, _REPO_ROOT, get_branch_by_email, get_current_user) ok, info = resolve_inbox_target(first_arg, _REPO_ROOT, get_branch_by_email, get_current_user)
@@ -278,14 +278,14 @@ def handle_inbox(args: List[str]) -> bool:
def handle_view(args: List[str]) -> bool: def handle_view(args: List[str]) -> bool:
"""View email content and mark as opened.""" """View email content and mark as opened."""
log_operation("view_email_initiated", {"args": args}) json_handler.log_operation("view_email_initiated", {"args": args})
if not args: if not args:
error("Usage: drone @ai_mail view <message_id>") error("Usage: drone @ai_mail view <message_id>")
return False return False
try: try:
branch_path = Path(get_current_user()["mailbox_path"]).parent branch_path = Path(get_current_user()["mailbox_path"]).parent
success, message, email_data = mark_as_opened(branch_path, args[0]) success, message, email_data = mark_as_opened(branch_path, args[0])
if not success: if not success or email_data is None:
error(message) error(message)
return False return False
console.print(f"\n{'='*60}") console.print(f"\n{'='*60}")
@@ -296,7 +296,7 @@ def handle_view(args: List[str]) -> bool:
console.print(f"[dim]Status: opened | ID: {args[0]}[/dim]") console.print(f"[dim]Status: opened | ID: {args[0]}[/dim]")
console.print(f"[dim]To reply: drone @ai_mail reply {args[0]} \"your message\"[/dim]") console.print(f"[dim]To reply: drone @ai_mail reply {args[0]} \"your message\"[/dim]")
console.print(f"[dim]To close: drone @ai_mail close {args[0]}[/dim]") console.print(f"[dim]To close: drone @ai_mail close {args[0]}[/dim]")
log_operation("email_viewed", {"message_id": args[0]}) json_handler.log_operation("email_viewed", {"message_id": args[0]})
return True return True
except BrokenPipeError: except BrokenPipeError:
return True return True
@@ -308,7 +308,7 @@ def handle_view(args: List[str]) -> bool:
def handle_close(args: List[str]) -> bool: def handle_close(args: List[str]) -> bool:
"""Close email(s) and archive to deleted.""" """Close email(s) and archive to deleted."""
log_operation("close_email_initiated", {"args": args}) json_handler.log_operation("close_email_initiated", {"args": args})
if not args: if not args:
error("Usage: drone @ai_mail close <id> [id2 ...] | close all") error("Usage: drone @ai_mail close <id> [id2 ...] | close all")
return False return False
@@ -321,7 +321,7 @@ def handle_close(args: List[str]) -> bool:
else: else:
error(message) error(message)
if success: if success:
log_operation("email_closed_all", {"count": count}) json_handler.log_operation("email_closed_all", {"count": count})
return success return success
results, closed, failed = batch_close(branch_path, args, mark_as_closed_and_archive) results, closed, failed = batch_close(branch_path, args, mark_as_closed_and_archive)
@@ -331,7 +331,7 @@ def handle_close(args: List[str]) -> bool:
else: else:
error(message) error(message)
if success: if success:
log_operation("email_closed", {"message_id": msg_id}) json_handler.log_operation("email_closed", {"message_id": msg_id})
if len(args) > 1 and closed > 0: if len(args) > 1 and closed > 0:
try: try:
@@ -350,7 +350,7 @@ def handle_close(args: List[str]) -> bool:
def handle_reply(args: List[str]) -> bool: def handle_reply(args: List[str]) -> bool:
"""Reply to an email.""" """Reply to an email."""
log_operation("reply_email_initiated", {"args": args}) json_handler.log_operation("reply_email_initiated", {"args": args})
if len(args) < 2: if len(args) < 2:
error("Usage: drone @ai_mail reply <message_id> \"your message\"") error("Usage: drone @ai_mail reply <message_id> \"your message\"")
return False return False
@@ -367,7 +367,7 @@ def handle_reply(args: List[str]) -> bool:
else: else:
error(message) error(message)
if success: if success:
log_operation("email_replied", {"message_id": args[0], "reply_id": reply_id}) json_handler.log_operation("email_replied", {"message_id": args[0], "reply_id": reply_id})
return success return success
except Exception as e: except Exception as e:
logger.error(f"[email] Reply failed: {e}") logger.error(f"[email] Reply failed: {e}")
@@ -377,7 +377,7 @@ def handle_reply(args: List[str]) -> bool:
def handle_sent(args: List[str]) -> bool: def handle_sent(args: List[str]) -> bool:
"""View sent messages.""" """View sent messages."""
log_operation("sent_viewed") json_handler.log_operation("sent_viewed")
try: try:
sent_folder = Path(get_current_user()["mailbox_path"]) / "sent" sent_folder = Path(get_current_user()["mailbox_path"]) / "sent"
if not sent_folder.exists(): if not sent_folder.exists():
@@ -402,7 +402,7 @@ def handle_sent(args: List[str]) -> bool:
def handle_contacts(args: List[str]) -> bool: def handle_contacts(args: List[str]) -> bool:
"""View contacts.""" """View contacts."""
log_operation("contacts_viewed") json_handler.log_operation("contacts_viewed")
try: try:
branches = get_all_branches() branches = get_all_branches()
if not branches: if not branches: