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に映らなかった圧瞮メモリ46GBの犯人を10分おきに自動回収する

0
Posted at

月10䞇の倧孊生から始たっお、掛け持ちで月60䞇たで䌞ばし、䌚瀟郜合で解雇されおれロに戻り、半幎かけおClaude Codeの自埋環境を建お盎した結果、今は月商120䞇で回っおいたす。その環境が1日たるごず止たった日の話をしたす。

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

「RSSが小さいプロセスは安党」ずいう誀った前提

macOSのメモリ監芖は長幎 top -o mem で事足りおいたした。RSSの倧きい順に䞊べお、䞊から目芖で確認する。500MB超えおいるものがあれば調べる。これが倚くの人の運甚で、私もそう思っおいたした。

2026幎8月9日に、この前提が完党に厩れたした。

SNS自動投皿の党レヌンが1日分たるごず0件になっおいたのです。Claude Codeが管理するPlaywrightは動いおいるように芋える。ログを遡るず launchPersistentContext が180秒でタむムアりトし続けおいたした。原因を探しおたず top -o mem を実行したした。䞊䜍20プロセスにいかにも怪しいものは芋圓たりたせん。RSSの䞀番倧きいプロセスでも数GB台、dasd はリストのどこにも登堎しない。

top の別画面でSwap䜿甚量を確認した時に気づきたした。37GB です。物理RAMの倍近い数字です。スワップがそこたで膚らんでいれば、新しいメモリ芁求はほがディスクI/Oになりたす。Playwrightがブラりザコンテキストを生成しようずした瞬間、OSはスワップを読み曞きしながらペヌゞを確保しようずし、それが180秒を超えお萜ちた。

ではスワップを37GBたで抌し䞊げた犯人はどこにいるのか。vm.swapusage の数字は明らかに異垞なのに、RSS順の top には映らない。

CMPRS列を芋るず犯人がいた

macOSの top には、-stats オプションで取埗できる cmprscompressed memoryずいう列がありたす。アクティブに䜿われおいないメモリをOS偎がzlib圧瞮しおRAMに抌し蟌み、実際のペヌゞずのマッピングだけ保持する仕組みで、この圧瞮領域はRSSには蚈䞊されたせん。物理RAMを䜿っおいるのに、RSS監芖には映らない。

top -l 1 -n 20 -o mem -stats pid,command,mem,cmprs を実行するず、出力の䞭に dasd がありたした。

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

RSS盞圓の MEM は264MB。これがRSS降順リストに絶察に䞊䜍20に入らない理由でした。しかし CMPRS は 46GB。スワップ37GBの原因はこれです。dasdDuet Activity Scheduler、macOSのバックグラりンドタスク調停デヌモンが、起動から21時間で圧瞮メモリを46GB抱え蟌み、OSはスワップで逃げるしか遞択肢がなくなっおいたした。

「䜜業」ではなく「環境」を盎す理由

月商120䞇の実態は、自動化スクリプトずClaude Codeが䞊行しお動き続けるこずで成立しおいたす。私が寝おいる間もSNSぞの投皿が走り、ファネルが回り、翌朝には結果がたずたっおいる。この仕組みが1日止たるず売䞊にそのたたダメヌゞが入りたす。

dasd を手で再起動するだけなら5分で終わりたす。しかし問題は 次たた止たる こずです。実際この症状は「皌働21時間で発生」ずコメントに曞き残したした。翌日も翌々日も同じこずが起きるなら、毎朝 top を確認しお sudo killall dasd を叩く䜜業が発生したす。それは「䜜業」であり「環境」ではありたせん。

自埋環境の鉄則は「自分が気づかなくおも盎っおいる」です。10分おきに監芖スクリプトが走り、閟倀を超えたら再起動し、Discordに通知が来る。私が翌朝ログを芋たずき「深倜2時32分に回収枈み」ずあれば、それが環境の正しい動き方です。

さらに、dasd は単䜓の問題でもありたせんでした。iiiagentmemory、゚ヌゞェントのセッション蚘憶を管理するNode.jsプロセスが時速玄4GBで増え続ける second leaker ずしお存圚しおいたした。2぀の挏れが同時に起きおいる状態で、手動察応を前提にした仕組みでは限界がありたす。

RSSではなくCMPRSを閟倀にする意味

閟倀の蚭蚈も倧事です。dasd のRSSは264MB、閟倀をRSS基準にするず氞遠に匕っかかりたせん。CMPRSずMEM≒RSSを合算した mem 列の倀を䜿いたす。

スクリプトの 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 }
  }'
}

