Skill: abdullah4ai/openclaw-watchdog (version 1.3.0)

--- name: openclaw-watchdog description: Self-healing monitoring system for OpenClaw gateway. Auto-detects failures, fixes crashes, and sends Telegram alerts. homepage: https://github.com/Abdullah4AI/openclaw-watchdog metadata: {"openclaw":{"emoji":"🐕","disableModelInvocation":true,"requires":{"bins":["python3","openssl"],"env":["TELEGRAM_TOKEN","TELEGRAM_CHAT_ID"]},"install":[{"id":"setup","kind":"script","script":"scripts/setup.sh","label":"Install watchdog service (LaunchAgent/systemd user)","persistence":"user-level","service":true}]}} --- # openclaw-watchdog **Description:** Self-healing monitoring system for OpenClaw gateway. Monitors health, auto-restarts on failure, and sends Telegram alerts. Diagnostics and log analysis run locally on-device. Alert notifications are sent to the user's Telegram bot. Use when user wants to set up gateway monitoring, watchdog, or auto-recovery. ## Prerequisites - **Telegram Bot Token** — Create via [@BotFather](https://t.me/BotFather) - **Telegram Chat ID** — Your personal chat ID for receiving alerts - **Python 3** — Required for the watchdog service - **OpenClaw** — Installed and running ## Trigger Keywords - watchdog, monitoring, auto-fix, gateway health, self-healing, auto-recovery, watch dog ## Setup Send the user ONE message with everything they need: --- 🐕 **Watch Dog — Self-Healing Gateway Monitor** Watch Dog is a background service that pings your OpenClaw gateway every 15 seconds. If the gateway goes down, it automatically attempts to restart it and sends you Telegram alerts so you're always in the loop. All diagnostics run locally on your device. To set it up, I need: 1. **Telegram Bot Token** — Create a bot via [@BotFather](https://t.me/BotFather) on Telegram, then send me the token (looks like `123456:ABC-DEF...`) 2. **Your Telegram Chat ID** — Send `/start` to your bot, then visit `https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates` to find your chat ID Send me the token and chat ID and I'll handle the rest (including a test run to make sure everything works)! --- ## After Receiving Credentials Run these steps in order: ### 1. Validate credentials ```bash python3 ~/.openclaw/workspace/openclaw-watchdog/scripts/validate.py "$TELEGRAM_TOKEN" ``` ### 2. Run setup script ```bash chmod +x ~/.openclaw/workspace/openclaw-watchdog/scripts/setup.sh ~/.openclaw/workspace/openclaw-watchdog/scripts/setup.sh \ --telegram-token "$TELEGRAM_TOKEN" \ --telegram-chat-id "$TELEGRAM_CHAT_ID" \ --gateway-port "$GATEWAY_PORT" # optional, auto-detected from openclaw.json ``` ### 3. Connect via Telegram (Pairing) ```bash python3 ~/.openclaw/workspace/openclaw-watchdog/scripts/test-message.py "$TELEGRAM_TOKEN" "$TELEGRAM_CHAT_ID" ``` Wait for user to confirm they received the Telegram message before proceeding. ### 4. Verify it's running ```bash # Check service status if [[ "$(uname)" == "Darwin" ]]; then launchctl list | grep openclaw.watchdog else systemctl --user status openclaw-watchdog fi # Check logs tail -20 ~/.openclaw/watchdog/watchdog.log ``` ### 5. Confirm to user Tell them Watch Dog is active, what it monitors, and that they'll get Telegram alerts if anything goes wrong. ## How It Works - Pings gateway health endpoint every 15 seconds (auto-detects port from config, or set via `--gateway-port`) - After 3 consecutive failures, attempts `openclaw gateway restart` - Up to 2 restart attempts, then asks user for reinstall permission via Telegram - User approves by running: `touch ~/.openclaw/watchdog/approve-reinstall` - Without approval, only sends notifications — no destructive actions - Local pattern-matching diagnostics (no logs sent externally) - Runs as macOS LaunchAgent or Linux systemd user service - Credentials encrypted with AES-256 using machine-specific key ## Uninstall ```bash if [[ "$(uname)" == "Darwin" ]]; then launchctl unload ~/Library/LaunchAgents/com.openclaw.watchdog.plist 2>/dev/null rm -f ~/Library/LaunchAgents/com.openclaw.watchdog.plist else systemctl --user stop openclaw-watchdog 2>/dev/null systemctl --user disable openclaw-watchdog 2>/dev/null rm -f ~/.config/systemd/user/openclaw-watchdog.service fi rm -rf ~/.openclaw/watchdog ```

path: openclaw-watchdog/_meta.json
content: ownerId: kn7737fzx2j2818teh92kpn2j580c6c4
slug: openclaw-watchdog
version: 1.2.1
publishedAt: 1770783846963

path: openclaw-watchdog/references/troubleshooting.md
content: # Watch Dog Troubleshooting

## Common Issues

### Watch Dog won't start
```bash
# Check logs
tail -50 ~/.openclaw/watchdog/watchdog-error.log

# Verify Python venv
~/.openclaw/watchdog/venv/bin/python3 -c "import aiohttp; print('OK')"

# Re-run setup
~/.openclaw/workspace/openclaw-watchdog/scripts/setup.sh --telegram-token "..." --telegram-chat-id "..." --openai-key "..."
```

### Decryption fails
The config is encrypted with a machine-specific key derived from hardware UUID + hostname. If you changed hostname or migrated machines, re-run setup.

### No Telegram alerts
1. Verify bot token: `curl https://api.telegram.org/bot<TOKEN>/getMe`
2. Verify chat ID: `curl https://api.telegram.org/bot<TOKEN>/getUpdates`
3. Make sure you sent `/start` to the bot first

### Gateway keeps dying
Check gateway logs:
```bash
tail -200 ~/.openclaw/gateway.log
openclaw gateway status
```

Watch Dog attempts fixes in this order:
1. `openclaw gateway restart` (attempts 1-2)
2. Sends Telegram alert asking for reinstall permission (attempt 3)
3. If user approves (`touch ~/.openclaw/watchdog/approve-reinstall`): runs `npm install -g openclaw` + start (attempts 4-5)
4. After 5 failed attempts → stops and alerts for manual intervention
5. Without user approval, no reinstall happens — only notifications

### Service management

**macOS:**
```bash
launchctl list | grep watchdog          # Status
launchctl unload ~/Library/LaunchAgents/com.openclaw.watchdog.plist  # Stop
launchctl load ~/Library/LaunchAgents/com.openclaw.watchdog.plist    # Start
```

