#!/usr/bin/env bash # Drift watchdog for OpenClaw memorySearch embeddings config. set -euo pipefail SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ENFORCE_SH="${SKILL_DIR}/enforce.sh" CONFIG_PATH="${OPENCLAW_CONFIG_PATH:-${HOME}/.openclaw/openclaw.json}" MODEL="" BASE_URL="http://127.0.0.1:11434/v1/" INTERVAL_SEC=60 ONCE=0 RESTART_ON_HEAL=0 INSTALL_LAUNCHD=0 UNINSTALL_LAUNCHD=0 QUIET=0 PLIST_NAME="bot.molt.openclaw.embedding-guard" PLIST_PATH="${HOME}/Library/LaunchAgents/${PLIST_NAME}.plist" LOG_DIR="${HOME}/.openclaw/logs" STDOUT_LOG="${LOG_DIR}/embedding-guard.out.log" STDERR_LOG="${LOG_DIR}/embedding-guard.err.log" usage() { cat <<'EOF' Usage: watchdog.sh [options] Modes: --once run one check/heal cycle, then exit (default) run continuously and check every --interval-sec --install-launchd install + load launchd job (macOS) --uninstall-launchd unload + remove launchd job (macOS) Other OS guidance: Linux: run --once via cron/systemd timer Windows: not supported (bash script) Linux cron example (every 5 min): */5 * * * * /bin/bash ~/.openclaw/skills/ollama-memory-embeddings/watchdog.sh --once --model embeddinggemma >/dev/null 2>&1 Options: --model model to enforce (required for new installs) --base-url base URL to enforce (default: http://127.0.0.1:11434/v1/) --openclaw-config config path (default: ~/.openclaw/openclaw.json) --interval-sec check interval (default: 60) --restart-on-heal restart gateway after drift heal --quiet suppress non-error output --help show help EOF } while [ $# -gt 0 ]; do case "$1" in --model) MODEL="$2"; shift 2 ;; --base-url) BASE_URL="$2"; shift 2 ;; --openclaw-config) CONFIG_PATH="$2"; shift 2 ;; --interval-sec) INTERVAL_SEC="$2"; shift 2 ;; --once) ONCE=1; shift ;; --restart-on-heal) RESTART_ON_HEAL=1; shift ;; --install-launchd) INSTALL_LAUNCHD=1; shift ;; --uninstall-launchd) UNINSTALL_LAUNCHD=1; shift ;; --quiet) QUIET=1; shift ;; --help|-h) usage; exit 0 ;; *) echo "Unknown option: $1"; usage; exit 1 ;; esac done log() { [ "$QUIET" -eq 1 ] || echo "$@" } require_cmd() { command -v "$1" >/dev/null 2>&1 || { echo "ERROR: '$1' not found in PATH." exit 1 } } resolve_model_if_missing() { if [ -n "$MODEL" ]; then return 0 fi MODEL="$(node -e ' const fs=require("fs"); const p=process.argv[1]; try { const cfg=JSON.parse(fs.readFileSync(p,"utf8")); process.stdout.write(cfg?.agents?.defaults?.memorySearch?.model || ""); } catch (_) {} ' "$CONFIG_PATH")" if [ -z "$MODEL" ]; then echo "ERROR: --model is required (or set memorySearch.model first)." exit 1 fi } run_cycle() { set +e "$ENFORCE_SH" \ --check-only \ --model "$MODEL" \ --base-url "$BASE_URL" \ --openclaw-config "$CONFIG_PATH" \ --quiet status=$? set -e if [ "$status" -eq 0 ]; then log "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] OK: no drift" return 0 fi if [ "$status" -ne 10 ]; then log "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: drift check failed (status $status)" return 1 fi log "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] DRIFT: healing..." if [ "$RESTART_ON_HEAL" -eq 1 ]; then "$ENFORCE_SH" --model "$MODEL" --base-url "$BASE_URL" --openclaw-config "$CONFIG_PATH" --restart-on-change --quiet else "$ENFORCE_SH" --model "$MODEL" --base-url "$BASE_URL" --openclaw-config "$CONFIG_PATH" --quiet fi log "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] HEALED" } install_launchd() { if [ "$(uname)" != "Darwin" ]; then echo "ERROR: --install-launchd is macOS only." echo "Linux recommendation:" echo " Use cron or a systemd timer to run:" echo " /bin/bash ${SKILL_DIR}/watchdog.sh --once --model " echo "Windows: not supported (bash script)." exit 1 fi require_cmd launchctl require_cmd node resolve_model_if_missing mkdir -p "$(dirname "$PLIST_PATH")" "$LOG_DIR" local shell_bin shell_bin="$(command -v bash)" cat > "$PLIST_PATH" < Label ${PLIST_NAME} ProgramArguments ${shell_bin} ${SKILL_DIR}/watchdog.sh --once --model ${MODEL} --base-url ${BASE_URL} --openclaw-config ${CONFIG_PATH} $( [ "$RESTART_ON_HEAL" -eq 1 ] && echo " --restart-on-heal" ) RunAtLoad StartInterval ${INTERVAL_SEC} StandardOutPath ${STDOUT_LOG} StandardErrorPath ${STDERR_LOG} EOF launchctl bootout "gui/$(id -u)/${PLIST_NAME}" >/dev/null 2>&1 || true launchctl bootstrap "gui/$(id -u)" "$PLIST_PATH" launchctl kickstart -k "gui/$(id -u)/${PLIST_NAME}" log "Installed launchd watchdog: ${PLIST_PATH}" } uninstall_launchd() { if [ "$(uname)" != "Darwin" ]; then echo "ERROR: --uninstall-launchd is macOS only." echo "Windows: not supported (bash script)." exit 1 fi require_cmd launchctl launchctl bootout "gui/$(id -u)/${PLIST_NAME}" >/dev/null 2>&1 || true rm -f "$PLIST_PATH" log "Removed launchd watchdog: ${PLIST_PATH}" } if [ "$INSTALL_LAUNCHD" -eq 1 ] && [ "$UNINSTALL_LAUNCHD" -eq 1 ]; then echo "ERROR: choose only one of --install-launchd or --uninstall-launchd." exit 1 fi if [ "$INSTALL_LAUNCHD" -eq 1 ]; then install_launchd exit 0 fi if [ "$UNINSTALL_LAUNCHD" -eq 1 ]; then uninstall_launchd exit 0 fi require_cmd node resolve_model_if_missing if [ "$ONCE" -eq 1 ]; then run_cycle exit 0 fi while true; do run_cycle || true sleep "$INTERVAL_SEC" done