top が返す "47G" や "264M" を数倀MBに倉換し、閟倀ず比范したす。dasd は MEM 列が「47G」CMPRSを含む仮想的な合蚈ずしお衚瀺されるため、5120MB5GBの閟倀で正しく怜知できたす。RSSの264MBでは氞遠に玠通りしたす。

党䜓の流れ

システム構成図

┌────────────────────────────────────────────────────────────┐
│  launchd (com.lily.mem-hog-guard)                          │
│  毎時 :02 :12 :22 :32 :42 :52 に起動10分間隔           │
└──────────────────────┬─────────────────────────────────────┘
                       │
                       ▌
         top -l 1 -n 20 -o mem \
              -stats pid,command,mem,cmprs
                       │
                       â–Œ  awk で PID行以降を抜出
          ┌────────────────────────────┐
          │  pid  command   mem   cmprs │  ← MEM列 = CMPRS蟌み
          └────────────┬───────────────┘
                       │ プロセスごずにルヌプ
                       ▌
         ┌─────────────────────────────────────────────┐
         │  NEVER_TOUCH に含たれる                    │
         │  kernel_task / WindowServer / launchd / Finder │
         │       YES → スキップ                         │
         └──────────────────────┬──────────────────────┘
                                │ NO
                                ▌
         ┌──────────────────────────────────────────────┐
         │  AUTO_RESTART_NAMES に含たれるdasd      │
         │  か぀ MEM ≥ 5120MB                           │
         │  か぀ クヌルダりン未満30分でない           │
         │       YES → sudo killall / launchd が自動埩掻 │
         └──────────────────────┬───────────────────────┘
                                │ NO
                                ▌
         ┌──────────────────────────────────────────────┐
         │  LAUNCHD_RESTART_MAP に含たれるiii      │
         │  か぀ MEM ≥ 2048MB                           │
         │       YES → TERM → 2秒埅ち → KILL             │
         │             → kickstart -k                    │
         └──────────────────────┬───────────────────────┘
                                │ NO
                                ▌
         ┌──────────────────────────────────────────────┐
         │  MEM ≥ 6144MB                                │
         │       YES → Discord通知のみ觊らない        │
         └──────────────────────────────────────────────┘
                                │ 党プロセス凊理埌
                                ▌
                  findings があれば Discord通知
                  swap䜿甚量を添付

plist10分おきに起動する仕組み

<key>StartCalendarInterval</key>
<array>
  <dict><key>Minute</key><integer>2</integer></dict>
  <dict><key>Minute</key><integer>12</integer></dict>
  <dict><key>Minute</key><integer>22</integer></dict>
  <dict><key>Minute</key><integer>32</integer></dict>
  <dict><key>Minute</key><integer>42</integer></dict>
  <dict><key>Minute</key><integer>52</integer></dict>
</array>

StartInterval秒数指定のポヌリングでも600秒間隔にすれば同じ頻床になりたすが、StartCalendarInterval の分単䜍指定を遞びたした。理由はシンプルで、ログの時刻が「:02」「:12」ず䞀定になるため、埌から「䜕時に回収されたか」を目芖で確認しやすいからです。RunAtLoad は false にしおありたす。ロヌド盎埌のシステム起動盎埌に走っおもswapが安定しおいないため、最初の10分は様子芋です。

Nice は 10䜎優先床、LowPriorityIO は true に蚭定しおありたす。監芖スクリプト自䜓がCPUやI/Oを食い぀ぶしたら本末転倒です。top -l 1 は軜いですが、念のためにOS偎のスケゞュヌラに「バックグラりンド優先床でよい」ず明瀺しおいたす。

topの出力をawkでパヌスする栞心郚分

スクリプトの栞心は、top -l 1 -n 20 -o mem -stats pid,command,mem,cmprs の出力から、PID行以降だけを拟うawk凊理です。

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

while read -r pid command mem cmprs; do
  [ -n "$pid" ] || continue
  case "$pid" in ''|*[!0-9]*) continue ;; esac

  mem_mb=$(to_mb "$mem")
  cmprs_mb=$(to_mb "$cmprs")
  ...
done <<<"$(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")"

awk の /^PID/ でヘッダ行を怜知し、それ以降の行だけを凊理したす。プロセス名が「Google Chrome Helper」のようにスペヌスを含む堎合があるため、NF-2 列目ず NF-1 列目が mem ず cmprs、1列目が pid、2列目から NF-2 列目たでをすべお結合しおプロセス名ずしおいたす。最埌に gsub(/ /, "", name) でスペヌスを陀去するこずで、埌続のシェルの in_list() 関数で単玔な文字列比范ができるようにしおいたす。

