1717#
1818# Usage: run-with-ram <command> [args...]
1919# Env: NOT_CURSOR=1 skip closing Cursor (so the agent doesn't kill itself while debugging)
20+ # Env: DRY_RUN=1 skip closing any apps
2021
2122set -o errexit
2223set -o nounset
@@ -25,6 +26,7 @@ set -o pipefail
2526if [[ $# -lt 1 ]]; then
2627 >&2 echo " USAGE: run-with-ram <command> [args...]"
2728 >&2 echo " NOT_CURSOR=1 skip closing Cursor (for debugging)"
29+ >&2 echo " DRY_RUN=1 skip closing any apps"
2830 exit 1
2931fi
3032
@@ -71,7 +73,7 @@ is_running() {
7173#
7274# Thresholds are conservative (kill the job, not the machine):
7375# - free < 512 MB AND compressed > 8 GB → near-panic territory
74- # - pageouts > 50000 → kernel actively evicting pages
76+ # - pageouts since watchdog start > 50000 → kernel actively evicting pages
7577# - swap file count >= 30 → at 36 files, we got a kernel panic last time
7678WATCHDOG_PID=" "
7779
@@ -82,15 +84,21 @@ start_watchdog() {
8284 # swap files, vm_stat hiccups) must not kill the monitoring loop.
8385 set +o errexit
8486 set +o pipefail
87+
88+ # vm_stat Pageouts is cumulative since boot; track delta from watchdog start.
89+ local baseline_pageouts
90+ baseline_pageouts=$( awk ' /Pageouts/{gsub(/\./,"",$2); print $2}' <<< " $(vm_stat)" )
91+
8592 while kill -0 " $child_pid " 2> /dev/null; do
8693 local vm_output
8794 vm_output=$( vm_stat)
8895
8996 # Page size on Apple Silicon is 16384 bytes
90- local pages_free pages_compressed pageouts
97+ local pages_free pages_compressed pageouts pageouts_since_start
9198 pages_free=$( awk ' /Pages free/{gsub(/\./,"",$3); print $3}' <<< " $vm_output" )
9299 pages_compressed=$( awk ' /Pages stored in compressor/{gsub(/\./,"",$5); print $5}' <<< " $vm_output" )
93100 pageouts=$( awk ' /Pageouts/{gsub(/\./,"",$2); print $2}' <<< " $vm_output" )
101+ pageouts_since_start=$(( pageouts - baseline_pageouts ))
94102
95103 local free_mb=$(( pages_free * 16384 / 1024 / 1024 ))
96104 local compressed_mb=$(( pages_compressed * 16384 / 1024 / 1024 ))
@@ -101,13 +109,13 @@ start_watchdog() {
101109
102110 local timestamp
103111 timestamp=$( date ' +%Y-%m-%d %H:%M:%S' )
104- echo " $timestamp free=${free_mb} MB compressed=${compressed_mb} MB pageouts= $pageouts swapfiles=$swap_count " >> " $LOG_FILE "
112+ echo " $timestamp free=${free_mb} MB compressed=${compressed_mb} MB pageouts_since_start= $pageouts_since_start swapfiles=$swap_count " >> " $LOG_FILE "
105113
106114 local kill_reason=" "
107115 if [[ $free_mb -lt 512 && $compressed_mb -gt 8192 ]]; then
108116 kill_reason=" free=${free_mb} MB < 512MB AND compressed=${compressed_mb} MB > 8GB"
109- elif [[ $pageouts -gt 50000 ]]; then
110- kill_reason=" pageouts= $pageouts > 50000"
117+ elif [[ $pageouts_since_start -gt 50000 ]]; then
118+ kill_reason=" pageouts_since_start= $pageouts_since_start > 50000"
111119 elif [[ $swap_count -ge 30 ]]; then
112120 kill_reason=" swap_count=$swap_count >= 30 (panic happened at 36)"
113121 fi
@@ -131,28 +139,32 @@ start_watchdog() {
131139echo " === run-with-ram: closing apps ==="
132140for (( i = 0 ; i < ${# APP_NAMES[@]} ; i++ )) ; do
133141 name=" ${APP_NAMES[$i]} "
134- if is_running " $name " ; then
142+ if is_running " $name " && [[ -z " ${DRY_RUN :- } " ]] ; then
135143 WAS_RUNNING_INDICES=" $WAS_RUNNING_INDICES $i "
136144 echo " Quitting $name ..."
137145 osascript -e " tell application \" $name \" to quit" 2> /dev/null || true
138146 # Some Electron apps (Teams, Discord) ignore AppleScript quit; pkill as fallback
139- sleep 1
147+ sleep 0. 1
140148 if is_running " $name " ; then
141149 pkill -f " $name " 2> /dev/null || true
142150 fi
143151 fi
144152done
145153
146154# Give Electron apps a moment to flush state before we eat all the RAM
147- sleep 2
155+ if [[ -z " ${DRY_RUN:- } " ]]; then
156+ sleep 2
157+ fi
148158
149159# ── Run command ───────────────────────────────────────────────────────────────
150160echo " === run-with-ram: starting: $* ==="
151161echo " Watchdog log: $LOG_FILE "
152162
153163# caffeinate -i: prevent idle sleep during run
154164# nice -n 10: deprioritizes CPU so watchdogd and kernel threads stay schedulable
155- caffeinate -i nice -n 10 " $@ " &
165+ # <&0: explicitly inherit stdin so piped input reaches the child (bash redirects
166+ # background jobs' stdin away from pipes in interactive shells)
167+ caffeinate -i nice -n 10 " $@ " < & 0 &
156168CHILD_PID=$!
157169
158170start_watchdog " $CHILD_PID "
0 commit comments