CRITICAL: This commit fixes both the zombie process issue AND the broken context recall system that was failing silently due to encoding errors. ROOT CAUSES FIXED: 1. Periodic save running every 1 minute (540 processes/hour) 2. Missing timeouts on subprocess calls (hung processes) 3. Background spawning with & (orphaned processes) 4. No mutex lock (overlapping executions) 5. Missing UTF-8 encoding in log functions (BREAKING context saves) FIXES IMPLEMENTED: Fix 1.1 - Reduce Periodic Save Frequency (80% reduction) - File: .claude/hooks/setup_periodic_save.ps1 - Change: RepetitionInterval 1min -> 5min - Impact: 540 -> 108 processes/hour from periodic saves Fix 1.2 - Add Subprocess Timeouts (prevent hangs) - Files: periodic_save_check.py (3 calls), periodic_context_save.py (4 calls) - Change: Added timeout=5 to all subprocess.run() calls - Impact: Prevents indefinitely hung git/ssh processes Fix 1.3 - Remove Background Spawning (eliminate orphans) - Files: user-prompt-submit (line 68), task-complete (lines 171, 178) - Change: Removed & from sync-contexts spawning, made synchronous - Impact: Eliminates 290 orphaned processes/hour Fix 1.4 - Add Mutex Lock (prevent overlaps) - File: periodic_save_check.py - Change: Added acquire_lock()/release_lock() with try/finally - Impact: Prevents Task Scheduler from spawning overlapping instances Fix 1.5 - Add UTF-8 Encoding (CRITICAL - enables context saves) - Files: periodic_context_save.py, periodic_save_check.py - Change: Added encoding="utf-8" to all log file opens - Impact: FIXES silent failure preventing ALL context saves since deployment TOOLS ADDED: - monitor_zombies.ps1: PowerShell script to track process counts and memory EXPECTED RESULTS: - Before: 1,010 processes/hour, 3-7 GB RAM/hour - After: ~151 processes/hour (85% reduction), minimal RAM growth - Context recall: NOW WORKING (was completely broken) TESTING: - Run monitor_zombies.ps1 before and after 30min work session - Verify context auto-injection on Claude Code restart - Check .claude/periodic-save.log for successful saves (no encoding errors) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
164 lines
5.4 KiB
Bash
164 lines
5.4 KiB
Bash
#!/bin/bash
|
|
#
|
|
# Claude Code Hook: user-prompt-submit (v2 - with offline support)
|
|
# Runs BEFORE each user message is processed
|
|
# Injects relevant context from the database into the conversation
|
|
# FALLBACK: Uses local cache when API is unavailable
|
|
#
|
|
# Expected environment variables:
|
|
# CLAUDE_PROJECT_ID - UUID of the current project
|
|
# JWT_TOKEN - Authentication token for API
|
|
# CLAUDE_API_URL - API base URL (default: http://172.16.3.30:8001)
|
|
# CONTEXT_RECALL_ENABLED - Set to "false" to disable (default: true)
|
|
# MIN_RELEVANCE_SCORE - Minimum score for context (default: 5.0)
|
|
# MAX_CONTEXTS - Maximum number of contexts to retrieve (default: 10)
|
|
#
|
|
|
|
# Load configuration if exists
|
|
CONFIG_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/context-recall-config.env"
|
|
if [ -f "$CONFIG_FILE" ]; then
|
|
source "$CONFIG_FILE"
|
|
fi
|
|
|
|
# Default values
|
|
API_URL="${CLAUDE_API_URL:-http://172.16.3.30:8001}"
|
|
ENABLED="${CONTEXT_RECALL_ENABLED:-true}"
|
|
MIN_SCORE="${MIN_RELEVANCE_SCORE:-5.0}"
|
|
MAX_ITEMS="${MAX_CONTEXTS:-10}"
|
|
|
|
# Local storage paths
|
|
CLAUDE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
CACHE_DIR="$CLAUDE_DIR/context-cache"
|
|
QUEUE_DIR="$CLAUDE_DIR/context-queue"
|
|
|
|
# Exit early if disabled
|
|
if [ "$ENABLED" != "true" ]; then
|
|
exit 0
|
|
fi
|
|
|
|
# Detect project ID from git repo if not set
|
|
if [ -z "$CLAUDE_PROJECT_ID" ]; then
|
|
# Try to get from git config
|
|
PROJECT_ID=$(git config --local claude.projectid 2>/dev/null)
|
|
|
|
if [ -z "$PROJECT_ID" ]; then
|
|
# Try to derive from git remote URL
|
|
GIT_REMOTE=$(git config --get remote.origin.url 2>/dev/null)
|
|
if [ -n "$GIT_REMOTE" ]; then
|
|
# Hash the remote URL to create a consistent ID
|
|
PROJECT_ID=$(echo -n "$GIT_REMOTE" | md5sum | cut -d' ' -f1)
|
|
fi
|
|
fi
|
|
else
|
|
PROJECT_ID="$CLAUDE_PROJECT_ID"
|
|
fi
|
|
|
|
# Exit if no project ID available
|
|
if [ -z "$PROJECT_ID" ]; then
|
|
# Silent exit - no context available
|
|
exit 0
|
|
fi
|
|
|
|
# Create cache directory if it doesn't exist
|
|
PROJECT_CACHE_DIR="$CACHE_DIR/$PROJECT_ID"
|
|
mkdir -p "$PROJECT_CACHE_DIR" 2>/dev/null
|
|
|
|
# Try to sync any queued contexts first (opportunistic)
|
|
# NOTE: Changed from background (&) to synchronous to prevent zombie processes
|
|
if [ -d "$QUEUE_DIR/pending" ] && [ -n "$JWT_TOKEN" ]; then
|
|
bash "$(dirname "${BASH_SOURCE[0]}")/sync-contexts" >/dev/null 2>&1
|
|
fi
|
|
|
|
# Build API request URL
|
|
RECALL_URL="${API_URL}/api/conversation-contexts/recall"
|
|
QUERY_PARAMS="project_id=${PROJECT_ID}&limit=${MAX_ITEMS}&min_relevance_score=${MIN_SCORE}"
|
|
|
|
# Try to fetch context from API (with timeout and error handling)
|
|
API_AVAILABLE=false
|
|
if [ -n "$JWT_TOKEN" ]; then
|
|
CONTEXT_RESPONSE=$(curl -s --max-time 3 \
|
|
"${RECALL_URL}?${QUERY_PARAMS}" \
|
|
-H "Authorization: Bearer ${JWT_TOKEN}" \
|
|
-H "Accept: application/json" 2>/dev/null)
|
|
|
|
if [ $? -eq 0 ] && [ -n "$CONTEXT_RESPONSE" ]; then
|
|
# Check if response is valid JSON (not an error)
|
|
echo "$CONTEXT_RESPONSE" | python3 -c "import sys, json; json.load(sys.stdin)" 2>/dev/null
|
|
if [ $? -eq 0 ]; then
|
|
API_AVAILABLE=true
|
|
# Save to cache for offline use
|
|
echo "$CONTEXT_RESPONSE" > "$PROJECT_CACHE_DIR/latest.json"
|
|
echo "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" > "$PROJECT_CACHE_DIR/last_updated"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Fallback to local cache if API unavailable
|
|
if [ "$API_AVAILABLE" = "false" ]; then
|
|
if [ -f "$PROJECT_CACHE_DIR/latest.json" ]; then
|
|
CONTEXT_RESPONSE=$(cat "$PROJECT_CACHE_DIR/latest.json")
|
|
CACHE_AGE="unknown"
|
|
if [ -f "$PROJECT_CACHE_DIR/last_updated" ]; then
|
|
CACHE_AGE=$(cat "$PROJECT_CACHE_DIR/last_updated")
|
|
fi
|
|
echo "<!-- Using cached context (API unavailable) - Last updated: $CACHE_AGE -->" >&2
|
|
else
|
|
# No cache available, exit silently
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
# Parse and format context
|
|
CONTEXT_COUNT=$(echo "$CONTEXT_RESPONSE" | grep -o '"id"' | wc -l)
|
|
|
|
if [ "$CONTEXT_COUNT" -gt 0 ]; then
|
|
if [ "$API_AVAILABLE" = "true" ]; then
|
|
echo "<!-- Context Recall: Retrieved $CONTEXT_COUNT relevant context(s) from API -->"
|
|
else
|
|
echo "<!-- Context Recall: Retrieved $CONTEXT_COUNT relevant context(s) from LOCAL CACHE (offline mode) -->"
|
|
fi
|
|
echo ""
|
|
echo "## Previous Context"
|
|
echo ""
|
|
if [ "$API_AVAILABLE" = "false" ]; then
|
|
echo "[WARNING] **Offline Mode** - Using cached context (API unavailable)"
|
|
echo ""
|
|
fi
|
|
echo "The following context has been automatically recalled:"
|
|
echo ""
|
|
|
|
# Extract and format each context entry
|
|
echo "$CONTEXT_RESPONSE" | python3 -c "
|
|
import sys, json
|
|
try:
|
|
contexts = json.load(sys.stdin)
|
|
if isinstance(contexts, list):
|
|
for i, ctx in enumerate(contexts, 1):
|
|
title = ctx.get('title', 'Untitled')
|
|
summary = ctx.get('dense_summary', '')
|
|
score = ctx.get('relevance_score', 0)
|
|
ctx_type = ctx.get('context_type', 'unknown')
|
|
|
|
print(f'### {i}. {title} (Score: {score}/10)')
|
|
print(f'*Type: {ctx_type}*')
|
|
print()
|
|
print(summary)
|
|
print()
|
|
print('---')
|
|
print()
|
|
except:
|
|
pass
|
|
" 2>/dev/null
|
|
|
|
echo ""
|
|
if [ "$API_AVAILABLE" = "true" ]; then
|
|
echo "*Context automatically injected to maintain continuity across sessions.*"
|
|
else
|
|
echo "*Context from local cache - new context will sync when API is available.*"
|
|
fi
|
|
echo ""
|
|
fi
|
|
|
|
# Exit successfully
|
|
exit 0
|