**Linux:**
```bash
systemctl --user status openclaw-watchdog
systemctl --user stop openclaw-watchdog
systemctl --user start openclaw-watchdog
journalctl --user -u openclaw-watchdog -f   # Live logs
```

### Update credentials
Re-run the setup script with new tokens. It will re-encrypt and restart the service.

### Uninstall completely
```bash
# macOS
launchctl unload ~/Library/LaunchAgents/com.openclaw.watchdog.plist
rm ~/Library/LaunchAgents/com.openclaw.watchdog.plist

# Linux
systemctl --user stop openclaw-watchdog
systemctl --user disable openclaw-watchdog
rm ~/.config/systemd/user/openclaw-watchdog.service

# Both
rm -rf ~/.openclaw/watchdog
```


path: openclaw-watchdog/scripts/setup.sh
content: #!/usr/bin/env bash
set -euo pipefail

# ===========================================================================
# OpenClaw Watch Dog — Setup Script
# ===========================================================================

WATCHDOG_DIR="$HOME/.openclaw/watchdog"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
VENV_DIR="$WATCHDOG_DIR/venv"
CONFIG_ENC="$WATCHDOG_DIR/config.enc"

# Colors
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
info()  { echo -e "${GREEN}[✓]${NC} $*"; }
warn()  { echo -e "${YELLOW}[!]${NC} $*"; }
error() { echo -e "${RED}[✗]${NC} $*"; exit 1; }

# ---------------------------------------------------------------------------
# Parse arguments
# ---------------------------------------------------------------------------
TELEGRAM_TOKEN="" TELEGRAM_CHAT_ID="" OPENAI_KEY="" ANTHROPIC_KEY=""

while [[ $# -gt 0 ]]; do
    case "$1" in
        --telegram-token)  TELEGRAM_TOKEN="$2";  shift 2 ;;
        --telegram-chat-id) TELEGRAM_CHAT_ID="$2"; shift 2 ;;
        --openai-key)      OPENAI_KEY="$2";      shift 2 ;;
        --anthropic-key)   ANTHROPIC_KEY="$2";   shift 2 ;;
        *) error "Unknown argument: $1" ;;
    esac
done

[[ -z "$TELEGRAM_TOKEN" ]] && error "Missing --telegram-token"
[[ -z "$TELEGRAM_CHAT_ID" ]] && error "Missing --telegram-chat-id"
# AI keys are optional (used for future extensions only)

# ---------------------------------------------------------------------------
# Machine-specific password (must match watchdog.py logic)
# ---------------------------------------------------------------------------
machine_password() {
    local parts=""
    if [[ "$(uname)" == "Darwin" ]]; then
        local uuid
        uuid=$(ioreg -rd1 -c IOPlatformExpertDevice | awk -F'"' '/IOPlatformUUID/{print $4}') || true
        parts="${uuid}:"
    fi
    parts="${parts}$(hostname):${USER:-openclaw}"
    echo -n "$parts" | shasum -a 256 | awk '{print $1}'
}

# ---------------------------------------------------------------------------
# 1. Create working directory
# ---------------------------------------------------------------------------
info "Creating $WATCHDOG_DIR"
mkdir -p "$WATCHDOG_DIR"

# ---------------------------------------------------------------------------
# 2. Copy watchdog script
# ---------------------------------------------------------------------------
info "Installing watchdog.py"
cp "$SCRIPT_DIR/watchdog.py" "$WATCHDOG_DIR/watchdog.py"
chmod +x "$WATCHDOG_DIR/watchdog.py"

# ---------------------------------------------------------------------------
# 3. Python venv + dependencies
# ---------------------------------------------------------------------------
info "Setting up Python virtual environment"
python3 -m venv "$VENV_DIR"
"$VENV_DIR/bin/pip" install --quiet --upgrade pip
"$VENV_DIR/bin/pip" install --quiet aiohttp
info "Dependencies installed"

# ---------------------------------------------------------------------------
# 4. Encrypt credentials
# ---------------------------------------------------------------------------
info "Encrypting credentials"
[REDACTED_SECRET])