二重起動防止のロックも入っおいたす。10分おきに起動するため、前回の実行が䜕らかの理由で長匕いおいた堎合sudo killall がタむムアりトするケヌスなどに同じPIDを2回killしに行くリスクがありたす。

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 はmacOS暙準のアトミックなロックファむル䜜成コマンドで、-f でロックファむルパス、-p でPIDを指定したす。すでに有効なPIDがロックを持っおいれば倱敗しお即座に exit 0 したす。trap でEXIT/HUP/INT/TERMを捕たえおロックを解攟したす。

プロセスの皮別によっお再起動方法が異なる理由

怜知したプロセスをどう凊理するかは、そのプロセスをOSが管理しおいるかどうか で分岐したす。

dasd のケヌスAUTO_RESTART

dasd はmacOSのroot所有デヌモンで、launchdが垞時管理しおいたす。sudo killall dasd を実行するず、launchdが即座に新しいプロセスを立ち䞊げたす。ナヌザヌ偎は䜕も蚭定䞍芁で、ただkillするだけで「再起動」が完了したす。

if sudo -n /usr/bin/killall "$command" 2>/dev/null; then
  log "restarted $command pid=$pid mem=$mem cmprs=$cmprs"

sudo -n は察話的パスワヌド入力を芁求しない非察話モヌドです。sudoers に NOPASSWD ルヌルがない環境では倱敗したすが、その堎合はDiscord通知に萜ずし、手動察応を促したす。自動化が匷暩的に倱敗するより、通知しおオペレヌタに刀断を委ねるほうが安党です。

iiiagentmemoryのケヌスLAUNCHD_MAP

iii はナヌザヌのlaunchdゞョブずしお動くNode.jsプロセスです。ここで最初に詊した kickstart -k だけのアプロヌチが倱敗したした。

実枬2026-08-09で刀明したのは、iii は起動時に自分自身をdetachしおフォヌクするため、launchdが芪プロセスnodeを入れ替えおも、旧プロセスがPPID=1の孀児ずしお生き残るずいう挙動です。メモリを抱えたたた旧プロセスが動き続け、新しいプロセスが増えるだけで、合蚈メモリ䜿甚量はむしろ増加したした。

正しい順序は次のずおりです。

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 が残存"

TERM → 2秒埅ち → KILL → kickstart の順です。先にTERMでグレヌスフルな終了を詊み、2秒埌に kill -0 で生存確認し、ただいれば KILLで匷制終了したす。その埌 kickstart -k でlaunchdに新プロセスを起動させたす。最埌に3秒埅っお旧PIDが残存しおいないかを確認し、残っおいればログに restart-failed を蚘録したす。

クヌルダりンの蚭蚈党䜓1ファむルから「プロセスごず」ぞの修正

最初の実装では、クヌルダりンを1぀のファむルで党䜓管理しおいたした。

# 旧実装バグあり
COOLDOWN_FILE="$STATE_DIR/.mem-hog-last"

これが2026-08-09に問題を起こしたした。16時42分に dasd を回収しおクヌルダりンを蚘録した盎埌、iii が閟倀の1.5倍3.1GBたで膚らんでいたにもかかわらず、グロヌバルクヌルダりンが有効なため30分間䞀切回収されたせんでした。1件盎したら他が30分攟眮になる蚭蚈は誀りです。

修正埌は プロセスごずにクヌルダりンファむルを持぀ 構造に倉えおいたす。

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)
  case "$last" in ''|*[!0-9]*) return 1 ;; esac
  [ $(( $(date +%s) - last )) -lt "$COOLDOWN_SEC" ]
}

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

プロセス名たたはlaunchdラベルをファむル名に䜿いたす。tr -c 'A-Za-z0-9._-' '_' でファむル名ずしお安党な文字のみに倉換したす。dasd の回収が走っおいる30分間でも、iii のクヌルダりンファむルは独立しおいるため、iii が閟倀を超えた次の10分サむクルで回収が走りたす。

NEVER_TOUCHリストも明瀺的に定矩されおいたす。

NEVER_TOUCH="kernel_task WindowServer launchd loginwindow Finder"

kernel_task はmacOSがCPUサヌマルスロットリングに䜿う特殊プロセスで、killすればシステムがフリヌズしたす。WindowServer はGUI党䜓を管理しおおり、これを萜ずすず画面が消えたす。これらは閟倀以䞊に膚らんでいおも絶察に觊りたせん。NOTIFY_ONLY6144MB超えのカテゎリで通知だけしお、刀断は人間に委ねたす。

実装の詳现

launchdコンテキストではPATHを明瀺する

スクリプトの冒頭はこうなっおいたす。

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

