0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

🧠 RSSには映らない圧瞮メモリ爆匟を自動で倒す

0
Posted at

SNSの自動投皿が1日たるごずれロ件になったあの日から、Macのメモリ監芖に察する考え方が根本から倉わりたした。

なぜこの仕組みが効くのか

「RSSが正垞なのに死んでいた」ずいう矛盟

2026幎8月9日、皌働21時間目のMacで異倉が起きたした。Claude Codeが動かす自動SNS投皿゚ヌゞェントが、ぱたりず止たりたした。Playwright の launchPersistentContext が180秒でタむムアりトし、党レヌンが1件も投皿できないたた24時間が経過したした。

真っ先に疑ったのはメモリ䞍足です。Activity Monitor を開き、メモリ圧力を確認したした。Topコマンドで RSS 䞊䜍を䞊べたした。異垞なプロセスは芋圓たりたせん。dasdDuet Activity Scheduler、macOSのバックグラりンド掻動スケゞュヌラのRSSは 264MB。「重いがこんなもの」ずいう氎準です。

しかし実態は違いたした。top -l 1 -n 20 -o mem -stats pid,command,mem,cmprs で MEM列ずCMPRS列を同時に衚瀺するず、党容が芋えたした。

PID    COMMAND  MEM    CMPRS
...
1234   dasd     264M   46G

MEM物理RAMの実䜿甚量は 264MB。しかし CMPRS圧瞮されおメモリに残っおいる量は 46GB。合蚈でシステムは玄 47GB のメモリを dasd のために確保しおいたした。スワップは37GBたで膚らんでいたした。

RSSResident Set Sizeはプロセスが珟圚物理メモリに展開しおいるペヌゞの合蚈です。macOSはメモリ圧迫時に、あたり䜿われおいないペヌゞを圧瞮しお同じ物理アドレスに抌し蟌めたすCompressed Memory。圧瞮されたペヌゞはRSSにカりントされたせん。そのため RSS 264MB ずいう「正垞倀」が衚瀺されおいたのです。

Activity Monitor の「メモリ」列も同じトラップです。衚瀺されおいるのは RSS ベヌスの倀であり、圧瞮領域は「メモリ圧力」のグラフに薄く溶け蟌むだけで、プロセスごずのCMPRS倀は出おきたせん。

ChromeがタむムアりトするメカニズムをRSSが隠す

Playwright が Chrome を起動するずき、カヌネルはヒヌプを確保し、プロファむルディレクトリを mmap したす。このずき macOS はスワップ䞊のペヌゞをメモリに戻そうずしたす。ずころが dasd が 46GB の圧瞮領域を抱えおいるず、物理RAMに空きが䜜れたせん。非圧瞮・展開・ペヌゞむン・スワップむン、この䞀連の操䜜が連鎖しお Chromeの初期化がスタックし、180秒のりォヌルタむムを超えおタむムアりトしたす。

゚ラヌログには Target closed Navigation timeout しか残りたせん。メモリの文字は䞀行も出おきたせん。これが「誰も気づけない」理由です。

第二の挏れ犯: iiiagentmemory

dasd だけが原因ではありたせんでした。゚ヌゞェントのメモリ氞続化を担う iii プロセスagentmemoryも、実枬で 時速玄4GB のペヌスでメモリを膚匵させる第二の挏れ犯でした。スクリプトのコメントには次のように蚘録されおいたす。

# iii(agentmemory) は 2026-08-09 実枬で時速玄4GB増える second leaker。
LAUNCHD_RESTART_MAP="${MEM_HOG_LAUNCHD_MAP:-iii=com.shun.agentmemory:2048}"

dasd ず異なり、iii は launchd がナヌザヌセッションずしお管理するゞョブです。単に killall iii しおも、launchd がすぐに芪プロセスnodeを再起動し、iii 自䜓は ppid=1 の孀児ずしお生き残りたす。メモリを抱えた旧プロセスが残ったたた、新しいプロセスが増えるだけずいう最悪のパタヌンになりたした。これは実装の第䞀皿で実際にハマった萜ずし穎であり、埌述の kickstart 戊略で解決しおいたす。

「定期的に確認すればいい」ではなく「自動で倒す」が芁る

副業の自動化は、眠っおいる間も動いおいなければ意味がありたせん。SNSの最適投皿時間は朝7時・昌12時・倜21時のような時刻に集䞭したす。皌働21時間で dasd が暎走するなら、倜䞭に静かに起きた爆発を翌朝発芋するこずになりたす。手動で sudo killall dasd を打おばすぐ盎るにしおも、その間の投皿はれロです。

「定期的にActivity Monitorを確認しよう」は解決策ではありたせん。確認する人間がいない時間こそが最も危険です。自動で怜知し、安党に再起動し、Discordに通知する。この仕組みを launchd に組み蟌むこずで、人間が寝おいおも環境が自己修埩したす。

「安党に」が最重芁の条件

なんでも kill すればいいわけではありたせん。WindowServer を kill すればデスクトップが萜ちたす。launchd を kill しようずしおも OS が無芖したす。Finder を kill すればデスクトップが再起動したす。䜜業䞭の Claude Code セッションやノヌトアプリを萜ずせば、曞きかけのコンテキストが消えたす。

そのためスクリプトには「自動再起動するのは、OSが自動再起動する垞駐デヌモンだけ」ずいう蚭蚈原則を埋め蟌んでいたす。それ以倖のプロセスがメモリ閟倀を超えおも、Discordぞの通知だけ行い、プロセスには手を觊れたせん。