# Build JSON safely via Python to prevent shell injection
CONFIG_JSON=$(python3 -c "
import json, sys
print(json.dumps({
    'telegram_token': sys.argv[1],
    'telegram_chat_id': sys.argv[2],
    'openai_key': sys.argv[3],
    'anthropic_key': sys.argv[4]
}))
" "$TELEGRAM_TOKEN" "$TELEGRAM_CHAT_ID" "$OPENAI_KEY" "$ANTHROPIC_KEY")

printf '%s' "$CONFIG_JSON" | openssl enc -aes-256-cbc -pbkdf2 -pass "pass:$PASSWORD" -out "$CONFIG_ENC"
chmod 600 "$CONFIG_ENC"
info "Credentials encrypted at $CONFIG_ENC"

# ---------------------------------------------------------------------------
# 5. Install as service
# ---------------------------------------------------------------------------
PYTHON_BIN="$VENV_DIR/bin/python3"
WATCHDOG_PY="$WATCHDOG_DIR/watchdog.py"

if [[ "$(uname)" == "Darwin" ]]; then
    # macOS LaunchAgent — generate plist safely via Python (no shell interpolation)
    PLIST="$HOME/Library/LaunchAgents/com.openclaw.watchdog.plist"
    info "Installing LaunchAgent → $PLIST"
    mkdir -p "$HOME/Library/LaunchAgents"
    python3 -c "
import plistlib, sys
plist = {
    'Label': 'com.openclaw.watchdog',
    'ProgramArguments': [sys.argv[1], sys.argv[2]],
    'RunAtLoad': True,
    'KeepAlive': True,
    'StandardOutPath': sys.argv[3] + '/watchdog.log',
    'StandardErrorPath': sys.argv[3] + '/watchdog-error.log',
    'EnvironmentVariables': {
        'PATH': '/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin'
    }
}
with open(sys.argv[4], 'wb') as f:
    plistlib.dump(plist, f)
" "$PYTHON_BIN" "$WATCHDOG_PY" "$WATCHDOG_DIR" "$PLIST"

    launchctl unload "$PLIST" 2>/dev/null || true
    launchctl load "$PLIST"
    info "LaunchAgent loaded"

else
    # Linux systemd user service — write via printf (no heredoc interpolation)
    SERVICE_DIR="$HOME/.config/systemd/user"
    SERVICE_FILE="$SERVICE_DIR/openclaw-watchdog.service"
    info "Installing systemd service → $SERVICE_FILE"
    mkdir -p "$SERVICE_DIR"
    printf '[Unit]\nDescription=OpenClaw Watch Dog\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=%s %s\nRestart=always\nRestartSec=10\nEnvironment=PATH=/usr/local/bin:/usr/bin:/bin\n\n[Install]\nWantedBy=default.target\n' \
        "$PYTHON_BIN" "$WATCHDOG_PY" > "$SERVICE_FILE"

    systemctl --user daemon-reload
    systemctl --user enable openclaw-watchdog
    systemctl --user restart openclaw-watchdog
    info "systemd service started"
fi

# ---------------------------------------------------------------------------
# 6. Verify
# ---------------------------------------------------------------------------
sleep 3
if [[ "$(uname)" == "Darwin" ]]; then
    if launchctl list | grep -q "com.openclaw.watchdog"; then
        info "Watch Dog is running! 🐕"
    else
        warn "Service may not have started — check $WATCHDOG_DIR/watchdog-error.log"
    fi
else
    if systemctl --user is-active openclaw-watchdog >/dev/null 2>&1; then
        info "Watch Dog is running! 🐕"
    else
        warn "Service may not have started — check: journalctl --user -u openclaw-watchdog"
    fi
fi

echo ""
info "Setup complete! Watch Dog is monitoring your gateway."
info "Logs: $WATCHDOG_DIR/watchdog.log"
info "Config: $CONFIG_ENC (encrypted)"


path: openclaw-watchdog/scripts/test-message.py
content: #!/usr/bin/env python3
"""Send a test message via Telegram to verify bot + chat ID work."""
import urllib.request, json, sys, re

if len(sys.argv) < 3:
    print("Usage: test-message.py <telegram_token> <chat_id>", file=sys.stderr)
    sys.exit(1)

[REDACTED_SECRET]
chat_id = sys.argv[2]

# Validate token format
if not re.match(r'^\d+:[A-Za-z0-9_-]+$', token):
    print("Error: Token format invalid.", file=sys.stderr)
    sys.exit(1)

# Validate chat_id is numeric (positive or negative)
if not re.match(r'^-?\d+$', chat_id):
    print("Error: Chat ID must be numeric.", file=sys.stderr)
    sys.exit(1)

try:
    data = json.dumps({
        'chat_id': chat_id,
        'text': '\U0001f415 Watch Dog connected successfully! Monitoring your gateway 24/7.'
    }).encode()
    req = urllib.request.Request(
        f'https://api.telegram.org/bot{token}/sendMessage',
        data=data,
        headers={'Content-Type': 'application/json'}
    )
    resp = json.loads(urllib.request.urlopen(req).read())
    if resp.get('ok'):
        print('Test message sent!')
    else:
        print(f"Error: {resp}", file=sys.stderr)
        sys.exit(1)
except Exception as e:
    print(f"Error: {e}", file=sys.stderr)
    sys.exit(1)


path: openclaw-watchdog/scripts/validate.py
content: #!/usr/bin/env python3
"""Validate a Telegram bot token by calling getMe."""
import urllib.request, json, sys

if len(sys.argv) < 2:
    print("Usage: validate.py <telegram_token>", file=sys.stderr)
    sys.exit(1)

[REDACTED_SECRET]

# Validate token format (digits:alphanumeric)
import re
if not re.match(r'^\d+:[A-Za-z0-9_-]+$', token):
    print("Error: Token format invalid. Expected format: 123456:ABC-DEF...", file=sys.stderr)
    sys.exit(1)

try:
    resp = json.loads(urllib.request.urlopen(f'https://api.telegram.org/bot{token}/getMe').read())
    if resp.get('ok'):
        print(f"Token valid: @{resp['result']['username']}")
    else:
        print("Error: Token rejected by Telegram API", file=sys.stderr)
        sys.exit(1)
except Exception as e:
    print(f"Error: {e}", file=sys.stderr)
    sys.exit(1)


path: openclaw-watchdog/scripts/watchdog.py
content: #!/usr/bin/env python3
"""
OpenClaw Watch Dog — Self-healing gateway monitor.
Pings the gateway every 15 seconds, auto-fixes crashes, sends Telegram alerts.
"""

import asyncio
import json
import logging
import os
import platform
import subprocess
import sys
import time
import hashlib
import base64
from pathlib import Path
from datetime import datetime

import aiohttp

# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------
WATCHDOG_DIR = Path.home() / ".openclaw" / "watchdog"
CONFIG_FILE = WATCHDOG_DIR / "config.enc"
LOG_FILE = WATCHDOG_DIR / "watchdog.log"
STATE_FILE = WATCHDOG_DIR / "state.json"
GATEWAY_HEALTH = "http://127.0.0.1:3117/health"
APPROVE_REINSTALL = WATCHDOG_DIR / "approve-reinstall"

# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler(LOG_FILE, encoding="utf-8"),
        logging.StreamHandler(),
    ],
)
log = logging.getLogger("watchdog")

# ---------------------------------------------------------------------------
# Encryption helpers (AES-256-CBC via openssl CLI — no extra deps)
# ---------------------------------------------------------------------------

def _machine_password() -> str:
    """Derive a machine-specific password from hardware identifiers."""
    parts = []
    if platform.system() == "Darwin":
        try:
            out = subprocess.check_output(
                ["ioreg", "-rd1", "-c", "IOPlatformExpertDevice"], text=True
            )
            for line in out.splitlines():
                if "IOPlatformUUID" in line:
                    parts.append(line.split('"')[-2])
                    break
        except Exception:
            pass
    # Fallback: hostname + username
    parts.extend([platform.node(), os.getenv("USER", "openclaw")])
    return hashlib.sha256(":".join(parts).encode()).hexdigest()


def decrypt_config() -> dict:
    """Decrypt config.enc → dict."""
    if not CONFIG_FILE.exists():
        log.error("Config file not found: %s", CONFIG_FILE)
        sys.exit(1)
    pw = _machine_password()
    try:
        result = subprocess.run(
            ["openssl", "enc", "-aes-256-cbc", "-d", "-pbkdf2",
             "-in", str(CONFIG_FILE), "-pass", f"pass:{pw}"],
            capture_output=True,
        )
        if result.returncode != 0:
            log.error("Decryption failed: %s", result.stderr.decode())
            sys.exit(1)
        return json.loads(result.stdout.decode())
    except Exception as e:
        log.error("Failed to decrypt config: %s", e)
        sys.exit(1)


# ---------------------------------------------------------------------------
# Telegram
# ---------------------------------------------------------------------------

