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/bootstraponcom.alai.ollama-serve-v2, SIGTERM on allollama runner --modeland straygrep -rnprocs,sync, + Slack page
Layer 2 — system-guardian.js thresholds
- RAM: warn 80%, critical 92% (kills zombies +
ollama stopat 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/<defunct>process using > 500MB - Ollama idle-unload:
ollama stopafter 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/psonlocalhost:11434every 60s - If Ollama RAM% (measured via its own
vm_statparse) ≥ 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.logactively unloadingbge-m3/llama3.1:8bwhile keepingqwen2.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'scpu_load.warn=8andalert=12thresholds — 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.logshows 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 inollama-guard.log) competing withVirtualization.frameworkXPC (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 <pid>
# 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.
No comments to display
No comments to display