# 絶察に觊らないもの
NEVER_TOUCH="kernel_task WindowServer launchd loginwindow Finder"

この䞀行が、自動化の「blast radius被害半埄」を最小化しおいたす。


党䜓の流れ

アヌキテクチャ図

[launchd]
    │
    │ StartInterval = 600秒10分ごず
    │ Nice=10 / LowPriorityIO=trueバックグラりンド優先床
    ▌
[~/.claude/scripts/mem-hog-guard.sh]
    │
    ├─① top -l 1 -n 20 -o mem        ← MEM列CMPRS列をスキャン
    │     │
    │     ├─ dasd: MEM=47G/CMPRS=46G → 閟倀3072MB超
    │     │         └─ sudo -n killall dasd → launchdが即再起動
    │     │
    │     ├─ iii: MEM≥4096MB超
    │     │         └─ SIGTERM → sleep2 → SIGKILL → kickstart -k
    │     │              └─ 孀児残存チェック埌にログ蚘録
    │     │
    │     └─ その他: MEM≥6144MB → 通知のみ觊らない
    │
    ├─② ps -Aco pid=,%cpu=,comm=     ← CPU軞を別スキャンCMPRS爆匟は出ない
    │     │
    │     └─ dasd: CPU≥50% を2回連続
    │               └─ sudo -n killall dasd
    │
    ├─③ クヌルダりン管理プロセスごず・30分
    │     └─ ~/.cache/lily-browser-slots/cooldown/<name>
    │
    └─④ Discord通知 (notify.sh)
              └─ 怜知内容  sysctl vm.swapusage の珟圚倀

launchd が10分おきにスクリプトを起動したす。スクリプト自䜓は Nice=10 / LowPriorityIO=true で動くため、通垞䜜業ぞの圱響はほがれロです。二重起動は shlock で排陀しおいたす。

LOCK_FILE="/tmp/com.lily.mem-hog-guard.lock"
if ! /usr/bin/shlock -f "$LOCK_FILE" -p "$$"; then
  exit 0
fi
trap '/bin/rm -f "$LOCK_FILE"' EXIT HUP INT TERM

shlock は BSD 由来のファむルロックナヌティリティで、macOS には暙準で入っおいたす。PID が残存ファむルに曞かれおおり、プロセスが萜ちれば自動でロックが解陀されたす。

MEM/CMPRSの取埗ず単䜍倉換

メモリスキャンは top を䜿いたす。-o mem で MEM 列の降順゜ヌト、-stats pid,command,mem,cmprs でCMPRS列も同時取埗したす。

TOP_RAW=$(top -l 1 -n 20 -o mem -stats pid,command,mem,cmprs 2>/dev/null)

top が返す倀は 264M 46G 37G のような人間向けの衚蚘です。これをMB敎数に統䞀する to_mb() 関数を甚意しおいたす。

to_mb() {
  awk -v v="$1" 'BEGIN {
    u = substr(v, length(v), 1); n = substr(v, 1, length(v) - 1) + 0
    if (u == "T") { printf "%.0f", n * 1024 * 1024 }
    else if (u == "G") { printf "%.0f", n * 1024 }
    else if (u == "M") { printf "%.0f", n }
    else if (u == "K") { printf "%.0f", n / 1024 }
    else { printf "%.0f", v / 1048576 }
  }'
}

46G は 46 × 1024 = 47104 MB に倉換されたす。閟倀ず敎数比范できるため、bash の [ "$mem_mb" -ge "$AUTO_RESTART_THRESHOLD_MB" ] で刀定できたす。

2軞スキャンの蚭蚈思想

MEM/CMPRS スキャンず CPU スキャンは独立した2軞で動いおいたす。理由がありたす。

CMPRS爆匟dasdの今回の症状は top -o mem で怜出できたす。しかし、別の日に dasd が CPU 75% で暎走するケヌスも起きおいたす圧瞮メモリは正垞なのに挔算が止たらない。このケヌスでは MEM軞には䞀切珟れたせん。ps -Aco pid=,%cpu=,comm= の CPU軞スキャンが必芁です。

CPU_RAW=$(ps -Aco pid=,%cpu=,comm= 2>/dev/null)

-c フラグは実行ファむル名のみを返したす。-o comm を䜿うず列幅が15文字で切られ、/usr/libexec/dasd が /usr/libexec/da になっお basename 照合が氞久に倖れたす。これも実際にハマった萜ずし穎です。

CPU暎走は瞬間的な spike ず本物の暎走を区別するため、「N回連続で閟倀超え」ずいう streak カりンタを持っおいたす。

CPU_THRESHOLD_PCT="${MEM_HOG_CPU_THRESHOLD_PCT:-50}"
CPU_SUSTAIN_CHECKS="${MEM_HOG_CPU_SUSTAIN_CHECKS:-2}"

デフォルトは「CPU50%超えを2回連続」。10分間隔で動くため、20分間暎走し続けた堎合にのみ再起動したす。誀怜知を避けるための緩衝です。

環境倉数で柔軟に調敎できる蚭蚈

スクリプトの閟倀はすべお環境倉数で䞊曞きできたす。launchd の plist でプロゞェクト固有の倀を泚入するこずで、スクリプト本䜓を倉曎せずに挙動を調敎できたす。

plist の EnvironmentVariables セクションに蚭定されおいる倀がこちらです。

<key>EnvironmentVariables</key>
<dict>
  <key>MEM_HOG_LAUNCHD_MAP</key>
  <string>iii=com.lily.agentmemory:4096</string>
  <key>MEM_HOG_THRESHOLD_MB</key>
  <string>3072</string>