PATH を4ディレクトリに限定しおいるのは、launchdから起動したプロセスが継承するPATHが /usr/bin:/bin だけだからです。~/.zshrc に䜕を曞いおいおも、Homebrewが /opt/homebrew/bin に眮いたツヌルも、nvmが管理するNode.jsも、launchdからは䞀切芋えたせん。

このスクリプトが呌ぶコマンドは top・awk・sysctl・killall・launchctl・shlock のみです。すべお /usr/bin か /usr/sbin に存圚したす。「欲しいものを党郚持぀」のではなく「䜿うものだけを確実に持぀」蚭蚈です。4ディレクトリ以倖を含めない、぀たり ~/.nvm 配䞋のランタむムがPATHに乗らないこずを保蚌しおいる、ずいう芋方もできたす。

set -uo pipefail を䜿い set -e を避ける理由

set -e を䜿うず、れロ以倖の終了コヌドを返したコマンドでスクリプトが即死したす。これが正しくない堎面がありたす。

kill -TERM "$pid" 2>/dev/null || true
sleep 2
kill -0 "$pid" 2>/dev/null && kill -KILL "$pid" 2>/dev/null || true

kill -0 はプロセスの生存確認です。TERMが効いお2秒埌に察象プロセスが消えおいるのは正垞系であり、その堎合 kill -0 は終了コヌド1を返したす。set -e 環境では || true を曞いおいおも、条件匏やサブシェル展開の文脈によっお「゚ラヌを吞収できおいるか」の挙動が埮劙に倉わるため、「ここは倱敗しおよい」を確実に衚珟しにくくなりたす。

set -u未定矩倉数の参照で゚ラヌず pipefailパむプの途䞭コマンドが倱敗したら党䜓を倱敗扱いは必芁です。だから set -uo pipefail だけを遞び、蚱容される倱敗箇所は || true で明瀺的にマヌクする方針を取っおいたす。case "$pid" in ''|*[!0-9]*) continue ;; esac ずいう数倀チェックも、set -u がある以䞊は空文字列参照で萜ちるリスクぞのガヌドです。

LAUNCHD_RESTART_MAPのパヌス

耇数のゞョブを環境倉数ひず぀で蚭定できるよう、"プロセス名=launchdラベル:閟倀MB" の圢匏を採甚しおいたす。

LAUNCHD_RESTART_MAP="${MEM_HOG_LAUNCHD_MAP:-iii=com.lily.agentmemory:2048}"

パヌス郚分はこうなっおいたす。

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

${m#*=} はPOSIX準拠のパラメヌタ展開で、= より前を最短マッチで削陀したす。iii=com.lily.agentmemory:2048 から com.lily.agentmemory:2048 を切り出したす。続いお ${entry%:*} で : より埌ろを最短マッチ削陀しお com.lily.agentmemory、${entry##*:} で : より前を最長マッチ削陀しお 2048 を埗たす。

**% が「埌ろから・短い偎」、## が「前から・長い偎」**です。launchdラベルは com.foo.bar.baz のようにドット区切りで、コロンを含たない呜名芏則なので、この展開は確実に機胜したす。スペヌスで区切れば耇数゚ントリを䞊べられたすが、珟状の管理察象は iii だけなので1゚ントリです。

DRY_RUNモヌドで閟倀を実デヌタで確認する

case "${1:-}" in
  '') ;;
  --dry-run) DRY_RUN=1 ;;
  *) printf 'Usage: %s [--dry-run]\n' "$0" >&2; exit 2 ;;
esac

launchdにロヌドする前に bash ~/.claude/scripts/mem-hog-guard.sh --dry-run を実行したす。珟圚のメモリ状態に察しお䜕がトリガヌされるかを、実際のkillなしに確認できたす。

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

閟倀が合っおいるか、NEVER_TOUCHが効いおいるか、想定倖のプロセスが察象になっおいないかを本番デプロむ前に実デヌタで確認できたす。この確認なしにlaunchdぞロヌドするず、次の10分サむクルで䜕かが意図せずkillされる可胜性がありたす。閟倀を蚭定する前に必ず --dry-run で珟圚の状態を芋る、がこの皮のスクリプトの基本手順です。

通知の集積ず最終1回送信

findings=""

# ルヌプ内:
findings="${findings}✅ ${msg}"$'\n'

# ルヌプ埌:
if [ -n "$findings" ]; then
  swap=$(sysctl -n vm.swapusage | sed 's/vm.swapusage: //')
  notify "🧠 mem-hog-guard
$findings
swap: $swap"
fi

findings をルヌプ内で文字列結合しお積み䞊げ、党プロセスの走査が終わった埌に1回だけDiscordぞ投げたす。10分サむクルで耇数プロセスが同時に閟倀超えしおいおも、APIコヌルは1回で枈みたす。$'\n' はbashの $'...' 蚘法でリテラル改行を生成したす。単玔なダブルクォヌト文字列に改行を混ぜるず、展開文脈によっお゚スケヌプが厩れるこずがあるため、この蚘法を䜿っおいたす。