async def send_telegram(session: aiohttp.ClientSession, cfg: dict, text: str):
    """Send a Telegram message."""
    [REDACTED_SECRET]"telegram_token", "")
    chat_id = cfg.get("telegram_chat_id", "")
    if not token or not chat_id:
        log.warning("Telegram not configured, skipping alert")
        return
    url = f"https://api.telegram.org/bot{token}/sendMessage"
    try:
        async with session.post(url, json={
            "chat_id": chat_id,
            "text": text,
            "parse_mode": "HTML",
        }) as resp:
            if resp.status != 200:
                log.warning("Telegram send failed: %s", await resp.text())
    except Exception as e:
        log.warning("Telegram error: %s", e)


# ---------------------------------------------------------------------------
# Local Diagnostics (no external API calls — logs stay on device)
# ---------------------------------------------------------------------------

def diagnose_locally(logs: str) -> str:
    """Analyze logs locally using pattern matching. No data leaves the device."""
    lines = logs.lower()
    issues = []
    if "eaddrinuse" in lines:
        issues.append("Port already in use — another process may be bound to the gateway port")
    if "enomem" in lines or "out of memory" in lines:
        issues.append("Out of memory — consider freeing resources or restarting the machine")
    if "enoent" in lines or "not found" in lines:
        issues.append("Missing file or binary — OpenClaw may need reinstallation")
    if "permission denied" in lines or "eacces" in lines:
        issues.append("Permission denied — check file/directory permissions")
    if "segfault" in lines or "segmentation fault" in lines:
        issues.append("Segmentation fault — possible corrupted installation")
    if "module not found" in lines or "cannot find module" in lines:
        issues.append("Missing Node.js module — may need manual reinstallation")
    if not issues:
        issues.append("No obvious pattern detected — check logs manually")
    return "; ".join(issues)


# ---------------------------------------------------------------------------
# Log collection
# ---------------------------------------------------------------------------

def collect_logs(lines: int = 200) -> str:
    """Collect last N lines from OpenClaw gateway logs."""
    log_paths = [
        Path.home() / ".openclaw" / "gateway.log",
        Path.home() / ".openclaw" / "logs" / "gateway.log",
        Path("/tmp") / "openclaw-gateway.log",
    ]
    for p in log_paths:
        if p.exists():
            try:
                result = subprocess.run(
                    ["tail", f"-{lines}", str(p)], capture_output=True, text=True
                )
                if result.stdout.strip():
                    return result.stdout
            except Exception:
                continue

    # Try journalctl on Linux
    if platform.system() == "Linux":
        try:
            result = subprocess.run(
                ["journalctl", "--user", "-u", "openclaw-gateway", f"-n{lines}", "--no-pager"],
                capture_output=True, text=True,
            )
            if result.stdout.strip():
                return result.stdout
        except Exception:
            pass

    return "(no logs found)"


# ---------------------------------------------------------------------------
# Auto-fix
# ---------------------------------------------------------------------------

async def attempt_fix(session: aiohttp.ClientSession, cfg: dict, attempt: int) -> bool:
    """Try to fix the gateway. Returns True if health check passes after fix."""
    log.info("Auto-fix attempt #%d", attempt)

    if attempt <= 2:
        # Step 1-2: Simple restart
        log.info("Restarting gateway...")
        await send_telegram(session, cfg, "🔧 <b>Watch Dog:</b> Restarting gateway...")
        try:
            subprocess.run(["openclaw", "gateway", "restart"], timeout=30,
                           capture_output=True, text=True)
        except Exception as e:
            log.error("Restart command failed: %s", e)
            return False

        await asyncio.sleep(10)
        return await check_health(session)

    elif attempt == 3:
        # Step 3: Ask user for permission to reinstall
        log.info("Restart attempts exhausted, asking user for reinstall permission...")
        await send_telegram(session, cfg,
            "⚠️ <b>Watch Dog:</b> Gateway restart failed after 2 attempts.\n\n"
            "Reinstalling OpenClaw might fix the issue.\n"
            "To approve reinstall, run:\n"
            "<code>touch ~/.openclaw/watchdog/approve-reinstall</code>\n\n"
            "I'll check for your approval and proceed if given."
        )
        return False

    elif attempt <= 5 and APPROVE_REINSTALL.exists():
        # Step 4-5: User approved reinstall
        log.info("User approved reinstall, running npm install -g openclaw...")
        APPROVE_REINSTALL.unlink(missing_ok=True)
        await send_telegram(session, cfg, "🔧 <b>Watch Dog:</b> Reinstalling OpenClaw (approved by user)...")
        try:
            subprocess.run(["npm", "install", "-g", "openclaw"], timeout=120,
                           capture_output=True, text=True)
            await asyncio.sleep(5)
            subprocess.run(["openclaw", "gateway", "start"], timeout=30,
                           capture_output=True, text=True)
        except Exception as e:
            log.error("Reinstall failed: %s", e)
            return False

        await asyncio.sleep(10)
        return await check_health(session)

    else:
        # No approval or attempts exhausted — notify user
        log.info("Waiting for user action...")
        if attempt == 5:
            await send_telegram(session, cfg,
                "❌ <b>Watch Dog:</b> All auto-fix attempts exhausted.\n"
                "Manual intervention required:\n"
                "<code>openclaw gateway restart</code>"
            )
        return False


# ---------------------------------------------------------------------------
# Health check
# ---------------------------------------------------------------------------

async def check_health(session: aiohttp.ClientSession) -> bool:
    """Ping the gateway health endpoint."""
    try:
        async with session.get(GATEWAY_HEALTH, timeout=aiohttp.ClientTimeout(total=10)) as resp:
            return resp.status == 200
    except Exception:
        return False


# ---------------------------------------------------------------------------
# State management
# ---------------------------------------------------------------------------

def load_state() -> dict:
    if STATE_FILE.exists():
        try:
            return json.loads(STATE_FILE.read_text())
        except Exception:
            pass
    return {"consecutive_failures": 0, "fix_attempts": 0, "last_status": "unknown", "started_at": time.time()}


def save_state(state: dict):
    STATE_FILE.write_text(json.dumps(state, indent=2))


# ---------------------------------------------------------------------------
# Main loop
# ---------------------------------------------------------------------------

