Spaces:
Sleeping
Sleeping
| # Mission Control Phase 3: Notification Delivery Daemon | |
| # Polls undelivered notifications and sends them to agent sessions via OpenClaw | |
| # | |
| # Usage: | |
| # scripts/notification-daemon.sh [options] | |
| # | |
| # Options: | |
| # --agent AGENT_NAME Only deliver notifications to specific agent | |
| # --limit N Max notifications to process per batch (default: 50) | |
| # --dry-run Test mode - don't actually deliver notifications | |
| # --daemon Run in daemon mode (continuous polling) | |
| # --interval SECONDS Polling interval in daemon mode (default: 60) | |
| set -e | |
| # Configuration | |
| MISSION_CONTROL_URL="${MISSION_CONTROL_URL:-http://localhost:3000}" | |
| LOG_DIR="${LOG_DIR:-$HOME/.mission-control/logs}" | |
| LOG_FILE="$LOG_DIR/notification-daemon-$(date +%Y-%m-%d).log" | |
| PID_FILE="/tmp/notification-daemon.pid" | |
| DEFAULT_INTERVAL=60 | |
| DEFAULT_LIMIT=50 | |
| # Command line options | |
| AGENT_FILTER="" | |
| LIMIT=$DEFAULT_LIMIT | |
| DRY_RUN=false | |
| DAEMON_MODE=false | |
| INTERVAL=$DEFAULT_INTERVAL | |
| # Ensure log directory exists | |
| mkdir -p "$LOG_DIR" | |
| # Logging function | |
| log() { | |
| local level="$1" | |
| shift | |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" | tee -a "$LOG_FILE" | |
| } | |
| # Check if Mission Control is running | |
| check_mission_control() { | |
| if ! curl -s "$MISSION_CONTROL_URL/api/status" > /dev/null 2>&1; then | |
| log "ERROR" "Mission Control not accessible at $MISSION_CONTROL_URL" | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| # Process and deliver notifications | |
| deliver_notifications() { | |
| log "INFO" "Starting notification delivery batch" | |
| # Build API request | |
| local api_payload="{\"limit\": $LIMIT" | |
| if [[ -n "$AGENT_FILTER" ]]; then | |
| api_payload+=", \"agent_filter\": \"$AGENT_FILTER\"" | |
| fi | |
| if [[ "$DRY_RUN" == "true" ]]; then | |
| api_payload+=", \"dry_run\": true" | |
| fi | |
| api_payload+="}" | |
| # Call notification delivery endpoint | |
| local response | |
| response=$(curl -s -w "HTTP_STATUS:%{http_code}" \ | |
| -X POST \ | |
| -H "Content-Type: application/json" \ | |
| -d "$api_payload" \ | |
| "$MISSION_CONTROL_URL/api/notifications/deliver" 2>/dev/null) | |
| local http_code | |
| http_code=$(echo "$response" | grep -o "HTTP_STATUS:[0-9]*" | cut -d: -f2) | |
| local body | |
| body=$(echo "$response" | sed 's/HTTP_STATUS:[0-9]*$//') | |
| if [[ "$http_code" != "200" ]]; then | |
| log "ERROR" "Notification delivery failed: HTTP $http_code" | |
| log "ERROR" "Response: $body" | |
| return 1 | |
| fi | |
| # Parse results | |
| local status delivered errors total_processed | |
| status=$(echo "$body" | jq -r '.status // "unknown"' 2>/dev/null || echo "parse_error") | |
| delivered=$(echo "$body" | jq -r '.delivered // 0' 2>/dev/null || echo "0") | |
| errors=$(echo "$body" | jq -r '.errors // 0' 2>/dev/null || echo "0") | |
| total_processed=$(echo "$body" | jq -r '.total_processed // 0' 2>/dev/null || echo "0") | |
| if [[ "$status" == "success" ]]; then | |
| if [[ "$total_processed" -gt 0 ]]; then | |
| log "INFO" "Batch completed: $total_processed processed, $delivered delivered, $errors failed" | |
| # Log detailed errors if any | |
| if [[ "$errors" -gt 0 ]]; then | |
| local error_details | |
| error_details=$(echo "$body" | jq -r '.error_details[]? | "- \(.recipient): \(.error)"' 2>/dev/null || echo "") | |
| if [[ -n "$error_details" ]]; then | |
| log "WARN" "Error details:" | |
| echo "$error_details" | while read -r line; do | |
| log "WARN" " $line" | |
| done | |
| fi | |
| fi | |
| else | |
| log "INFO" "No notifications to deliver" | |
| fi | |
| return 0 | |
| else | |
| log "ERROR" "Unexpected delivery response: $status" | |
| return 1 | |
| fi | |
| } | |
| # Get delivery statistics | |
| get_delivery_stats() { | |
| local stats_url="$MISSION_CONTROL_URL/api/notifications/deliver" | |
| if [[ -n "$AGENT_FILTER" ]]; then | |
| stats_url+="?agent=$AGENT_FILTER" | |
| fi | |
| local response | |
| response=$(curl -s "$stats_url" 2>/dev/null) | |
| if [[ $? -eq 0 ]]; then | |
| echo "$response" | jq -r ' | |
| "Delivery Statistics:", | |
| " Total notifications: \(.statistics.total)", | |
| " Delivered: \(.statistics.delivered)", | |
| " Undelivered: \(.statistics.undelivered)", | |
| " Delivery rate: \(.statistics.delivery_rate)%", | |
| "", | |
| "Agents with pending notifications:", | |
| (.agents_with_pending[] | " \(.recipient): \(.pending_count) pending\(if .session_key then "" else " (no session key)" end)") | |
| ' 2>/dev/null || echo "Failed to parse statistics" | |
| else | |
| echo "Failed to fetch delivery statistics" | |
| fi | |
| } | |
| # Daemon mode signal handlers | |
| cleanup() { | |
| log "INFO" "Received shutdown signal, stopping daemon" | |
| rm -f "$PID_FILE" | |
| exit 0 | |
| } | |
| # Check if daemon is already running | |
| check_daemon() { | |
| if [[ -f "$PID_FILE" ]]; then | |
| local old_pid | |
| old_pid=$(cat "$PID_FILE" 2>/dev/null || echo "") | |
| if [[ -n "$old_pid" ]] && kill -0 "$old_pid" 2>/dev/null; then | |
| log "ERROR" "Notification daemon already running with PID $old_pid" | |
| exit 1 | |
| else | |
| log "WARN" "Stale PID file found, removing" | |
| rm -f "$PID_FILE" | |
| fi | |
| fi | |
| } | |
| # Run in daemon mode | |
| run_daemon() { | |
| log "INFO" "Starting notification daemon (PID: $$)" | |
| # Check if already running | |
| check_daemon | |
| # Write PID file | |
| echo $$ > "$PID_FILE" | |
| # Set up signal handlers | |
| trap cleanup SIGTERM SIGINT SIGQUIT | |
| # Main daemon loop | |
| while true; do | |
| if ! check_mission_control; then | |
| log "WARN" "Mission Control not accessible, sleeping $INTERVAL seconds" | |
| sleep "$INTERVAL" | |
| continue | |
| fi | |
| # Process notifications | |
| if deliver_notifications; then | |
| log "DEBUG" "Delivery batch completed successfully" | |
| else | |
| log "WARN" "Delivery batch had errors" | |
| fi | |
| # Sleep until next cycle | |
| sleep "$INTERVAL" | |
| done | |
| } | |
| # Parse command line arguments | |
| parse_args() { | |
| while [[ $# -gt 0 ]]; do | |
| case $1 in | |
| --agent) | |
| AGENT_FILTER="$2" | |
| shift 2 | |
| ;; | |
| --limit) | |
| LIMIT="$2" | |
| shift 2 | |
| ;; | |
| --dry-run) | |
| DRY_RUN=true | |
| shift | |
| ;; | |
| --daemon) | |
| DAEMON_MODE=true | |
| shift | |
| ;; | |
| --interval) | |
| INTERVAL="$2" | |
| shift 2 | |
| ;; | |
| --stats) | |
| get_delivery_stats | |
| exit 0 | |
| ;; | |
| --stop) | |
| if [[ -f "$PID_FILE" ]]; then | |
| local pid | |
| pid=$(cat "$PID_FILE" 2>/dev/null || echo "") | |
| if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then | |
| kill -TERM "$pid" | |
| log "INFO" "Sent stop signal to daemon (PID: $pid)" | |
| exit 0 | |
| else | |
| log "WARN" "No running daemon found" | |
| rm -f "$PID_FILE" | |
| exit 1 | |
| fi | |
| else | |
| log "WARN" "No daemon PID file found" | |
| exit 1 | |
| fi | |
| ;; | |
| --help|-h) | |
| show_help | |
| exit 0 | |
| ;; | |
| *) | |
| echo "Unknown option: $1" >&2 | |
| show_help | |
| exit 1 | |
| ;; | |
| esac | |
| done | |
| } | |
| # Show help | |
| show_help() { | |
| cat << 'EOF' | |
| Mission Control Notification Delivery Daemon | |
| Usage: notification-daemon.sh [options] | |
| Options: | |
| --agent AGENT_NAME Only deliver notifications to specific agent | |
| --limit N Max notifications to process per batch (default: 50) | |
| --dry-run Test mode - don't actually deliver notifications | |
| --daemon Run in daemon mode (continuous polling) | |
| --interval SECONDS Polling interval in daemon mode (default: 60) | |
| --stats Show delivery statistics and exit | |
| --stop Stop running daemon | |
| --help, -h Show this help message | |
| Examples: | |
| # Single batch delivery | |
| ./notification-daemon.sh | |
| # Dry run to test | |
| ./notification-daemon.sh --dry-run | |
| # Deliver only to specific agent | |
| ./notification-daemon.sh --agent "coordinator" | |
| # Run as daemon | |
| ./notification-daemon.sh --daemon --interval 30 | |
| # Show statistics | |
| ./notification-daemon.sh --stats | |
| # Stop daemon | |
| ./notification-daemon.sh --stop | |
| Environment variables: | |
| MISSION_CONTROL_URL Mission Control base URL (default: http://localhost:3005) | |
| Log files: | |
| $LOG_DIR/notification-daemon-YYYY-MM-DD.log | |
| EOF | |
| } | |
| # Validate arguments | |
| validate_args() { | |
| if ! [[ "$LIMIT" =~ ^[1-9][0-9]*$ ]]; then | |
| log "ERROR" "Invalid limit: $LIMIT (must be positive integer)" | |
| exit 1 | |
| fi | |
| if ! [[ "$INTERVAL" =~ ^[1-9][0-9]*$ ]]; then | |
| log "ERROR" "Invalid interval: $INTERVAL (must be positive integer)" | |
| exit 1 | |
| fi | |
| } | |
| # Main execution | |
| main() { | |
| parse_args "$@" | |
| validate_args | |
| if [[ "$DAEMON_MODE" == "true" ]]; then | |
| run_daemon | |
| else | |
| # Single run mode | |
| log "INFO" "Starting single notification delivery run" | |
| if ! check_mission_control; then | |
| log "ERROR" "Aborting: Mission Control not accessible" | |
| exit 1 | |
| fi | |
| if deliver_notifications; then | |
| log "INFO" "Notification delivery completed successfully" | |
| exit 0 | |
| else | |
| log "ERROR" "Notification delivery failed" | |
| exit 1 | |
| fi | |
| fi | |
| } | |
| # Run main function | |
| main "$@" | |