mission-control/scripts/notification-daemon.sh

348 lines
9.8 KiB
Bash

#!/bin/bash
# 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 "$@"