スワップ䜿甚量は党走査が終わった埌に sysctl -n vm.swapusage で取埗したす-n で倀のみ。sed 's/vm.swapusage: //' でキヌ郚分を陀去し、数倀文字列だけを通知本文に含めたす。


私が詰たった話

kickstart -k だけで詊したらメモリが増えた

最初にiiiぞの察凊ずしお launchctl kickstart -k "gui/$(id -u)/com.lily.agentmemory" だけを実行したした。実行盎埌に ps aux | grep iii で確認するず、2぀のPIDが䞊んでいたす。旧プロセスが残存したたた新プロセスが立ち䞊がっおおり、合蚈メモリ䜿甚量は回収前より増えおいたした。

原因はiiiの起動パタヌンです。iiiはNode.jsプロセスずしお起動したすが、初期化埌に自分自身をdetachしおフォヌクしたす。launchdが把握しおいる「芪プロセス」はフォヌク前の短呜なnodeプロセスであり、kickstartで眮き換えるのはこの芪だけです。フォヌク埌に垞駐した実䜓プロセスは PPID=1launchd盎蜄の孀児になっおおり、launchdのゞョブ管理の倖に出おしたっおいたす。

コヌドにもこの経緯がコメントで残っおいたす。

# kickstart だけでは足りない。実枬(2026-08-09): launchd は芪(node)を入れ替えるが
# iii は detach しお spawn されるため ppid=1 の孀児ずしお生き残り、
# メモリを抱えたたた新プロセスが増えるだけだった。先に本䜓を萜ずす。
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 が残存"

正しい順序は「たず本䜓をTERMで萜ずし、2秒埅っお kill -0 で生存確認し、ただいればKILLで匷制終了、それからkickstartで新プロセスを起動させる」です。kickstartを先に実行しおしたうず、「launchd芳点では新プロセスが走っおいる」のに「PPID=1の旧プロセスも走っおいる」ずいう二重起動状態になりたす。最埌の sleep 3 ず kill -0 で旧PIDの残存を確認し、残っおいれば restart-failed をログに蚘録したす。この確認ステップを入れたのも、「回収した぀もりが実は増えおいた」ずいう最初の倱敗を螏んだからです。

16:42に回収したら16:52のiiiが30分攟眮された

初期実装のクヌルダりンは1ファむルで党プロセス共通でした。

# 旧実装
COOLDOWN_FILE="$STATE_DIR/.mem-hog-last"

2026幎8月9日16時42分に dasd の回収が走り、クヌルダりンファむルにタむムスタンプが蚘録されたした。この盎埌、iii は閟倀2048MBに察しお3.1GB玄1.5倍たで膚らんでいたした。しかし16時52分のサむクルでグロヌバルクヌルダりンが有効ず刀定され、iii の回収がスキップされたす。同じこずが17時02分・17時12分ず続き、30分間 iii が攟眮されたした。

「盎したばかりだから少し様子を芋る」ずいう意図でクヌルダりンを実装したのに、「dasd を回収したせいで iii の回収が止たる」ずいう逆効果になっおいたした。ログを芋返しおこの事実に気づいたのは翌朝です。コヌドのコメントに 16:42にdasdを回収した30分間は iii が閟倀の1.5倍(3.1GB)たで膚らんでも䞀切回収されなかった ず曞き残したのは、次にコヌドを読んだずきに同じ蚭蚈に戻さないためです。

修正埌はプロセス名たたはlaunchdラベルをファむル名にしお独立したクヌルダりンを持たせおいたす。

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)
  case "$last" in ''|*[!0-9]*) return 1 ;; esac
  [ $(( $(date +%s) - last )) -lt "$COOLDOWN_SEC" ]
}

tr -c 'A-Za-z0-9._-' '_' でlaunchdラベルcom.lily.agentmemoryをそのたたファむル名に倉換したす。dasd のクヌルダりンファむルは dasd、iiiのそれは com.lily.agentmemory ずしお完党に独立しお存圚したす。2件が同時に閟倀超えしおも、それぞれのタむマヌで独立制埡されたす。

launchdから起動するずスクリプトが即死する

初期版ではPATHを明瀺しおいたせんでした。タヌミナルからの手動実行は問題なく動くのに、launchdによる定期起動が毎回倱敗する事態になりたした。log show --predicate 'process == "mem-hog-guard"' で確認するず、スクリプトが終了コヌド127で即死しおいたした。127はシェルが「コマンドが芋぀からない」ず返す終了コヌドです。

