ANVIL Memory Troubleshooting — Mac Studio ANVIL Memory Troubleshooting — Mac Studio Practical runbook for diagnosing and clearing RAM pressure on ANVIL (Makinja's Mac Studio, M3 Ultra, 96GB). Companion to Disk & Memory Health Alarms — What Fires, Where It Lands, How to Test , which documents the CEO-facing Telegram/email alert layer ( health-monitor-anvil.js ). This page covers the auto-remediation daemons — what actually kills processes and frees RAM, independent of whether anyone gets notified. Verified against live system state 2026-07-28 08:50 local (tool-first: vm_stat , uptime , plist files, and today's log files were read directly — see Evidence at bottom). The 4 layers that touch ANVIL memory Layer Script Trigger What it does 1. Watchdog ~/system/tools/memory-watchdog.sh LaunchAgent com.alai.memory-watchdog , every 300s Free-RAM tiered response: WARN/ALARM/PANIC (see below) 2. Guardian ~/system/tools/system-guardian.js LaunchAgent com.john.system-guardian , every 300s RAM/disk/load/docker auto-optimizer, zombie killer, Ollama idle-unload 3. Ollama guard ~/system/tools/ollama-memory-guard.js LaunchAgent com.alai.ollama-guard , every 60s Enforces max-1-model-loaded in Ollama; unloads all models if RAM ≥ 80% 4. Zombie cleanup ~/system/tools/zombie-proc-cleanup.sh LaunchAgent com.alai.zombie-cleanup (hourly) + invoked directly by layer 1's ALARM branch Kills orphaned ollama runner processes and stale grep -rn processes (>10 min) The CEO-alert layer ( health-monitor-anvil.js , Telegram → email → log fallback) is a 5th, separate mechanism — see the linked page above. It reads the same vm_stat data but does not perform remediation itself. Layer 1 — memory-watchdog.sh thresholds (free RAM, of 96GB total) WARN : free < 15 GB → log + Slack notice only ALARM : free < 8 GB → runs zombie-proc-cleanup.sh + Slack alarm PANIC : free < 3 GB → launchctl bootout / bootstrap on com.alai.ollama-serve-v2 , SIGTERM on all ollama runner --model and stray grep -rn procs, sync , + Slack page Layer 2 — system-guardian.js thresholds RAM: warn 80%, critical 92% (kills zombies + ollama stop at critical) Disk: warn 75%, critical 85% (Docker image/builder prune at critical; APFS-snapshot-aware) Load avg (5m): warn 15 (tuned for 24-core M3 Ultra) Zombie RSS: kills any grep / process using > 500MB Ollama idle-unload: ollama stop after 30 min with no state-file update (wall-clock based, does not depend on the RAM reading) Layer 3 — ollama-memory-guard.js Polls GET /api/ps on localhost:11434 every 60s If Ollama RAM% (measured via its own vm_stat parse) ≥ 80% → unloads all loaded models ( keep_alive: 0 ) Else if > 1 model loaded → unloads all but the first (oldest survives, rest get unloaded) This is currently the most reliable of the three remediation daemons (see bugs below) — confirmed live in ~/system/logs/ollama-guard.log actively unloading bge-m3 / llama3.1:8b while keeping qwen2.5-coder:32b . ⚠️ Two known live bugs (verified 2026-07-28, not previously documented) Both are launchd PATH bugs — the scripts' logic is correct, but the shell environment launchd gives them is missing binaries they call by bare name. Bug A — memory-watchdog.sh: Slack alerts silently fail com.alai.memory-watchdog.plist sets no EnvironmentVariables /PATH override , so it runs under launchd's bare default PATH ( /usr/bin:/bin:/usr/sbin:/sbin ), which does not include /opt/homebrew/bin where node lives on this machine. The script's slack_alert() function shells out to node "$HOME/system/tools/slack.js" ... , which fails with node: command not found on every single WARN/ALARM/PANIC cycle . Live proof — ~/system/logs/memory-watchdog.log , today (2026-07-28), the 08:29:26 PANIC event: [2026-07-28 08:29:26] PANIC — free RAM 1.35 GB (< 3 GB threshold) [2026-07-28 08:29:26] PANIC action 1: brew services restart ollama (kills zombie runners) [2026-07-28 08:29:28] PANIC action 2: SIGTERM all ollama runner procs + grep -rn procs [2026-07-28 08:29:28] PANIC action 3: purge page caches (sudo required — may skip) /Users/makinja/system/tools/memory-watchdog.sh: line 29: node: command not found The remediation itself ( launchctl bootout / bootstrap , pgrep / kill , sync ) still runs fine — those are bash builtins or default-PATH binaries. Only the Slack notification is silently dropped , every cycle, at every severity level. This has been happening at least since today's log window and is likely long-standing (the plist has no PATH override at all, so it was never going to work since the tool was written to call node by bare name). Fix: add an EnvironmentVariables dict to com.alai.memory-watchdog.plist with PATH including /opt/homebrew/bin (mirror what com.john.system-guardian.plist already does), then launchctl kickstart -k gui/501/com.alai.memory-watchdog . Bug B — system-guardian.js: RAM/load checks always NaN com.john.system-guardian.plist does set a PATH override, but it is /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin — it drops /usr/sbin , where sysctl lives. getMemory() and getLoadAvg() in system-guardian.js both shell out to bare sysctl ( sysctl -n hw.memsize , sysctl -n vm.loadavg ), which fails, so both functions return NaN . Live proof — ~/system/logs/system-guardian.log , today, every 5-min cycle: /bin/sh: sysctl: command not found /bin/sh: sysctl: command not found [2026-07-28T06:48:24.644Z] [INFO] RAM: NaN/NaNGB (NaN%) | Disk /System/Volumes/Data: 82% (153Gi free) | Load: NaN/NaN/NaN | Docker: 20.2GB Because NaN >= 92 and NaN >= 15 both evaluate to false in JS, the entire RAM-critical branch (zombie kill + ollama stop ) and the load-avg alert never fire , regardless of actual memory pressure. Only the disk-percent branch (uses df , which works) and the wall-clock Ollama-idle-unload timer (doesn't need the RAM reading) still function. In effect, Guardian's headline job — auto-react to real RAM pressure — has been a no-op for as long as this PATH has been set this way. Fix: add /usr/sbin (and ideally /sbin ) to the PATH value in com.john.system-guardian.plist , then launchctl kickstart -k gui/501/com.john.system-guardian . Net effect of both bugs together Right now the only layer reliably protecting ANVIL from an Ollama-driven OOM is Layer 3 ( ollama-memory-guard.js ), because it reads vm_stat itself (in-process, no bare- sysctl / node PATH dependency) rather than shelling out. Layers 1 and 2 are still doing their kill/restart actions on the RAM axis (Layer 1's PANIC branch has no PATH dependency for the actual kills — only its Slack call fails), but Layer 2's RAM branch is fully dead, and Layer 1 is flying blind from a notification standpoint. Live snapshot at time of writing (2026-07-28, ~08:50 local) uptime : load averages 12.83 / 11.04 / 9.11 (5m load 11.04 is above Guardian's own WARN=15? No — below 15, but above health-monitor-anvil's cpu_load.warn=8 and alert=12 thresholds — i.e. CPU load has been in ALERT territory on the CEO-facing monitor even though Guardian's own load check is silently broken per Bug B). memory-watchdog.log shows the system oscillating between OK / WARN / ALARM / PANIC multiple times within a single hour (08:04 ALARM, 08:29 PANIC, 08:49 WARN) — driven by Ollama model loads ( qwen2.5-coder:32b + others cycling in ollama-guard.log ) competing with Virtualization.framework XPC (Docker Desktop's VM) and Claude Code ( claude , node ) processes. This is a recurring, not one-off pattern per the log — the machine is running close to its RAM ceiling under normal multi-agent load. Manual diagnostic commands # Current free/used memory (what all the daemons parse) vm_stat # Top RSS consumers right now ps -A -o pid,rss,comm | sort -k2 -rn | head -15 # Ollama's own view of loaded models + RAM curl -s http://localhost:11434/api/ps | python3 -m json.tool # Are the daemons actually running? launchctl list | grep -iE "memory-watchdog|system-guardian|ollama-guard|zombie-cleanup" # Tail today's remediation activity tail -50 ~/system/logs/memory-watchdog.log tail -50 ~/system/logs/system-guardian.log tail -50 ~/system/logs/ollama-guard.log tail -30 ~/system/logs/zombie-cleanup.log # Historical breach data (health-monitor-anvil.js writes here — separate from the two daemons above) sqlite3 ~/system/databases/health-events.db \ "SELECT timestamp, check_name, status, value FROM health_events WHERE source='anvil' ORDER BY timestamp DESC LIMIT 20;" Manual remediation commands (if daemons haven't caught it yet) # Unload all Ollama models immediately ollama stop # Kill a specific stuck ollama runner by PID (from `ps` above) kill -TERM # Restart the Ollama service (equivalent to memory-watchdog.sh's PANIC action 1) launchctl bootout gui/501/com.alai.ollama-serve-v2 2>/dev/null launchctl bootstrap gui/501 ~/Library/LaunchAgents/com.alai.ollama-serve-v2.plist # Kick a specific watchdog daemon to re-run right now launchctl kickstart -k gui/501/com.alai.memory-watchdog launchctl kickstart -k gui/501/com.john.system-guardian Evidence (tool-verified, not from memory) ~/system/tools/memory-watchdog.sh , ~/system/tools/system-guardian.js , ~/system/tools/ollama-memory-guard.js , ~/system/tools/zombie-proc-cleanup.sh , ~/system/tools/health-monitor-anvil.js — read in full 2026-07-28. ~/Library/LaunchAgents/com.alai.memory-watchdog.plist (no PATH override) and ~/Library/LaunchAgents/com.john.system-guardian.plist (PATH missing /usr/sbin ) — read in full 2026-07-28. ~/system/logs/memory-watchdog.log , ~/system/logs/system-guardian.log , ~/system/logs/ollama-guard.log , ~/system/logs/zombie-cleanup.log — tailed live 2026-07-28 ~08:50. vm_stat , uptime — run live 2026-07-28 ~08:50. BookStack page anvil-memory-troubleshooting-mac-studio (id 2676, book "Operations") — confirmed pre-existing as an empty stub (0 chars markdown, last updated 2026-07-05) before this runbook was written.