async def main():
    WATCHDOG_DIR.mkdir(parents=True, exist_ok=True)

    cfg = decrypt_config()
    state = load_state()
    state["started_at"] = time.time()
    save_state(state)

    log.info("Watch Dog started — monitoring %s", GATEWAY_HEALTH)

    async with aiohttp.ClientSession() as session:
        await send_telegram(session, cfg, "🐕 <b>Watch Dog started</b>\nMonitoring gateway health every 15s.")

        while True:
            healthy = await check_health(session)

            if healthy:
                if state["last_status"] != "healthy":
                    log.info("Gateway is healthy ✓")
                    if state["consecutive_failures"] > 0 or state["fix_attempts"] > 0:
                        await send_telegram(session, cfg,
                            "✅ <b>Gateway recovered!</b>\n"
                            f"After {state['fix_attempts']} fix attempt(s)."
                        )
                    state["consecutive_failures"] = 0
                    state["fix_attempts"] = 0
                    state["last_status"] = "healthy"
                    save_state(state)
            else:
                state["consecutive_failures"] += 1
                state["last_status"] = "unhealthy"
                log.warning("Health check failed (%d consecutive)", state["consecutive_failures"])

                if state["consecutive_failures"] == 3:
                    # First failure notification with diagnostics
                    logs = collect_logs()
                    diagnosis = diagnose_locally(logs)
                    await send_telegram(session, cfg,
                        "🚨 <b>Gateway DOWN!</b>\n"
                        f"3 consecutive failures detected.\n\n"
                        f"<b>AI Diagnosis:</b>\n<code>{diagnosis[:500]}</code>\n\n"
                        "Attempting auto-fix..."
                    )

                if state["consecutive_failures"] >= 3:
                    state["fix_attempts"] += 1
                    fixed = await attempt_fix(session, cfg, state["fix_attempts"])
                    if fixed:
                        state["consecutive_failures"] = 0
                        state["fix_attempts"] = 0
                        state["last_status"] = "healthy"
                        log.info("Auto-fix succeeded!")
                        await send_telegram(session, cfg, "✅ <b>Auto-fix succeeded!</b> Gateway is back online.")
                    elif state["fix_attempts"] >= 5:
                        await send_telegram(session, cfg,
                            "❌ <b>Auto-fix exhausted</b> (5 attempts).\n"
                            "Manual intervention required!"
                        )
                        state["fix_attempts"] = 0
                        state["consecutive_failures"] = 0  # Reset to avoid spam

                save_state(state)

            await asyncio.sleep(15)


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        log.info("Watch Dog stopped by user")
    except Exception as e:
        log.exception("Watch Dog crashed: %s", e)
        sys.exit(1)


path: references/troubleshooting.md
content: # Watch Dog Troubleshooting

## Common Issues

### Watch Dog won't start
```bash
# Check logs
tail -50 ~/.openclaw/watchdog/watchdog-error.log

# Verify Python venv
~/.openclaw/watchdog/venv/bin/python3 -c "import aiohttp; print('OK')"

# Re-run setup
~/.openclaw/workspace/openclaw-watchdog/scripts/setup.sh --telegram-token "..." --telegram-chat-id "..." --openai-key "..."
```

### Decryption fails
The config is encrypted with a machine-specific key derived from hardware UUID + hostname. If you changed hostname or migrated machines, re-run setup.

### No Telegram alerts
1. Verify bot token: `curl https://api.telegram.org/bot<TOKEN>/getMe`
2. Verify chat ID: `curl https://api.telegram.org/bot<TOKEN>/getUpdates`
3. Make sure you sent `/start` to the bot first

### Gateway keeps dying
Check gateway logs:
```bash
tail -200 ~/.openclaw/gateway.log
openclaw gateway status
```

Watch Dog attempts fixes in this order:
1. `openclaw gateway restart` (attempts 1-2)
2. Sends Telegram alert asking for reinstall permission (attempt 3)
3. If user approves (`touch ~/.openclaw/watchdog/approve-reinstall`): runs `npm install -g openclaw` + start (attempts 4-5)
4. After 5 failed attempts → stops and alerts for manual intervention
5. Without user approval, no reinstall happens — only notifications

### Service management

**macOS:**
```bash
launchctl list | grep watchdog          # Status
launchctl unload ~/Library/LaunchAgents/com.openclaw.watchdog.plist  # Stop
launchctl load ~/Library/LaunchAgents/com.openclaw.watchdog.plist    # Start
```

**Linux:**
```bash
systemctl --user status openclaw-watchdog
systemctl --user stop openclaw-watchdog
systemctl --user start openclaw-watchdog
journalctl --user -u openclaw-watchdog -f   # Live logs
```

### Update credentials
Re-run the setup script with new tokens. It will re-encrypt and restart the service.

### Uninstall completely
```bash
# macOS
launchctl unload ~/Library/LaunchAgents/com.openclaw.watchdog.plist
rm ~/Library/LaunchAgents/com.openclaw.watchdog.plist

# Linux
systemctl --user stop openclaw-watchdog
systemctl --user disable openclaw-watchdog
rm ~/.config/systemd/user/openclaw-watchdog.service

# Both
rm -rf ~/.openclaw/watchdog
```


path: scripts/setup.sh
content: #!/usr/bin/env bash
set -euo pipefail

# ===========================================================================
# OpenClaw Watch Dog — Setup Script
# ===========================================================================

WATCHDOG_DIR="$HOME/.openclaw/watchdog"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
VENV_DIR="$WATCHDOG_DIR/venv"
CONFIG_ENC="$WATCHDOG_DIR/config.enc"

# Colors
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
info()  { echo -e "${GREEN}[✓]${NC} $*"; }
warn()  { echo -e "${YELLOW}[!]${NC} $*"; }
error() { echo -e "${RED}[✗]${NC} $*"; exit 1; }

# ---------------------------------------------------------------------------
# Parse arguments
# ---------------------------------------------------------------------------
TELEGRAM_TOKEN="" TELEGRAM_CHAT_ID="" OPENAI_KEY="" ANTHROPIC_KEY="" GATEWAY_PORT=""

while [[ $# -gt 0 ]]; do
    case "$1" in
        --telegram-token)  TELEGRAM_TOKEN="$2";  shift 2 ;;
        --telegram-chat-id) TELEGRAM_CHAT_ID="$2"; shift 2 ;;
        --openai-key)      OPENAI_KEY="$2";      shift 2 ;;
        --anthropic-key)   ANTHROPIC_KEY="$2";   shift 2 ;;
        --gateway-port)    GATEWAY_PORT="$2";    shift 2 ;;
        *) error "Unknown argument: $1" ;;
    esac
done

# Auto-detect gateway port if not specified
if [[ -z "$GATEWAY_PORT" ]]; then
    GATEWAY_PORT=$(grep -o '"port"[[:space:]]*:[[:space:]]*[0-9]*' "$HOME/.openclaw/openclaw.json" 2>/dev/null | head -1 | grep -o '[0-9]*' || echo "3117")
    if [[ -z "$GATEWAY_PORT" ]]; then
        GATEWAY_PORT="3117"
    fi
fi
HEALTH_URL="http://127.0.0.1:${GATEWAY_PORT}/health"
info "Gateway health endpoint: $HEALTH_URL"