launchdが起動するプロセスが持぀PATHは /usr/bin:/bin だけです。~/.zshrc の蚭定は完党に無芖されたす。Homebrewが /opt/homebrew/bin に眮いたツヌルも、nvmが管理するNode.jsも、launchdからは芋えたせん。

問題はタヌミナルで詊しおいるずきには絶察に再珟しない点です。zsh を起動するず .zshrc が読み蟌たれおPATHが通るため、手動実行は成功したす。launchdのコンテキストに入っお初めお倱敗したす。launchdスクリプトを曞くずきに「タヌミナルで動いたlaunchdでも動く」ずいう前提は成り立ちたせん。

珟圚のスクリプトが PATH="/usr/bin:/bin:/usr/sbin:/sbin" を2行目に眮いおいるのは、䜿うコマンドtop・awk・sysctl・killall・launchctl・shlockが /usr/bin ず /usr/sbin にしか存圚しないこずを確認したうえで、その4ディレクトリを明瀺しおいるからです。この2行を入れおから、launchdによる定期起動でPATH起因の倱敗は1床も発生しおいたせん。launchdスクリプトを曞くずき最初に確認すべき事項ずしお、今では反射的に曞くようになっおいたす。

぀たずきポむント

前段で詳述した3぀の倱敗kickstart単独で孀児が残った・グロヌバルクヌルダりンで30分攟眮・launchdからPATH127死以倖にも、実装䞭に匕っかかったポむントを網矅したす。

plist蚭蚈たわり

  • RunAtLoad true にするずOS起動盎埌に走る。 起動盎埌はswap統蚈が安定しおいないため、正垞倀を「高負荷」ず誀刀定しおデヌモンを再起動するこずがある。<false/> を明瀺する。

  • StandardOutPath / StandardErrorPath を盞察パスで曞くずlaunchdが曞けない。 launchdのカレントディレクトリはルヌト/。~/.cache/... のチルダ展開も効かないため、フルパスを盎曞きする。ログが䞀切残らないのにplistロヌドは成功するので、「動いおいるのに䜕も蚘録されない」ずいう状態に気づきにくい。

  • StartInterval 600秒数ポヌリングにするずOS再起動埌に最倧10分の空癜が生たれる。 StartCalendarInterval の分単䜍指定:02/:12/:22/:32/:42/:52なら、再起動埌も次の「:XX分」が来れば確実に発火する。さらにログの時刻が「毎時:02」ず固定になるため、「䜕時に回収されたか」を翌朝ログで远うずきに目芖しやすい。

  • Nice ず LowPriorityIO を省略するずOS偎のスケゞュヌラにずっおフォアグラりンド扱いになる。 毎10分起動する監芖スクリプト自䜓がI/Oキュヌに割り蟌むのは本末転倒。Nice 10 ず LowPriorityIO true は最䜎限入れる。

シェルスクリプトたわり

  • awk で gsub(/ /, "", name) を忘れるず、スペヌス入りプロセス名の比范が氞遠に倱敗する。 Google Chrome Helper (Renderer) は while read -r pid command mem cmprs の時点でフィヌルド分解され、command が Google だけになる。awk内でスペヌスを陀去しお1文字列に結合しおから in_list() に枡す。

  • case "$pid" in ''|*[!0-9]*) continue ;; esac のガヌドを倖すず、ヘッダ残滓や空行が kill の匕数に枡る。 -n 20 の出力が環境や top のバヌゞョン差で乱れるケヌスがあり、玔粋数倀チェックは倖せない。

  • trap '/bin/rm -f "$LOCK_FILE"' EXIT HUP INT TERM を曞かないず、途䞭死亡でロックファむルが残存する。 次のサむクルでは shlock が「前のプロセスが生きおいる」ず刀定し党スキップし続ける。䜕も起きないが通知も来ないので、気づくのがさらに遅れる。

  • kill -TERM 盎埌に kickstart を実行するず新旧2プロセスが䞊走する。 TERM→2秒埅ち→kill -0 生存確認→KILL→kickstart の順序を守らないず、回収した぀もりが「叀いプロセス新しいプロセス」の二重起動になりメモリが増える。

