From 5bb36150c4b9e9b8333daa937a801065686ebfeb Mon Sep 17 00:00:00 2001 From: sol Date: Sat, 7 Mar 2026 17:41:03 +0000 Subject: [PATCH] feat(phase4): add gateway:startup hook for auto-starting watcher daemon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - hooks/status-watcher-hook/HOOK.md — events: ["gateway:startup"], required env vars - hooks/status-watcher-hook/handler.js — checks PID file, spawns watcher-manager.js detached - Deployed hook to /home/node/.openclaw/workspace/hooks/status-watcher-hook/ - make check passes --- hooks/status-watcher-hook/HOOK.md | 53 ++++++++++++++ hooks/status-watcher-hook/handler.js | 102 +++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 hooks/status-watcher-hook/HOOK.md create mode 100644 hooks/status-watcher-hook/handler.js diff --git a/hooks/status-watcher-hook/HOOK.md b/hooks/status-watcher-hook/HOOK.md new file mode 100644 index 0000000..56bf5a2 --- /dev/null +++ b/hooks/status-watcher-hook/HOOK.md @@ -0,0 +1,53 @@ +# status-watcher-hook + +Auto-starts the Live Status v4 daemon when the OpenClaw gateway starts. + +## Events + +```json +["gateway:startup"] +``` + +## Description + +On gateway startup, this hook checks whether the status-watcher daemon is already +running (via PID file). If not, it spawns `watcher-manager.js start` as a detached +background process, then exits immediately. The daemon continues running independently +of this hook handler. + +## Required Environment Variables + +The following environment variables must be set for the watcher to function: + +``` +MM_TOKEN Mattermost bot token +MM_URL Mattermost base URL (e.g. https://slack.solio.tech) +TRANSCRIPT_DIR Path to agent sessions directory +SESSIONS_JSON Path to sessions.json +``` + +Optional (defaults shown): + +``` +THROTTLE_MS 500 +IDLE_TIMEOUT_S 60 +MAX_STATUS_LINES 15 +MAX_ACTIVE_SESSIONS 20 +MAX_MESSAGE_CHARS 15000 +HEALTH_PORT 9090 +LOG_LEVEL info +PID_FILE /tmp/status-watcher.pid +CIRCUIT_BREAKER_THRESHOLD 5 +CIRCUIT_BREAKER_COOLDOWN_S 30 +``` + +## Installation + +This hook is deployed automatically by `install.sh` or `deploy-to-agents.sh`. +To deploy manually: + +```sh +cp -r hooks/status-watcher-hook /home/node/.openclaw/workspace/hooks/ +``` + +The hook activates on the next gateway startup. diff --git a/hooks/status-watcher-hook/handler.js b/hooks/status-watcher-hook/handler.js new file mode 100644 index 0000000..e021bdc --- /dev/null +++ b/hooks/status-watcher-hook/handler.js @@ -0,0 +1,102 @@ +'use strict'; + +/** + * status-watcher-hook/handler.js + * + * Spawns the Live Status v4 watcher-manager daemon on gateway startup. + * + * Events: ["gateway:startup"] + * + * Behavior: + * 1. Check PID file — if watcher is already running, do nothing. + * 2. If not running, spawn watcher-manager.js as a detached background process. + * 3. The hook handler returns immediately; the daemon runs independently. + */ + +/* eslint-disable no-console */ + +const fs = require('fs'); +const path = require('path'); +const { spawn } = require('child_process'); + +// PID file location (must match watcher-manager.js default) +const PID_FILE = process.env.PID_FILE || '/tmp/status-watcher.pid'; + +// Path to watcher-manager.js (relative to this hook file's location) +// Hook is in: workspace/hooks/status-watcher-hook/handler.js +// Watcher is in: workspace/projects/openclaw-live-status/src/watcher-manager.js +const WATCHER_PATH = path.resolve( + __dirname, + '../../projects/openclaw-live-status/src/watcher-manager.js', +); + +/** + * Check if a process is alive given its PID. + * Returns true if process exists and is running. + */ +function isProcessRunning(pid) { + try { + process.kill(pid, 0); + return true; + } catch (_err) { + return false; + } +} + +/** + * Check if the watcher daemon is already running via PID file. + * Returns true if running, false if not (or PID file stale/missing). + */ +function isWatcherRunning() { + try { + // eslint-disable-next-line security/detect-non-literal-fs-filename + const pidStr = fs.readFileSync(PID_FILE, 'utf8').trim(); + const pid = parseInt(pidStr, 10); + if (isNaN(pid) || pid <= 0) return false; + return isProcessRunning(pid); + } catch (_err) { + // PID file missing or unreadable — watcher is not running + return false; + } +} + +/** + * Spawn the watcher daemon as a detached background process. + * The parent (this hook handler) does not wait for it. + */ +function spawnWatcher() { + if (!fs.existsSync(WATCHER_PATH)) { + console.error('[status-watcher-hook] watcher-manager.js not found at:', WATCHER_PATH); + console.error('[status-watcher-hook] Deploy the live-status project first: see install.sh'); + return; + } + + console.log('[status-watcher-hook] Starting Live Status v4 watcher daemon...'); + + const child = spawn(process.execPath, [WATCHER_PATH, 'start'], { + detached: true, + stdio: 'ignore', + env: process.env, + }); + + child.unref(); + + console.log( + '[status-watcher-hook] Watcher daemon spawned (PID will be written to', + PID_FILE + ')', + ); +} + +/** + * Hook entry point — called by OpenClaw on gateway:startup event. + */ +async function handle(_event) { + if (isWatcherRunning()) { + console.log('[status-watcher-hook] Watcher already running, skipping spawn.'); + return; + } + + spawnWatcher(); +} + +module.exports = { handle };