[[ -z "$TELEGRAM_TOKEN" ]] && error "Missing --telegram-token"
[[ -z "$TELEGRAM_CHAT_ID" ]] && error "Missing --telegram-chat-id"
# AI keys are optional (used for future extensions only)

# ---------------------------------------------------------------------------
# Machine-specific password (must match watchdog.py logic)
# ---------------------------------------------------------------------------
machine_password() {
    local parts=""
    if [[ "$(uname)" == "Darwin" ]]; then
        local uuid
        uuid=$(ioreg -rd1 -c IOPlatformExpertDevice 2>/dev/null | awk -F'"' '/IOPlatformUUID/{print $4}') || true
        if [[ -n "$uuid" ]]; then
            parts="${uuid}:"
        fi
    fi
    parts="${parts}$(hostname):${USER:-openclaw}"
    echo -n "$parts" | shasum -a 256 | awk '{print $1}'
}

# ---------------------------------------------------------------------------
# 1. Create working directory
# ---------------------------------------------------------------------------
info "Creating $WATCHDOG_DIR"
mkdir -p "$WATCHDOG_DIR"

# ---------------------------------------------------------------------------
# 2. Copy watchdog script
# ---------------------------------------------------------------------------
info "Installing watchdog.py"
cp "$SCRIPT_DIR/watchdog.py" "$WATCHDOG_DIR/watchdog.py"
chmod +x "$WATCHDOG_DIR/watchdog.py"

# ---------------------------------------------------------------------------
# 3. Python venv + dependencies
# ---------------------------------------------------------------------------
info "Setting up Python virtual environment"
python3 -m venv "$VENV_DIR"
"$VENV_DIR/bin/pip" install --quiet --upgrade pip
"$VENV_DIR/bin/pip" install --quiet aiohttp
info "Dependencies installed"

# ---------------------------------------------------------------------------
# 4. Encrypt credentials
# ---------------------------------------------------------------------------
info "Encrypting credentials"
[REDACTED_SECRET])

# Build JSON safely via Python to prevent shell injection
CONFIG_JSON=$(python3 -c "
import json, sys
print(json.dumps({
    'telegram_token': sys.argv[1],
    'telegram_chat_id': sys.argv[2],
    'openai_key': sys.argv[3],
    'anthropic_key': sys.argv[4]
}))
" "$TELEGRAM_TOKEN" "$TELEGRAM_CHAT_ID" "$OPENAI_KEY" "$ANTHROPIC_KEY")

printf '%s' "$CONFIG_JSON" | openssl enc -aes-256-cbc -pbkdf2 -pass "pass:$PASSWORD" -out "$CONFIG_ENC"
chmod 600 "$CONFIG_ENC"
info "Credentials encrypted at $CONFIG_ENC"

# ---------------------------------------------------------------------------
# 5. Install as service
# ---------------------------------------------------------------------------
PYTHON_BIN="$VENV_DIR/bin/python3"
WATCHDOG_PY="$WATCHDOG_DIR/watchdog.py"

if [[ "$(uname)" == "Darwin" ]]; then
    # macOS LaunchAgent — generate plist safely via Python (no shell interpolation)
    PLIST="$HOME/Library/LaunchAgents/com.openclaw.watchdog.plist"
    info "Installing LaunchAgent → $PLIST"
    mkdir -p "$HOME/Library/LaunchAgents"
    python3 -c "
import plistlib, sys
plist = {
    'Label': 'com.openclaw.watchdog',
    'ProgramArguments': [sys.argv[1], sys.argv[2]],
    'RunAtLoad': True,
    'KeepAlive': True,
    'StandardOutPath': sys.argv[3] + '/watchdog.log',
    'StandardErrorPath': sys.argv[3] + '/watchdog-error.log',
    'EnvironmentVariables': {
        'PATH': '/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin',
        'OPENCLAW_HEALTH_URL': sys.argv[5]
    }
}
with open(sys.argv[4], 'wb') as f:
    plistlib.dump(plist, f)
" "$PYTHON_BIN" "$WATCHDOG_PY" "$WATCHDOG_DIR" "$PLIST" "$HEALTH_URL"

    launchctl unload "$PLIST" 2>/dev/null || true
    launchctl load "$PLIST"
    info "LaunchAgent loaded"

else
    # Linux systemd user service — write via printf (no heredoc interpolation)
    SERVICE_DIR="$HOME/.config/systemd/user"
    SERVICE_FILE="$SERVICE_DIR/openclaw-watchdog.service"
    info "Installing systemd service → $SERVICE_FILE"
    mkdir -p "$SERVICE_DIR"
    printf '[Unit]\nDescription=OpenClaw Watch Dog\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=%s %s\nRestart=always\nRestartSec=10\nEnvironment=PATH=/usr/local/bin:/usr/bin:/bin\nEnvironment=OPENCLAW_HEALTH_URL=%s\n\n[Install]\nWantedBy=default.target\n' \
        "$PYTHON_BIN" "$WATCHDOG_PY" "$HEALTH_URL" > "$SERVICE_FILE"

    systemctl --user daemon-reload
    systemctl --user enable openclaw-watchdog
    systemctl --user restart openclaw-watchdog
    info "systemd service started"
fi

# ---------------------------------------------------------------------------
# 6. Verify
# ---------------------------------------------------------------------------
sleep 3
if [[ "$(uname)" == "Darwin" ]]; then
    if launchctl list | grep -q "com.openclaw.watchdog"; then
        info "Watch Dog is running! 🐕"
    else
        warn "Service may not have started — check $WATCHDOG_DIR/watchdog-error.log"
    fi
else
    if systemctl --user is-active openclaw-watchdog >/dev/null 2>&1; then
        info "Watch Dog is running! 🐕"
    else
        warn "Service may not have started — check: journalctl --user -u openclaw-watchdog"
    fi
fi

echo ""
info "Setup complete! Watch Dog is monitoring your gateway."
info "Logs: $WATCHDOG_DIR/watchdog.log"
info "Config: $CONFIG_ENC (encrypted)"


path: scripts/test-message.py
content: #!/usr/bin/env python3
"""Send a test message via Telegram to verify bot + chat ID work."""
import urllib.request, json, sys, re

if len(sys.argv) < 3:
    print("Usage: test-message.py <telegram_token> <chat_id>", file=sys.stderr)
    sys.exit(1)

[REDACTED_SECRET]
chat_id = sys.argv[2]

# Validate token format
if not re.match(r'^\d+:[A-Za-z0-9_-]+$', token):
    print("Error: Token format invalid.", file=sys.stderr)
    sys.exit(1)

# Validate chat_id is numeric (positive or negative)
if not re.match(r'^-?\d+$', chat_id):
    print("Error: Chat ID must be numeric.", file=sys.stderr)
    sys.exit(1)

try:
    data = json.dumps({
        'chat_id': chat_id,
        'text': '\U0001f415 Watch Dog connected successfully! Monitoring your gateway 24/7.'
    }).encode()
    req = urllib.request.Request(
        f'https://api.telegram.org/bot{token}/sendMessage',
        data=data,
        headers={'Content-Type': 'application/json'}
    )
    resp = json.loads(urllib.request.urlopen(req).read())
    if resp.get('ok'):
        print('Test message sent!')
    else:
        print(f"Error: {resp}", file=sys.stderr)
        sys.exit(1)
except Exception as e:
    print(f"Error: {e}", file=sys.stderr)
    sys.exit(1)


path: scripts/validate.py
content: #!/usr/bin/env python3
"""Validate a Telegram bot token by calling getMe."""
import urllib.request, json, sys

if len(sys.argv) < 2:
    print("Usage: validate.py <telegram_token>", file=sys.stderr)
    sys.exit(1)

[REDACTED_SECRET]

# Validate token format (digits:alphanumeric)
import re
if not re.match(r'^\d+:[A-Za-z0-9_-]+$', token):
    print("Error: Token format invalid. Expected format: 123456:ABC-DEF...", file=sys.stderr)
    sys.exit(1)

try:
    resp = json.loads(urllib.request.urlopen(f'https://api.telegram.org/bot{token}/getMe').read())
    if resp.get('ok'):
        print(f"Token valid: @{resp['result']['username']}")
    else:
        print("Error: Token rejected by Telegram API", file=sys.stderr)
        sys.exit(1)
except Exception as e:
    print(f"Error: {e}", file=sys.stderr)
    sys.exit(1)


path: scripts/watchdog.py
content: #!/usr/bin/env python3
"""
OpenClaw Watch Dog — Self-healing gateway monitor.
Pings the gateway every 15 seconds, auto-fixes crashes, sends Telegram alerts.
"""

import asyncio
import json
import logging
import os
import platform
import subprocess
import sys
import time
import hashlib
import base64
from pathlib import Path
from datetime import datetime

import aiohttp

# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------
WATCHDOG_DIR = Path.home() / ".openclaw" / "watchdog"
CONFIG_FILE = WATCHDOG_DIR / "config.enc"
LOG_FILE = WATCHDOG_DIR / "watchdog.log"
STATE_FILE = WATCHDOG_DIR / "state.json"
GATEWAY_HEALTH = os.environ.get("OPENCLAW_HEALTH_URL", "http://127.0.0.1:3117/health")
APPROVE_REINSTALL = WATCHDOG_DIR / "approve-reinstall"

# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler(LOG_FILE, encoding="utf-8"),
        logging.StreamHandler(),
    ],
)
log = logging.getLogger("watchdog")