閟倀・怜知たわり

  • -n 20 は䞊䜍20件しか取埗しない。 MEM降順で䞊ぶため、RSSが小さくCMPRSが巚倧なプロセスは䞋䜍に埋もれる。監芖察象プロセスが既知であれば -n 50 に増やすか、埌工皋で ps -eo pid,comm,rss を補完する。

  • NEVER_TOUCH を空にするず kernel_task がkillの候補になる。 kernel_task はCPUサヌマルスロットリングのためにOSが䜿う特殊プロセスで、killするずシステムがフリヌズする。kernel_task WindowServer launchd loginwindow Finder の5぀は最小セットずしお必ず定矩する。自分の環境で「絶察に萜ずしおはいけない」プロセスは事前に DRY_RUN で掗い出しお远加する。

  • sudo -n killall dasd は sudoers にNOPASSWDルヌルがない環境でサむレント倱敗する。 スクリプトはこのずき restart-failed(no-sudo) をログに曞き、Discord通知に「手動: sudo killall dasd」を添えおフォヌルバックする。デプロむ前に sudo visudo で /usr/bin/killall だけに限定しおNOPASSWDを付䞎しおおく。ALL=(ALL) NOPASSWD: ALL のように広げるず別のリスクが生たれる。

  • DRY_RUN なしに本番launchdぞロヌドしない。 bash ~/.claude/scripts/mem-hog-guard.sh --dry-run を実行するず珟圚の状態に察しお「WOULD RESTART」「WOULD NOTIFY」が出力される。意図しないプロセスが匕っかかっおいないか、閟倀が実数倀に察しお劥圓かを確認しおから launchctl load する。

  • 通知スクリプト~/.discord/notify.shの実行暩限x ビットがないず通知なしでサむレント動䜜する。 notify() 関数は [ -x "$NOTIFY_SCRIPT" ] で暩限確認し、なければ静かに return 1 する。゚ラヌは出ないが通知も来ない。ls -la ~/.discord/notify.sh で x ビットを確認する。


ベストプラクティス

1. CMPRSを含む mem 列で閟倀を蚭定する

top -stats pid,command,mem,cmprs の mem 列はCMPRS蟌みの合蚈倀です。RSSだけで監芖しおいるず、RSS 264MB・CMPRS 46GBのプロセスは氞遠に怜知できたせん。to_mb() 関数でT/G/M/Kの党単䜍を倉換し、数倀MBで閟倀比范するのが最小確実な蚭蚈です。

2. launchdスクリプトの2行目でPATHを固定する

PATH="/usr/bin:/bin:/usr/sbin:/sbin"
export PATH

この2行を入れた埌、launchdによる定期起動でのPATH起因倱敗は1床も発生しおいたせん。䜿うコマンドtop・awk・sysctl・killall・launchctl・shlockが党お /usr/bin か /usr/sbin にあるこずを先に確認し、その4ディレクトリだけを宣蚀したす。「タヌミナルで動いたlaunchdで動く」は成り立ちたせん。

3. set -uo pipefail だけ䜿い、倱敗蚱容箇所を || true で明瀺する

set -e は kill -0プロセス生存確認の終了コヌド1でスクリプトが死ぬ堎面がありたす。未定矩倉数参照ずパむプ途䞭の倱敗だけを set -uo pipefail で拟い、倱敗しおよいコマンドには || true を添えお意図を明確にしたす。

4. クヌルダりンはプロセスごずに独立したファむルで管理する

COOLDOWN_DIR="$STATE_DIR/cooldown"
file="$COOLDOWN_DIR/$(printf '%s' "$key" | tr -c 'A-Za-z0-9._-' '_')"

dasd ず iii のクヌルダりンが独立しおいるため、2件が同時に閟倀超えしおも片方が30分攟眮されるこずがありたせん。プロセス名をファむル名にする際の tr -c 'A-Za-z0-9._-' '_' でlaunchdラベルのドット区切りも安党にファむル名ぞ倉換できたす。

5. launchd管理プロセスの再起動は「TERM → 2秒埅ち → KILL → kickstart → 3秒埅ち → 残存確認」の順を守る

順序の意味は明確です。TERMでグレヌスフルな終了を詊みる。2秒埌に kill -0 で生存確認し、ただいれば KILLで匷制終了する。その埌kickstartで新プロセスを起動させる。最埌に3秒埅っお旧PIDが残存しおいないかを確認し、残っおいれば restart-failed をログに蚘録したす。kickstartを先に打぀ず新旧二重起動になりたす。

6. 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 はmacOS暙準のアトミックなロック䜜成コマンドです。10分間隔で起動するスクリプトが前回の実行ず重耇した堎合に、同じPIDを2回killしに行くのを防ぎたす。trap を忘れるず途䞭死亡でロックが残存し、以降のサむクルが党スキップされ続けたす。

7. NEVER_TOUCH リストを明瀺し、DRY_RUNで事前に匕っかかり確認する

最䜎5プロセスkernel_task WindowServer launchd loginwindow Finderを定矩し、--dry-run を実行しお「WOULD RESTART」に意図しないプロセスが珟れないかを確認しおからlaunchdにロヌドしたす。閟倀を䜎く蚭定するず普段䜿いのアプリが察象になるこずがありたす。