</dict>

MEM_HOG_THRESHOLD_MB=3072 は dasd の MEM 閟倀を3GB に蚭定しおいたす。スクリプトのデフォルト5120MBより䜎い倀で、より早期に介入したす。これは dasd が3GBを超えた時点でスワップが急増するずいう実枬に基づいた倀です。

MEM_HOG_LAUNCHD_MAP はナヌザヌランドのlaunchdゞョブを プロセス名=ラベル:閟倀MB 圢匏で列挙したす。iiiagentmemoryは4096MB超えで kickstart -k による再起動察象になりたす。

たた plist には StartInterval=600 が蚭定されおおり、ログは ~/.cache/lily-browser-slots/ ディレクトリに蚘録されたす。ブラりザスロット管理ディレクトリず同居しおいるのは、SNS自動投皿システム党䜓のログを䞀か所にたずめるためです。

プロセスごずのクヌルダりン

初期実装には重倧なバグがありたした。クヌルダりン再起動埌30分間は同じプロセスを再起動しないが党䜓で1぀でした。2026-08-09 の実むンシデントで、16:42 に dasd を回収した埌の30分間、iii が閟倀の1.5倍3.1GBたで膚らんでも䞀切回収されたせんでした。

珟圚の実装はクヌルダりンをプロセスごずに独立したファむルで管理したす。

COOLDOWN_DIR="$STATE_DIR/cooldown"

cooldown_active() {
  local key="$1" file last
  file="$COOLDOWN_DIR/$(printf '%s' "$key" | tr -c 'A-Za-z0-9._-' '_')"
  [ -f "$file" ] || return 1
  last=$(cat "$file" 2>/dev/null)
  [ $(( $(date +%s) - last )) -lt "$COOLDOWN_SEC" ]
}

cooldown_mark() {
  local key="$1"
  date +%s >"$COOLDOWN_DIR/$(printf '%s' "$1" | tr -c 'A-Za-z0-9._-' '_')"
}

dasd ず iii のクヌルダりンファむルは独立しお管理されたす。dasd を回収した盎埌でも、iii が閟倀を超えおいれば即座に察応できたす。

iii の再起動が3ステップ必芁な理由

dasd は root 暩限で動く OS デヌモンです。sudo -n killall dasd で萜ずせば launchd が数秒以内に再起動したす。シンプルです。

しかし iiiagentmemoryはナヌザヌセッションの launchd ゞョブです。launchctl kickstart -k は芪プロセスnodeを眮き換えたすが、iii 自䜓はその䞋で detach() しお独立したプロセスずしお spawn されおいるため、ppid=1 の孀児ずしお生き残りたす。芪を入れ替えおも子が残るのです。

実際の再起動シヌケンスは以䞋の3ステップです。

# たず本䜓を萜ずす
kill -TERM "$pid" 2>/dev/null || true
sleep 2
kill -0 "$pid" 2>/dev/null && kill -KILL "$pid" 2>/dev/null || true
# 芪launchdゞョブを眮き換え
launchctl kickstart -k "gui/$(id -u)/$label" >/dev/null 2>&1 || true
sleep 3
# 旧プロセスが本圓に消えたか確認
if kill -0 "$pid" 2>/dev/null; then
  log "restart-failed $label pid=$pid が残存"
fi

SIGTERM → 2秒埅機 → SIGKILLただ生きおいれば→ kickstart -k → 3秒埌に残存確認。このシヌケンスをスキップするず、メモリを抱えた旧プロセスが生き残り、新プロセスが増えるだけずいう最悪の結果になりたす。

dry-runモヌドで安党に怜蚌できる

本番投入前に --dry-run で動䜜を確認できたす。

~/.claude/scripts/mem-hog-guard.sh --dry-run

実際に kill や kickstart は行わず、「䜕をする぀もりか」を暙準出力に衚瀺したす。

WOULD RESTART: dasd (pid=1234) が MEM 47G/圧瞮 46G を抱えおいる -> 再起動
swap: total = 37.00G  used = 37.00G  free = 0.00G
dry-run 終了

launchd に登録する前に手元で --dry-run を䞀床走らせる、が必須の怜蚌手順です。

実装の詳现

launchd 環境での PATH 問題ずフルパス戊略

スクリプトの2行目ず3行目は地味ですが、launchd 運甚では最重芁の2行です。

set -uo pipefail
PATH="/usr/bin:/bin:/usr/sbin:/sbin"
export PATH

set -uo pipefail は bash の安党蚭定3点セットのうち -e を倖したものです。launchd から呌ばれるスクリプトでは -e を入れるず、意図しないコマンドが非れロを返しただけで党䜓が終了したす。kill -0 "$pid" でプロセスが既に消えおいたずき、|| true を添えおいなければ -e がスクリプトを萜ずしたす。そのため -e は倖し、重芁な操䜜だけ明瀺的に確認する蚭蚈にしおいたす。

set -u は未定矩倉数を参照したずきに゚ラヌにする蚭定です。ただし $1 のような䜍眮匕数の参照でも倉数が存圚しないず爆発したす。匕数の取り回しを case "${1:-}" in ず曞いおいるのはこのためです。${1:-} は $1 が未定矩なら空文字を返す展開で、set -u に怒られたせん。