# ---------------------------------------------------------------------------
# Encryption helpers (AES-256-CBC via openssl CLI — no extra deps)
# ---------------------------------------------------------------------------

def _machine_password() -> str:
    """Derive a machine-specific password from hardware identifiers."""
    parts = []
    if platform.system() == "Darwin":
        try:
            out = subprocess.check_output(
                ["ioreg", "-rd1", "-c", "IOPlatformExpertDevice"], text=True
            )
            for line in out.splitlines():
                if "IOPlatformUUID" in line:
                    parts.append(line.split('"')[-2])
                    break
        except Exception:
            pass
    # Fallback: hostname + username
    parts.extend([platform.node(), os.getenv("USER", "openclaw")])
    return hashlib.sha256(":".join(parts).encode()).hexdigest()


def decrypt_config() -> dict:
    """Decrypt config.enc → dict."""
    if not CONFIG_FILE.exists():
        log.error("Config file not found: %s", CONFIG_FILE)
        sys.exit(1)
    pw = _machine_password()
    try:
        result = subprocess.run(
            ["openssl", "enc", "-aes-256-cbc", "-d", "-pbkdf2",
             "-in", str(CONFIG_FILE), "-pass", f"pass:{pw}"],
            capture_output=True,
        )
        if result.returncode != 0:
            log.error("Decryption failed: %s", result.stderr.decode())
            sys.exit(1)
        return json.loads(result.stdout.decode())
    except Exception as e:
        log.error("Failed to decrypt config: %s", e)
        sys.exit(1)


# ---------------------------------------------------------------------------
# Telegram
# ---------------------------------------------------------------------------

async def send_telegram(session: aiohttp.ClientSession, cfg: dict, text: str):
    """Send a Telegram message."""
    [REDACTED_SECRET]"telegram_token", "")
    chat_id = cfg.get("telegram_chat_id", "")
    if not token or not chat_id:
        log.warning("Telegram not configured, skipping alert")
        return
    url = f"https://api.telegram.org/bot{token}/sendMessage"
    try:
        async with session.post(url, json={
            "chat_id": chat_id,
            "text": text,
            "parse_mode": "HTML",
        }) as resp:
            if resp.status != 200:
                log.warning("Telegram send failed: %s", await resp.text())
    except Exception as e:
        log.warning("Telegram error: %s", e)


# ---------------------------------------------------------------------------
# Local Diagnostics (no external API calls — logs stay on device)
# ---------------------------------------------------------------------------

def diagnose_locally(logs: str) -> str:
    """Analyze logs locally using pattern matching. No data leaves the device."""
    lines = logs.lower()
    issues = []
    if "eaddrinuse" in lines:
        issues.append("Port already in use — another process may be bound to the gateway port")
    if "enomem" in lines or "out of memory" in lines:
        issues.append("Out of memory — consider freeing resources or restarting the machine")
    if "enoent" in lines or "not found" in lines:
        issues.append("Missing file or binary — OpenClaw may need reinstallation")
    if "permission denied" in lines or "eacces" in lines:
        issues.append("Permission denied — check file/directory permissions")
    if "segfault" in lines or "segmentation fault" in lines:
        issues.append("Segmentation fault — possible corrupted installation")
    if "module not found" in lines or "cannot find module" in lines:
        issues.append("Missing Node.js module — may need manual reinstallation")
    if not issues:
        issues.append("No obvious pattern detected — check logs manually")
    return "; ".join(issues)


# ---------------------------------------------------------------------------
# Log collection
# ---------------------------------------------------------------------------

