diff --git a/skills/continuous-learning-v2/hooks/observe.sh b/skills/continuous-learning-v2/hooks/observe.sh index 7e524f29..14a862be 100755 --- a/skills/continuous-learning-v2/hooks/observe.sh +++ b/skills/continuous-learning-v2/hooks/observe.sh @@ -83,10 +83,13 @@ fi CONFIG_DIR="${HOME}/.claude/homunculus" -# Skip if disabled +# Skip if disabled (check both default and CLV2_CONFIG-derived locations) if [ -f "$CONFIG_DIR/disabled" ]; then exit 0 fi +if [ -n "${CLV2_CONFIG:-}" ] && [ -f "$(dirname "$CLV2_CONFIG")/disabled" ]; then + exit 0 +fi # Prevent observe.sh from firing on non-human sessions to avoid: # - ECC observing its own Haiku observer sessions (self-loop) @@ -275,12 +278,109 @@ if parsed["output"] is not None: print(json.dumps(observation)) ' >> "$OBSERVATIONS_FILE" -# Signal observer if running (check both project-scoped and global observer) +# Lazy-start observer if enabled but not running (first-time setup) +# Use flock for atomic check-then-act to prevent race conditions +# Fallback for macOS (no flock): use lockfile or skip +LAZY_START_LOCK="${PROJECT_DIR}/.observer-start.lock" +_CHECK_OBSERVER_RUNNING() { + local pid_file="$1" + if [ -f "$pid_file" ]; then + local pid + pid=$(cat "$pid_file" 2>/dev/null) + # Validate PID is a positive integer (>1) to prevent signaling invalid targets + case "$pid" in + ''|*[!0-9]*|0|1) + rm -f "$pid_file" 2>/dev/null || true + return 1 + ;; + esac + if kill -0 "$pid" 2>/dev/null; then + return 0 # Process is alive + fi + # Stale PID file - remove it + rm -f "$pid_file" 2>/dev/null || true + fi + return 1 # No PID file or process dead +} + +if [ -f "${CONFIG_DIR}/disabled" ]; then + OBSERVER_ENABLED=false +else + OBSERVER_ENABLED=false + CONFIG_FILE="${SKILL_ROOT}/config.json" + # Allow CLV2_CONFIG override + if [ -n "${CLV2_CONFIG:-}" ]; then + CONFIG_FILE="$CLV2_CONFIG" + fi + # Use effective config path for both existence check and reading + EFFECTIVE_CONFIG="$CONFIG_FILE" + if [ -f "$EFFECTIVE_CONFIG" ] && [ -n "$PYTHON_CMD" ]; then + _enabled=$(CLV2_CONFIG_PATH="$EFFECTIVE_CONFIG" "$PYTHON_CMD" -c " +import json, os +with open(os.environ['CLV2_CONFIG_PATH']) as f: + cfg = json.load(f) +print(str(cfg.get('observer', {}).get('enabled', False)).lower()) +" 2>/dev/null || echo "false") + if [ "$_enabled" = "true" ]; then + OBSERVER_ENABLED=true + fi + fi +fi + +# Check both project-scoped AND global PID files (with stale PID recovery) +if [ "$OBSERVER_ENABLED" = "true" ]; then + # Clean up stale PID files first + _CHECK_OBSERVER_RUNNING "${PROJECT_DIR}/.observer.pid" || true + _CHECK_OBSERVER_RUNNING "${CONFIG_DIR}/.observer.pid" || true + + # Check if observer is now running after cleanup + if [ ! -f "${PROJECT_DIR}/.observer.pid" ] && [ ! -f "${CONFIG_DIR}/.observer.pid" ]; then + # Use flock if available (Linux), fallback for macOS + if command -v flock >/dev/null 2>&1; then + ( + flock -n 9 || exit 0 + # Double-check PID files after acquiring lock + _CHECK_OBSERVER_RUNNING "${PROJECT_DIR}/.observer.pid" || true + _CHECK_OBSERVER_RUNNING "${CONFIG_DIR}/.observer.pid" || true + if [ ! -f "${PROJECT_DIR}/.observer.pid" ] && [ ! -f "${CONFIG_DIR}/.observer.pid" ]; then + nohup "${SKILL_ROOT}/agents/start-observer.sh" start >/dev/null 2>&1 & + fi + ) 9>"$LAZY_START_LOCK" + else + # macOS fallback: use lockfile if available, otherwise skip + if command -v lockfile >/dev/null 2>&1; then + # Use subshell to isolate exit and add trap for cleanup + ( + trap 'rm -f "$LAZY_START_LOCK" 2>/dev/null || true' EXIT + lockfile -r 1 -l 30 "$LAZY_START_LOCK" 2>/dev/null || exit 0 + _CHECK_OBSERVER_RUNNING "${PROJECT_DIR}/.observer.pid" || true + _CHECK_OBSERVER_RUNNING "${CONFIG_DIR}/.observer.pid" || true + if [ ! -f "${PROJECT_DIR}/.observer.pid" ] && [ ! -f "${CONFIG_DIR}/.observer.pid" ]; then + nohup "${SKILL_ROOT}/agents/start-observer.sh" start >/dev/null 2>&1 & + fi + rm -f "$LAZY_START_LOCK" 2>/dev/null || true + ) + fi + fi + fi +fi + +# Signal observer if running (check both project-scoped and global observer, deduplicate) +signaled_pids=" " for pid_file in "${PROJECT_DIR}/.observer.pid" "${CONFIG_DIR}/.observer.pid"; do if [ -f "$pid_file" ]; then - observer_pid=$(cat "$pid_file") + observer_pid=$(cat "$pid_file" 2>/dev/null || true) + # Validate PID is a positive integer (>1) + case "$observer_pid" in + ''|*[!0-9]*|0|1) rm -f "$pid_file" 2>/dev/null || true; continue ;; + esac + # Deduplicate: skip if already signaled this pass + case "$signaled_pids" in + *" $observer_pid "*) continue ;; + esac if kill -0 "$observer_pid" 2>/dev/null; then kill -USR1 "$observer_pid" 2>/dev/null || true + signaled_pids="${signaled_pids}${observer_pid} " fi fi done