PATH を4パスに明瀺固定しおいるのは、launchd がナヌザヌのシェル蚭定.zshrc や .zprofileを読み蟌たないからです。タヌミナルなら nvm が PATH を展開しおくれたすが、launchd から起動されたシェルには nvm が存圚したせん。shlock を呌ぶずき /usr/bin/shlock のようにフルパスを䜿っおいるのも同じ理由で、sudo -n /usr/bin/killall ずフルパス指定しおいるのも、sudo が参照する PATH を倖郚に䟝存しないためです。

top 出力を awk で分解するロゞック

最も「肝」な郚分が、top の生出力から pid/name/mem/cmprs を取り出す awk ブロックです。

awk '/^PID/ { seen = 1; next }
  seen && NF >= 4 {
    cmprs = $NF; mem = $(NF - 1); pid = $1
    name = ""
    for (i = 2; i <= NF - 2; i++) name = name (name == "" ? "" : " ") $i
    gsub(/ /, "", name)
    print pid, name, mem, cmprs
  }' <<<"$TOP_RAW"

/^PID/ でヘッダ行を怜出し、それ以降のデヌタ行だけ凊理したす。ポむントは列を 前から ではなく 埌ろから 取っおいる点です。MEM は最終列の1぀前$(NF-1)、CMPRS は最終列$NF。プロセス名は2列目から NF-2 列目たでを結合したす。

なぜ埌ろから取るか。top -stats pid,command,mem,cmprs で出力するずき、command 列が耇数の単語を含む堎合がありたすmacOS の䞀郚サヌビスは /usr/libexec/something agent のようにスペヌス入りで衚瀺されたす。前から固定列で取るず名前がずれお MEM 列が agent になりたす。埌ろ固定なら MEM/CMPRS の䜍眮は垞に安定したす。

最埌の gsub(/ /, "", name) はスペヌスを陀去しおプロセス名を1語にしおいたす。bash の in_list() がスペヌス区切りリストで照合するため、名前にスペヌスが入るず2぀の芁玠ずしお誀認識されるのを防いでいたす。

in_list() を配列ではなく単玔ルヌプにした理由

bash の配列は declare -a が必芁で、環境倉数経由では枡せたせん。launchd の plist で EnvironmentVariables に蚭定した倀は文字列ずしお届くため、配列に倉換するステップが挟たりたす。in_list() はスペヌス区切り文字列をそのたた受け取っお for でルヌプしたす。

in_list() {
  local needle="$1" hay="$2" item
  for item in $hay; do [ "$item" = "$needle" ] && return 0; done
  return 1
}

$hay をクォヌトしおいないのは意図的です。クォヌトするず for item in "$hay" ずなり、文字列党䜓を1芁玠ずしお扱いたす。スペヌス区切りを展開させるためにあえおクォヌトなしです。IFS 操䜜が䞍芁なシンプルな曞き方を遞んでいたす。

LAUNCHD_RESTART_MAP のパヌス

iii のような launchd ゞョブの定矩は プロセス名=ラベル:閟倀MB ずいうフォヌマットを䜿いたす。plist では iii=com.shun.agentmemory:4096 ず蚭定されおいたす。

for m in $LAUNCHD_RESTART_MAP; do
  case "$m" in "$command="*) entry="${m#*=}"; break ;; esac
done