8. sudo -n killall はNOPASSWD蚭定枈みを確認しおからデプロむする

sudoersぞの远加は /usr/bin/killall コマンドのみに限定したす。launchdからの起動はログむンセッション倖なので、パスワヌド入力を求める通垞の sudo は動䜜したせん。NOPASSWDが蚭定されおいない堎合は通知フォヌルバックが必芁で、スクリプトはこれを暙準で実装しおいたす。デプロむ前に sudo -n /usr/bin/killall --help が成功するか確認したす゚ラヌなく即座に返れば暩限あり。

9. 通知はルヌプ埌に1回たずめお送る

findings="${findings}✅ ${msg}"$'\n'   # ルヌプ内: 積み䞊げるだけ
# ルヌプ埌:
if [ -n "$findings" ]; then notify "🧠 mem-hog-guard\n${findings}\nswap: $swap"; fi

耇数プロセスが同時に閟倀超えしおもAPIコヌルは1回で枈みたす。スワップ䜿甚量は党走査埌に sysctl -n vm.swapusage-n で倀のみで取埗しお添付したす。findings が空であれば通知自䜓を送らないため、静音な正垞皌働サむクルにはDiscordの通知が来たせん。

10. ロヌカルログずDiscord通知の䞡方を残す

ログには [2026-08-11 02:32:17] restarted dasd pid=1842 mem=47G cmprs=46G のようにタむムスタンプ・PID・メモリ量を蚘録したす。Discord通知は即時性のためにあり、ロヌカルログは远跡のためにありたす。「通知は受け取ったが実際には回収されおいなかった旧プロセスが残存した」ずいう乖離はログの restart-failed から発芋できたす。

11. to_mb() でT/G/M/Kを党単䜍カバヌする

珟行の dasd は "47G" を返しおいたすが、将来 "1.2T" や "512K" を返すプロセスに察しおも正しく閟倀比范できるよう、党4単䜍を分岐するawkを入れたす。1単䜍欠けるず比范挔算に0や空文字が入り、意図しないトリガヌたたは氞遠に怜知しないが起きたす。

12. 孀児残存確認ステップを倖さない

kickstart埌に sleep 3 しお kill -0 "$pid" で旧PIDが消えおいるかを確認したす。残っおいれば restart-failed をログに蚘録したす。「回収完了」ずいう通知が来おいおも旧プロセスず新プロセスが二重起動しおいた事実は、この確認ステップだけが教えおくれたす。2026幎8月9日の実装初日にこれを倖しおいたため、「kickstart埌にメモリが増えおいた」ずいう事実に気づくたでに数サむクルかかりたした。


たずめ

top -o mem のRSS降順監芖に構造的な盲点がありたす。macOSの圧瞮メモリCMPRSはRSSに蚈䞊されないため、RSS 264MBのたた圧瞮メモリ46GBを抱えた dasd はリストのどこにも珟れたせん。この盲点がswap 37GBずいう状態を䜜り出し、PlaywrightのlaunchPersistentContextが180秒でタむムアりトし続け、SNS自動投皿が1日分たるごずれロになりたした。月商120䞇の環境が1日止たるずそのたたダメヌゞになりたす。

mem-hog-guard.sh が解くのは3぀の問題です。CMPRSを含む mem 列で怜知するRSSでは氞遠に玠通り、プロセスの管理䞻䜓に応じお再起動方法を倉えるOSデヌモンは killall のみ、ナヌザヌlaunchdゞョブは「TERM→KILL→kickstart」の順序が必芁、クヌルダりンをプロセスごずに独立させる党䜓1ファむルにするず耇数挏れの片方が30分攟眮される。

launchdの StartCalendarInterval で10分おきに走らせ、Nice 10 ず LowPriorityIO true でバックグラりンド優先床に抑える。ロックファむルで二重起動を防ぎ、クヌルダりンで連続再起動を防ぎ、--dry-run で本番ロヌド前に実デヌタ確認する。これらを揃えるこずで「自分が気づかなくおも盎っおいる」環境が成立したす。翌朝ログを開いお「深倜2時32分に回収枈み、swap 4.1GBたで䜎䞋」ずあれば、その倜の売䞊は守られおいたす。


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


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

  • 制䜜物・蚘事は bokuwalily.com にたずめおいたす🖥
  • AIで「寝おおも回る仕組み」を䜜っお月120䞇にした話は noteの有料蚘事 に💰
  • OSS: github.com/bokuwalily 🐙
  • 最新情報・お問い合わせは X @bokuwalily ぞ🌍

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

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?