def collect_logs(lines: int = 200) -> str:
    """Collect last N lines from OpenClaw gateway logs."""
    log_paths = [
        Path.home() / ".openclaw" / "gateway.log",
        Path.home() / ".openclaw" / "logs" / "gateway.log",
        Path("/tmp") / "openclaw-gateway.log",
    ]
    for p in log_paths:
        if p.exists():
            try:
                result = subprocess.run(
                    ["tail", f"-{lines}", str(p)], capture_output=True, text=True
                )
                if result.stdout.strip():
                    return result.stdout
            except Exception:
                continue

    # Try journalctl on Linux
    if platform.system() == "Linux":
        try:
            result = subprocess.run(
                ["journalctl", "--user", "-u", "openclaw-gateway", f"-n{lines}", "--no-pager"],
                capture_output=True, text=True,
            )
            if result.stdout.strip():
                return result.stdout
        except Exception:
            pass

    return "(no logs found)"


# ---------------------------------------------------------------------------
# Auto-fix
# ---------------------------------------------------------------------------

async def attempt_fix(session: aiohttp.ClientSession, cfg: dict, attempt: int) -> bool:
    """Try to fix the gateway. Returns True if health check passes after fix."""
    log.info("Auto-fix attempt #%d", attempt)

    if attempt <= 2:
        # Step 1-2: Simple restart
        log.info("Restarting gateway...")
        await send_telegram(session, cfg, "🔧 <b>Watch Dog:</b> Restarting gateway...")
        try:
            subprocess.run(["openclaw", "gateway", "restart"], timeout=30,
                           capture_output=True, text=True)
        except Exception as e:
            log.error("Restart command failed: %s", e)
            return False

        await asyncio.sleep(10)
        return await check_health(session)

    elif attempt == 3:
        # Step 3: Ask user for permission to reinstall
        log.info("Restart attempts exhausted, asking user for reinstall permission...")
        await send_telegram(session, cfg,
            "⚠️ <b>Watch Dog:</b> Gateway restart failed after 2 attempts.\n\n"
            "Reinstalling OpenClaw might fix the issue.\n"
            "To approve reinstall, run:\n"
            "<code>touch ~/.openclaw/watchdog/approve-reinstall</code>\n\n"
            "I'll check for your approval and proceed if given."
        )
        return False

    elif attempt <= 5 and APPROVE_REINSTALL.exists():
        # Step 4-5: User approved reinstall
        log.info("User approved reinstall, running npm install -g openclaw...")
        APPROVE_REINSTALL.unlink(missing_ok=True)
        await send_telegram(session, cfg, "🔧 <b>Watch Dog:</b> Reinstalling OpenClaw (approved by user)...")
        try:
            subprocess.run(["npm", "install", "-g", "openclaw"], timeout=120,
                           capture_output=True, text=True)
            await asyncio.sleep(5)
            subprocess.run(["openclaw", "gateway", "start"], timeout=30,
                           capture_output=True, text=True)
        except Exception as e:
            log.error("Reinstall failed: %s", e)
            return False

        await asyncio.sleep(10)
        return await check_health(session)

    else:
        # No approval or attempts exhausted — notify user
        log.info("Waiting for user action...")
        if attempt == 5:
            await send_telegram(session, cfg,
                "❌ <b>Watch Dog:</b> All auto-fix attempts exhausted.\n"
                "Manual intervention required:\n"
                "<code>openclaw gateway restart</code>"
            )
        return False


# ---------------------------------------------------------------------------
# Health check
# ---------------------------------------------------------------------------

async def check_health(session: aiohttp.ClientSession) -> bool:
    """Ping the gateway health endpoint."""
    try:
        async with session.get(GATEWAY_HEALTH, timeout=aiohttp.ClientTimeout(total=10)) as resp:
            return resp.status == 200
    except Exception:
        return False


# ---------------------------------------------------------------------------
# State management
# ---------------------------------------------------------------------------

def load_state() -> dict:
    if STATE_FILE.exists():
        try:
            return json.loads(STATE_FILE.read_text())
        except Exception:
            pass
    return {"consecutive_failures": 0, "fix_attempts": 0, "last_status": "unknown", "started_at": time.time()}


def save_state(state: dict):
    STATE_FILE.write_text(json.dumps(state, indent=2))


# ---------------------------------------------------------------------------
# Main loop
# ---------------------------------------------------------------------------

async def main():
    WATCHDOG_DIR.mkdir(parents=True, exist_ok=True)

    cfg = decrypt_config()
    state = load_state()
    state["started_at"] = time.time()
    save_state(state)

    log.info("Watch Dog started — monitoring %s", GATEWAY_HEALTH)

    async with aiohttp.ClientSession() as session:
        await send_telegram(session, cfg, "🐕 <b>Watch Dog started</b>\nMonitoring gateway health every 15s.")

        while True:
            healthy = await check_health(session)

            if healthy:
                if state["last_status"] != "healthy":
                    log.info("Gateway is healthy ✓")
                    if state["consecutive_failures"] > 0 or state["fix_attempts"] > 0:
                        await send_telegram(session, cfg,
                            "✅ <b>Gateway recovered!</b>\n"
                            f"After {state['fix_attempts']} fix attempt(s)."
                        )
                    state["consecutive_failures"] = 0
                    state["fix_attempts"] = 0
                    state["last_status"] = "healthy"
                    save_state(state)
            else:
                state["consecutive_failures"] += 1
                state["last_status"] = "unhealthy"
                log.warning("Health check failed (%d consecutive)", state["consecutive_failures"])

                if state["consecutive_failures"] == 3:
                    # First failure notification with diagnostics
                    logs = collect_logs()
                    diagnosis = diagnose_locally(logs)
                    await send_telegram(session, cfg,
                        "🚨 <b>Gateway DOWN!</b>\n"
                        f"3 consecutive failures detected.\n\n"
                        f"<b>AI Diagnosis:</b>\n<code>{diagnosis[:500]}</code>\n\n"
                        "Attempting auto-fix..."
                    )

                if state["consecutive_failures"] >= 3:
                    state["fix_attempts"] += 1
                    fixed = await attempt_fix(session, cfg, state["fix_attempts"])
                    if fixed:
                        state["consecutive_failures"] = 0
                        state["fix_attempts"] = 0
                        state["last_status"] = "healthy"
                        log.info("Auto-fix succeeded!")
                        await send_telegram(session, cfg, "✅ <b>Auto-fix succeeded!</b> Gateway is back online.")
                    elif state["fix_attempts"] >= 5:
                        await send_telegram(session, cfg,
                            "❌ <b>Auto-fix exhausted</b> (5 attempts).\n"
                            "Manual intervention required!"
                        )
                        state["fix_attempts"] = 0
                        state["consecutive_failures"] = 0  # Reset to avoid spam

                save_state(state)

            await asyncio.sleep(15)


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        log.info("Watch Dog stopped by user")
    except Exception as e:
        log.exception("Watch Dog crashed: %s", e)
        sys.exit(1)


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)