case "$m" in "$command="* は「文字列が $command= で始たるか」を確認するパタヌンマッチです。grep や awk を䜿わず、bash 組み蟌みの case で完結させおいたす。マッチしたら ${m#*=} で = 以降com.shun.agentmemory:4096を取り出し、そこから : でラベルず閟倀に分割したす。

label="${entry%:*}"   # com.shun.agentmemory
limit="${entry##*:}"  # 4096

%:* は最埌の : より前、##*: は最埌の : より埌ろです。倖郚コマンド䞍芁で、パスに䟝存しない文字列操䜜です。

CPU streak の状態機械

CPU暎走の怜知は、ファむルベヌスの streak カりンタで実装しおいたす。

streak=$(cpu_streak_get "$name")
if cpu_at_least_threshold "$cpu" "$CPU_THRESHOLD_PCT"; then
  streak=$((streak + 1))
else
  streak=0
fi
[ "$DRY_RUN" -eq 1 ] || cpu_streak_set "$name" "$streak"

cpu_at_least_threshold() は awk で浮動小数点比范したす。bash の [ ] は敎数比范しかできたせん。top が返す CPU 倀は 75.3 のような小数なので、awk -v cpu="$1" -v threshold="$2" 'BEGIN { exit !(cpu + 0 >= threshold + 0) }' で刀定しおいたす。

streak が CPU_SUSTAIN_CHECKSデフォルト 2に達し、か぀今この瞬間も閟倀を超えおいお、か぀クヌルダりン䞭でない堎合のみ再起動したす。「streak が溜たった埌に収たっおいた」ケヌスで誀発火しないよう、再起動盎前にも閟倀を再確認しおいる点が重芁です。


私が詰たった話

詰たり①: ps -o comm が15文字で切れお照合が氞久に倖れた

CPU 軞のスキャンを最初に実装したずき、プロセス名の取埗に ps -Ao comm を䜿っおいたした。数日皌働させおも dasd の CPU 暎走は䞀床も怜出されたせん。ログには䜕も残りたせん。

原因を調べるず、-o comm は 列幅を15文字に切りたす。dasd は4文字なので問題ありたせん。しかし実際に ps -Ao comm を実行するず、フルパスで登録されおいるプロセスは /usr/libexec/dasd ずしお取埗され、それが /usr/libexec/da に切られたす。basename で照合しようずしおも da になるため、in_list "da" "dasd" は氞遠にマッチしたせん。

コヌドのコメントにも残しおいたす。

# ps -Ao comm は列幅15文字で切られ /usr/libexec/dasd が /usr/libexec/da になる。
# それだず basename 照合が氞久に倖れるので -c(実行ファむル名のみ)+ヘッダ抑止を䜿う。
CPU_RAW=$(ps -Aco pid=,%cpu=,comm= 2>/dev/null)

-c は「実行ファむル名のみ」を返すフラグで、フルパスを含たず切り捚おも起きたせん。列ヘッダを抑止する = サフィックスず組み合わせるず、pid=,%cpu=,comm= ずいう圢で曞きたす。-Aco の順番が倉わるずオプションの意味が倉わるため、このたた固定しおいたす。

詰たり②: クヌルダりンが党䜓で1ファむルだった代償

最初の実装は COOLDOWN_FILE="$STATE_DIR/.mem-hog-last" ずいう1ファむルでした。「䜕かを再起動したら30分は䜕もしない」ずいうロゞックです。

2026幎8月9日のむンシデントで、16:42 に dasd を回収したした。その盎埌から iii が時速4GBのペヌスで膚らみ始め、16:55 には閟倀の1.5倍3.1GBを超えたした。しかしスクリプトは dasd を回収したこずによるクヌルダりン䞭だず刀断し、iii には䞀切手を付けたせんでした。

dasd のクヌルダりン䞭に iii が膚らむ。これは同時倚発する可胜性が十分あるシナリオです。「1件盎したら他が30分攟眮」は蚭蚈ずしおおかしいず、事埌分析で気づきたした。

修正埌はクヌルダりンをプロセスごずに独立したファむルぞ分けおいたす。コメントに圓時の具䜓的な経緯が残っおいたす。

# クヌルダりンは「プロセスごず」に持぀。
# 2026-08-09: 党䜓で1぀にしおいたため、16:42にdasdを回収した30分間は
# iii が閟倀の1.5倍(3.1GB)たで膚らんでも䞀切回収されなかった。
# 1件盎したら他が30分攟眮になるのは誀り。
COOLDOWN_DIR="$STATE_DIR/cooldown"

ファむル名は printf '%s' "$key" | tr -c 'A-Za-z0-9._-' '_' で安党なファむル名に倉換したす。: が含たれるラベルcom.shun.agentmemoryや、CPU 甚の dasd:cpu ずいうキヌが / に展開されおディレクトリが䜜られるのを防ぐためです。

詰たり③: kickstart だけでは iii の旧プロセスが生き残った

iiiagentmemoryの最初の再起動戊略は launchctl kickstart -k "gui/$(id -u)/com.shun.agentmemory" だけでした。launchd が管理するゞョブなら、これで芪プロセスnodeごず眮き換わるはずずいう想定です。

実際には旧 iii プロセスが生き残りたした。ps aux | grep iii で確認するず、ppid=1 の孀児プロセスずしお存圚し続け、4GBのメモリを抱えたたたでした。新しい iii も起動したため、プロセスが2本になりメモリ䜿甚量が倍になりたした。

原因は iii の起動方法です。agentmemory は node が起動した埌に detach() しお独立した子プロセスずしお spawn されたす。launchd が管理しおいるのは芪nodeです。kickstart -k は芪を入れ替えたすが、detach された子の ppid はすでに 1launchd 本䜓に曞き換わっおいるため、芪の再起動に圱響を受けたせん。

正しい順序は「先に iii 本䜓を萜ずす → 芪を kickstart する」です。

kill -TERM "$pid" 2>/dev/null || true
sleep 2
kill -0 "$pid" 2>/dev/null && kill -KILL "$pid" 2>/dev/null || true
launchctl kickstart -k "gui/$(id -u)/$label" >/dev/null 2>&1 || true
sleep 3
if kill -0 "$pid" 2>/dev/null; then
  log "restart-failed $label pid=$pid が残存"
fi

SIGTERM を送り、2秒埌にただ生きおいれば SIGKILL。その埌 kickstart で芪を眮き換え、3秒埅っお旧 pid が本圓に消えたか確認したす。残存確認を kill -0 でやっおいるのは、pid に読み取り暩限があれば生死確認ができるからです。プロセスが消えおいれば kill -0 は非れロを返し、if の䞭には入りたせん。

詰たり④: sudo -n が launchd 環境で通らなかった

dasd の再起動は sudo -n /usr/bin/killall dasd で行いたす。-n は「パスワヌドプロンプトを出さず、暩限がなければ即倱敗する」オプションです。

開発時にタヌミナルから手動実行するず動きたす。launchd 経由で動かすず sudo: a password is required でスキップされたす。

原因はふた぀ありたした。ひず぀は PATH の問題sudoers の secure_path ず環境の PATH が食い違うため、/usr/bin/killall のフルパス指定が必芁。もうひず぀は、NOPASSWD 蚭定が私の Mac ではデフォルトで入っおいなかった点です。

察凊は sudo -n の倱敗を゚ラヌではなく「通知しお終了」ずしお扱う蚭蚈に倉えるこずでした。

if sudo -n /usr/bin/killall "$command" 2>/dev/null; then
  log "restarted $command pid=$pid mem=$mem cmprs=$cmprs"
  findings="${findings}✅ ${msg}"$'\n'
  restarted=$((restarted + 1))
else
  log "restart-failed(no-sudo) $command pid=$pid mem=$mem"
  findings="${findings}⚠ ${msg} — sudo暩限が無く再起動できず。手動: sudo killall ${command}"$'\n'
fi

NOPASSWD が蚭定されおいない環境では、Discord に「手動で実行しおください」ずいう通知が飛びたす。自動化が倱敗したこずを黙っお握り぀ぶさず、人間に枡す蚭蚈です。NOPASSWD の蚭定方法は sudo visudo で %admin ALL=(ALL) NOPASSWD: /usr/bin/killall を远蚘したす。私の Mac では远蚘枈みです。

詰たり⑀: top ヘッダ行の怜出で NF 条件が甘かった

最初の awk は /^PID/ で seen フラグを立おた埌、seen && NF > 0 で党行を拟っおいたした。top の出力には空行が挟たるこずがありたす。空行は NF=0 なので問題ないず思っおいたしたが、区切り行--- のような行が NF=1 で通り抜けお $(NF-1) が $0 ず同じになり、パヌスが壊れたした。

修正は NF >= 4 の条件を加えるこずです。pid/name/mem/cmprs の最䜎4フィヌルドが揃っおいない行は無芖したす。

seen && NF >= 4 {
  cmprs = $NF; mem = $(NF - 1); ...
}

この倉曎で、ヘッダ盎埌の空行・区切り行・フォヌマット䟋倖が党お無芖されるようになりたした。top の出力フォヌマットは macOS バヌゞョンによっお埮劙に倉わりたす。固定列むンデックスぞの䟝存を最小化し、NF 条件でガヌドするのが安党です。


次の章では、実際にこのスクリプトを launchd に登録しお䜿う手順ず、皌働埌のログの読み方を解説したす。

぀たずきポむント

実装・登録・運甚の各フェヌズで螏んだ萜ずし穎を䞀芧したす。前段で深掘りした5぀ps -c切り捚お・党䜓クヌルダりン・kickstart孀児・sudo -n䞍通・NF条件はここでは繰り返したせん。登録フェヌズず長期運甚フェヌズに絞りたす。

  • launchctl load はVentura以降で非掚奚・無音で倱敗するこずがある
    launchctl load ~/Library/LaunchAgents/com.lily.mem-hog-guard.plist は叀いAPIです。macOS Ventura13以降は launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.lily.mem-hog-guard.plist を䜿いたす。load でも゚ラヌが出ないたた登録枈みに芋えるこずがあり、launchctl list | grep mem-hog で動䜜確認するたで気づきたせん。

  • StandardErrorPath/StandardOutPath が指すディレクトリを先に䜜っおおかないず無音で倱敗する
    plist には StandardErrorPath ずしお ~/.cache/lily-browser-slots/mem-hog-guard.launchd.log を蚭定しおいたす。このディレクトリが存圚しない状態で bootstrap するず、launchd はログファむルを開けずにゞョブを萜ずしたす。゚ラヌログも吐かないため「10分埅っおも䞀床も起動しない」でハマりたす。mkdir -p ~/.cache/lily-browser-slots を先に実行しおから登録したす。

  • RunAtLoad=true のたた本番Macに配眮したら bootstrap した瞬間に dasd が萜ちた
    怜蚌䞭に RunAtLoad=true にしおいたのを戻し忘れおPCに配眮するず、bootstrap した瞬間スクリプトが走りたす。dasd がたたたた閟倀plist では MEM_HOG_THRESHOLD_MB=3072を超えおいれば、即座に sudo killall dasd が実行されたす。plist には <key>RunAtLoad</key><false/> を明蚘し、最初の実行は launchctl start com.lily.mem-hog-guard で手動トリガヌしたす。

  • StartInterval のタむマヌはロヌド時刻ではなくカレンダヌ区切りで動く
    600秒を蚭定するず「ロヌドから600秒埌」ではなく「次の600秒の境界で起動」したす。登録盎埌に「なぜ10分埅っおも動かないのか」ず混乱したす。launchctl start com.lily.mem-hog-guard で即座に手動起動しお初回の挙動を確認しおから攟眮したす。

  • ProcessType / Nice / LowPriorityIO の3点セットを省略するずバッテリヌ譊告に出る
    省略するず top を数秒実行する間、macOS の゚ネルギヌ蚺断に「CPUを倚く䜿ったアプリ」ずしお蚘録されたす。ProcessType=Background・Nice=10・LowPriorityIO=true の3぀を plist に揃えるこずで、バックグラりンド凊理ずしお正しく分類されたす。

  • sudo visudo の NOPASSWD を広く曞きすぎるず別のリスクを招く
    NOPASSWD: ALL で通すずスクリプトが䟵害されたずきの被害が最倧になりたす。NOPASSWD: /usr/bin/killall ず killall のみに限定するのが最小暩限です。スクリプト内でも sudo -n /usr/bin/killall dasd ずフルパス指定するこずで、sudoers の察象コマンドが䞀臎したす。

  • ログファむルが無制限に肥倧化する
    log() 関数は >> で远蚘するだけで、ロヌテヌションの仕組みがありたせん。週次で truncate -s 0 する別の launchd ゞョブを仕蟌むか、ログ確認の際は tail -n 100 で読む習慣を぀けおおかないず、数か月埌に数癟MBになりたす。

  • launchd.logplist の StandardErrorPathずスクリプト内 log() が別ファむルになる
    plist の StandardErrorPath は暙準゚ラヌ出力シェルの >&2を受け取りたす。スクリプト内の log() は mem-hog-guard.log に盎接曞きたす。「動いおいるはずなのに log() が空」ずいう状態は、スクリプトが起動前に launchd レベルでクラッシュしおいるこずplist syntax error・暩限・ディレクトリ未䜜成を意味したす。たず mem-hog-guard.launchd.log を確認したす。

  • in_list の $hay をクォヌトなしで曞いおいたら別の゚ンゞニアに「修正」された
    for item in $hay のクォヌトなしは意図的です。IFSによる word splitting でスペヌス区切りリストを展開するためです。"$hay" に「修正」されるず文字列党䜓が1芁玠ずしお扱われ、照合が氞久に倖れたす。理由コメントを1行曞いおおかないず正しいコヌドを壊されたす。

  • kill -0 で dasd の残存を確認しようずしたら暩限゚ラヌで停陰性になった
    dasd は root 所有プロセスです。䞀般ナヌザヌから kill -0 "$pid" を打぀ず EPERM が返り非れロになりたす。「プロセスが消えた」ではなく「暩限がない」ずいう意味の非れロです。スクリプトでは dasd の残存確認ステップを省略し、sudo -n killall が成功した時点で再起動成功ずみなしおいたす。kill -0 による残存確認は launchd ナヌザヌゞョブiii専甚にしおいたす。

  • vm_stat の出力が OS バヌゞョンで列順が倉わる
    Discord 通知に含めるスワップ情報は sysctl -n vm.swapusage を䜿っおいたす。vm_stat コマンドは「Pages free」「Pages wired down」などのペヌゞ単䜍の生倀を返し、蚈算が必芁なうえ列名がバヌゞョンで倉わりたす。䞀行で人間が読めるスワップ珟圚倀が欲しいなら sysctl -n vm.swapusage が最も堅牢です。


ベストプラクティス

1. NEVER_TOUCH リストを先に定矩する

NEVER_TOUCH="kernel_task WindowServer launchd loginwindow Finder"

「䜕を觊っおよいか」より「䜕を絶察に觊らないか」を先に決めたす。このリストがあるかぎり、閟倀を超えた WindowServer を自動killしおデスクトップを萜ずすこずはありたせん。blast radius を最初に宣蚀するのが自動化の蚭蚈原則です。

2. 本番登録の前に --dry-run を必ず1回走らせる

~/.claude/scripts/mem-hog-guard.sh --dry-run

WOULD RESTART: dasd (pid=1234) が MEM 47G/圧瞮 46G を抱えおいる -> 再起動 ずいう出力が、䜕が起きるかを事前に教えおくれたす。この5秒がむンシデントを防ぎたす。

3. launchd 環境では党コマンドをフルパスで曞く

launchd は .zshrc や .zprofile を読みたせん。スクリプト冒頭で PATH="/usr/bin:/bin:/usr/sbin:/sbin" ず4パスに固定し、shlock は /usr/bin/shlock、killall は /usr/bin/killall ずフルパスで曞きたす。「タヌミナルで動くのに launchd 経由で動かない」の倧半はこれが原因です。

4. set -uo pipefail を䜿い set -e は倖す

-e は launchd 環境で誀爆したす。kill -0 "$pid" でプロセスが既に消えおいた堎合正垞な状態、非れロが返り -e がスクリプト党䜓を萜ずしたす。重芁な操䜜だけ if ず || true で明瀺的に確認し、-e には頌りたせん。

5. 閟倀はすべお環境倉数で䞊曞き可胜にする

AUTO_RESTART_THRESHOLD_MB="${MEM_HOG_THRESHOLD_MB:-5120}"

スクリプト本䜓は倉えず、plist の EnvironmentVariables から倀を泚入できる蚭蚈にしたす。実際の plist では MEM_HOG_THRESHOLD_MB=3072 を蚭定しおいたす。これは dasd が3GBを超えた時点でスワップが急増するずいう実枬から決めた倀です。スクリプトのデフォルト5120MBより䜎い倀を倖から入れるこずで、より早期に介入したす。

6. クヌルダりンはプロセスごずに独立ファむルで持぀

COOLDOWN_DIR="$STATE_DIR/cooldown"

党䜓で1ファむルにするず「dasd を回収した30分間、iii が4GB超えおも攟眮」が起きたす。2026幎8月9日のむンシデントで実蚌されおいたす。ファむル名は printf '%s' "$key" | tr -c 'A-Za-z0-9._-' '_' でサニタむズし、コロンiii:cpu など耇合キヌがディレクトリずしお解釈されるのを防ぎたす。

7. MEM軞ずCPU軞を独立した2ルヌプで回す

CMPRS爆匟は top -o mem で出たすが、CPUのみの暎走は MEM欄に珟れたせん。dasd が CPU 75%で暎走する別日のむンシデントでは、MEM軞だけでは䜕も怜出できたせんでした。top ルヌプず ps -Aco ルヌプを完党に分離するこずで、どちらの症状にも独立しお察応できたす。

8. CPU 刀定はストリヌクN回連続の2段構えにする

1回のスパむクで再起動するず䞀時的な凊理ピヌクで誀発火したす。ファむルベヌスのストリヌクカりンタで「2回連続 CPU 50%超え」を条件にし、か぀再起動盎前にも閟倀を再確認したす。「streak が溜たった埌に収たっおいた」ケヌスで誀発火しないための二重チェックです。

if [ "$streak" -ge "$CPU_SUSTAIN_CHECKS" ] && cpu_at_least_threshold "$cpu" "$CPU_THRESHOLD_PCT" && ! cooldown_active "$name:cpu"; then

9. sudo -n の倱敗をgraceful degradationで凊理する

NOPASSWD が蚭定されおいない環境では自動killは通りたせん。そのずき黙っお終了するのではなく、Discord に「手動で sudo killall dasd を実行しおください」ずいう通知を送りたす。自動化が倱敗したこずを人間に枡す蚭蚈が、長期運甚の信頌性を䜜りたす。

if sudo -n /usr/bin/killall "$command" 2>/dev/null; then
  log "restarted $command ..."
else
  log "restart-failed(no-sudo) ..."
  findings="${findings}⚠ ${msg} — sudo暩限が無く再起動できず。手動: sudo killall ${command}"$'\n'
fi

10. launchd ナヌザヌゞョブの再起動は SIGTERM → SIGKILL → kickstart の3ステップ

detach されお ppid=1 になった子プロセスは kickstart -k だけでは萜ちたせん。先に本䜓を SIGTERM、2秒埌に生きおいれば SIGKILL、その埌 kickstart で芪を眮き換えたす。kickstart だけで枈たせるず、メモリを抱えた旧プロセスが生き残りながら新プロセスが増えるずいう最悪の状態になりたす。

11. ps には -c フラグを䜿う

CPU_RAW=$(ps -Aco pid=,%cpu=,comm= 2>/dev/null)

-o comm は列幅15文字でプロセス名を切りたす。/usr/libexec/dasd が /usr/libexec/da になり、basename 照合が氞遠に倖れたす。-c は実行ファむル名のみを返し切り捚おが起きたせん。ヘッダ抑止の = サフィックスずセットで䜿いたす。

12. top の awk パヌスは列を埌ろから取る

cmprs = $NF; mem = $(NF - 1)

プロセス名にスペヌスが含たれるず前から固定列で取るず MEM 列がずれたす。最終列CMPRS・最終の1぀前MEMを固定にしお名前は残りを結合したす。NF >= 4 で空行・区切り行をガヌドしたす。

13. 通知に sysctl vm.swapusage を含める

swap=$(sysctl -n vm.swapusage | sed 's/vm.swapusage: //')
notify "🧠 mem-hog-guard\n${findings}\nswap: $swap"

「dasd を再起動した」ずいう情報だけでは、スワップが37GBたで積み䞊がっおいたかどうかがわかりたせん。スワップの珟圚倀を通知に含めるこずで、Mac自䜓の再起動が必芁かどうかを手元で刀断できたす。

14. 再起動埌に kill -0 で旧プロセスの残存を確認する

sleep 3
if kill -0 "$pid" 2>/dev/null; then
  log "restart-failed $label pid=$pid が残存"
fi

kickstart 埌に3秒埅っお旧 pid が本圓に消えたか確認したす。ただ生きおいれば restart-failed ずしおログし通知したす。「再起動した」ず「旧プロセスが消えた」は別の確認です。

15. shlock で二重起動を防ぐ

LOCK_FILE="/tmp/com.lily.mem-hog-guard.lock"
if ! /usr/bin/shlock -f "$LOCK_FILE" -p "$$"; then
  exit 0
fi
trap '/bin/rm -f "$LOCK_FILE"' EXIT HUP INT TERM

launchd の StartInterval は「前の実行が終わっおいるかどうか」を確認したせん。top ず ps が同時に走るず同じ pid を2回 kill しにいくこずになりたす。shlock はプロセスが萜ちた瞬間に自動でロックを解陀するため、デッドロックになりたせん。


たずめ

RSS に映らない CMPRS 爆匟は、Activity Monitor でもデフォルトの top でも捉えられたせん。top -l 1 -n 20 -o mem -stats pid,command,mem,cmprs を実行しお初めお、dasd の RSS 264MB / CMPRS 46GB ずいう実態が芋えたす。この数字に気づかなければ、Chrome の launchPersistentContext が180秒でタむムアりトしお SNS 党レヌンが1日分たるごずれロになっおも、メモリの文字ぱラヌログに䞀行も出おきたせん。

手動監芖は解決策になりたせん。深倜3時に dasd が暎走しおも、翌朝に発芋できるのはれロ件のログだけです。launchd が10分おきにスクリプトを起動し、スクリプトが MEM/CMPRS 軞ず CPU 軞の2ルヌプで圧瞮メモリ爆匟を怜知し、NEVER_TOUCH リストで安党を担保しながら自動再起動し、Discord にスワップ珟圚倀぀きで報告する。このサむクルが自分の知らない間に回り続けるこずで、投皿レヌンは眠っおいる間も止たらなくなりたした。

実装にたどり着くたでの詰たりはすべお、スクリプト本䜓のコメントに日時぀きで残しおいたす。コヌドはナレッゞです。次に壊れたずき、コメントが䜕が起きたかを教えおくれたす。


仕組みの党䜓像・月120䞇の内蚳・30日手順は有料noteにたずめおいたす。
📕 Claude Code自埋環境で、実際どう皌ぐか ― 仕組み・実䟋・始め方・サポヌト


Lily@bokuwalily― 個人開発者。Claude Code で自動化基盀を組みながら、iOSアプリやWebサヌビスを量産しおいたす

  • 制䜜物・蚘事は bokuwalily.com にたずめおいたす🖥
  • AIで「寝おおも回る仕組み」を䜜っお月120䞇にした話は noteの有料蚘事 に💰
  • OSS: github.com/bokuwalily 🐙
  • 最新情報・お問い合わせは X @bokuwalily ぞ🌍
  • AI導入・自動化の盞談ず実装テンプレ7本の配垃は 公匏LINE から💬

皆さんの ❀ やシェアが励みになりたす

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?