local-mac-clean · git:20260924.2784cb2 · 2026-09-24 · sha256 b9e860674861ca4d
local-mac-clean git:20260924.2784cb2A
Immutable. This exact content never changes and is served at /api/v1/blob/b9e860674861ca4d.
---
name: local-mac-clean
description: >
Чистка нагрузки на МАКЕ ([машина флота] и любой mac-узел): найти и убрать зомби-процессы,
залипшие CPU-обжоры и зависшие headless-LLM-вызовы, снять load average.
Триггеры: "/mac-clean", "/clean-mac", "почисти мак", "мак тормозит", "зомби процессы",
"убей зомби", "почисти память и проц", "kill zombies", "mac cleanup", "high load".
Обёртка над ~/.claude/scripts/proc_patrol.py. Порядок: сперва ЗАМЕР (растут ли зомби, горит ли
процесс ПРЯМО СЕЙЧАС по дельте CPU-времени), только потом kill. Стабильная пара зомби у
ActivityWatch — норма, а не утечка; залипший >12ч рендерер приложения — наоборот, снимается,
и патруль v2 делает это сам.
⛔ Только macOS — для Windows будет отдельный скилл (local-win-clean, узлы хаб/HP17).
---
# local-mac-clean — чистка зомби и нагрузки на маке
Движок: `~/.claude/scripts/proc_patrol.py` (единственный источник логики патруля, не дублировать).
Этот скилл = порядок действий вокруг движка. Собран 2026-07-31 из живой чистки
(3 зомби, залипший searchpartyd, gemini-вызов завис на 5ч48м) — каждый шаг проверен вживую.
## Шаг 0 — «ТОРМОЗИТ» ≠ LOAD AVERAGE (добавлен 04.08)
Жалоба «мак тормозит» — это три разные болезни с тремя разными виновниками. Лезть сразу в
зомби (Шаг 2) — значит чинить наугад: 04.08 зомби были ни при чём вообще.
| Что чувствует человек | Виновник | Проба |
|---|---|---|
| Курсор рвётся, окна перерисовываются рывками | swap / WindowServer под чужой нагрузкой | `sysctl vm.swapusage`, `memory_pressure \| tail -3` |
| Радуга, диск шуршит, Spotlight-поиск пустой | индексаторы (`mds_stores`, `corespotlightd`, `installd`) | дельта CPU по ним (Шаг 1) |
| Всё сразу, приложения открываются секундами | память съедена копиями MCP | Шаг 1б |
⚠️ **Load average на маке с десятками сессий не значит почти ничего** и как критерий обманчив:
04.08 он был 233 при 4% занятого CPU — процессы стояли в очереди к памяти, а не к процессору.
Честная пара цифр = `свободно CPU %` из `top` + `swap used`. Load — только для «до/после».
## Шаг 1 — ЗАМЕР (ничего не убивать вслепую)
```bash
top -l 1 -n 0 | head -12 # load avg + память
ps aux | awk '$8 ~ /^Z/ {print $2, $8}' # зомби (state Z)
python3 ~/.claude/scripts/proc_patrol.py --dry-run
memory_pressure | tail -3 # ИСТИНА про память
```
⚠️ Грабли замера (оплачены):
- **«257MB unused» в top ≠ памяти нет.** macOS держит RAM занятой кэшем намеренно.
Истина = `memory_pressure` → "System-wide memory free percentage" (70% = всё в порядке).
- **`ps %CPU` = среднее за ЖИЗНЬ процесса** [[ps-cpu-is-lifetime-average]]. Патруль может
крикнуть «ЗАЛИП 76%» про процесс, который давно успокоился. Перед kill мерить дельтой:
```bash
t1=$(ps -o time= -p <PID>); sleep 5; t2=$(ps -o time= -p <PID>); echo "$t1 -> $t2"
```
Дельта ~0 за 5с = процесс спит, НЕ убивать. Дельта ≈5с за 5с = жжёт целое ядро прямо сейчас.
- ⚠️ **ОДНОЙ пробы мало для рывкового демона.** Замерено 31.07 на `searchpartyd` в ОДНОЙ сессии:
проба №1 = 0.01с/5с (спит, обвинение патруля ложное) → через 20 минут проба №2 = **5.04с/5с**
(100% ядра, обвинение верное). Тот же процесс, противоположный вердикт. Правило: вердикт
«спит» живёт минуты, не часы — мерить ПЕРЕД killом, а не по памяти замера в начале сессии;
спорный случай = 2-3 пробы вразбежку.
## Шаг 1б — ПАМЯТЬ СЪЕДАЮТ КОПИИ MCP, а не толстый одиночка (добавлен 04.08)
Патруль ругается на «раздутый MCP >700МБ» — но главный едок обычно скромный и многочисленный.
**Вес класса = footprint × число сессий**, и считать надо именно так:
```bash
for M in computer-use telegram-mcp claude-in-chrome; do
printf "%-18s %2d шт %5d МБ\n" "$M" \
$(pgrep -fc "$M") \
$(( $(ps -Ao rss=,command= | grep "$M" | grep -v grep | awk '{s+=$1} END {print s+0}') / 1024 ))
done
```
Замер 04.08 на [машина флота]: `computer-use` = **163 МБ × 30 сессий = 4.9 ГБ**, при том что каждая
отдельная копия втрое ниже порога `MCP_BLOAT_MB` и патруль про неё молчал. Закрыл 13 спящих
сессий → 4.9 ГБ → 0.81 ГБ. Это, а не сироты и не зомби, было настоящей причиной тормозов.
Лечение класса — **один общий демон на машину вместо копии на сессию**. Для `telegram-mcp`
диета сделана (SSE-демон, порт 8765: было 48 копий × 378 МБ = 11.7 ГБ, стало 4 копии / 97 МБ)
[[telegram-mcp-shared-sse-daemon]]. Для `computer-use` диеты **пока нет** — до неё единственный
рычаг — закрывать лишние сессии.
## Шаг 2 — ЗОМБИ: сперва проверь, РАСТУТ ли они (чаще всего — нет)
⚠️ **Первым делом замер, а не kill.** Зомби бывают двух совершенно разных видов, и лечение
противоположное:
```bash
Z1=$(ps -Ao state|grep -c Z); sleep 45; Z2=$(ps -Ao state|grep -c Z); echo "$Z1 -> $Z2"
ps -Ao pid,ppid,etime,state,ucomm | awk '$4 ~ /^Z/' # возраст = возрасту родителя?
```
- **Число НЕ растёт, возраст зомби = возрасту родителя** → это не утечка, а постоянное состояние
программы (см. ActivityWatch ниже). Ничего не делать, доложить одной строкой.
- **Число растёт от замера к замеру** → вот это утечка, идти по развилке родителя ниже.
Замер 31.07 на [машина флота]: 2 → 2 за 45 секунд, в логе патруля 4 упоминания за двое суток. Вывод —
не утечка. Убивать родителя ради двух безвредных зомби = дёргать полезный сервис.
## Шаг 2б — ЕСЛИ зомби реально растут: убивается РОДИТЕЛЬ, не зомби
Зомби (STAT=Z, `<defunct>`) уже мёртв — kill по нему бесполезен. Он висит, потому что
родитель не забрал exit-код. Найти родителя:
```bash
ps -o pid,ppid,stat,command -p <ZPID>
ps -o pid,command -p <PPID> # кто родитель?
```
⛔ **ПЕРВОЕ ПРАВИЛО: цель kill — это PPID зомби, и он НИКОГДА не равен 1.**
`PID 1` = `launchd`, убийство = паника ядра. Если `PPID` зомби оказался `1`, зомби уже усыновлён
launchd и **делать нечего вообще** — launchd пожнёт его сам. Гейт перед любым killом:
```bash
PPID=$(ps -o ppid= -p <ZPID> | tr -d ' ')
[ "$PPID" = "1" ] && { echo "усыновлён launchd — не трогать"; exit 0; }
[ -z "$PPID" ] && { echo "родитель уже умер"; exit 0; }
```
Развилка по родителю (ниже `<PPID>` — всегда родитель зомби, и он ≠ 1):
- **Родитель — сам сирота** (у РОДИТЕЛЯ `ppid=1`), напр. брошенный `geckodriver` от selenium,
которого никто не ведёт → `kill <PPID>` смело: зомби переедет к launchd, тот его пожнёт.
Проверять надо ppid РОДИТЕЛЯ, не зомби: `ps -o ppid= -p $PPID`.
- **Родитель — aw-watcher-window (ActivityWatch)** → ⛔ **НЕ ТРОГАТЬ, это ложная цель.**
Замерено 31.07: убил watcher → поднял заново → **новый watcher за 2 минуты породил ровно
ту же пару зомби** (7139, 7140 при родителе 7137). ActivityWatch НЕ пожинает двух своих
детей ВСЕГДА, это его постоянное состояние, а не утечка. Цикл kill→restart = игра в
крота: счётчик обнуляется, корень стоит. Цена зомби = 2 строки в таблице PID, ноль CPU,
ноль памяти. Корень (баг ActivityWatch) лечится обновлением/отключением аппа — решает Антон.
⚠️ Плюс: `aw-qt` свой watcher после kill САМ НЕ поднимает — если всё же убил, поднимать руками:
```bash
nohup /Applications/ActivityWatch.app/Contents/MacOS/aw-watcher-window >/dev/null 2>&1 & disown
```
- **Родитель — живая рабочая сессия** (claude/терминал Антона) → НЕ убивать, зомби
безвреден (строчка в таблице PID), доложить и оставить.
## Шаг 3 — CPU-обжоры и зависшие LLM-вызовы
- **Зависший headless-LLM** (`gemini -p`, `claude -p`, `codex` > 15 минут — флотский
стандарт MAXSEC=900 [[inbox-robot-hang-guard]]): проследить цепочку
`ps -o ppid=` вверх до владельца. Владелец — живая claude-сессия, ждущая вызов
→ убить ПРАВИЛЬНО: сессия разблокируется и получит управление.
⚠️ **«Дерево» назвать поимённо, а не на глаз, и защититься от гонки PID.** Цель = сам вызов
+ его wrapper (у `gemini` это ПАРА node-процессов: `node .../gemini` и его дочерний
`node --max-old-space-size=…`), но ⛔ НЕ поднимаясь до владеющей claude-сессии — её убивать нельзя.
PID успевает освободиться и переиспользоваться между `ps` и `kill`, поэтому цель проверяется
непосредственно перед killом:
```bash
for P in <PID_wrapper> <PID_call>; do
ps -o command= -p "$P" | grep -q 'gemini' && kill "$P" || echo "PID $P уже не тот процесс — пропуск"
done
```
Возраст берётся у самого вызова (`ps -o etime= -p <PID>`), а не у сессии-владельца.
- **Root-демоны** (searchpartyd, mds и т.п.): без sudo не убить, sudo требует пароль
Антона (Tier-D). Сначала дельта-замер (Шаг 1) — чаще всего уже успокоился.
Реально залип → сказать Антону одной строкой: `sudo kill <PID>` (launchd перезапустит).
- **Claude Helper (Renderer) — развилка по ВОЗРАСТУ, не по проценту** (уточнено 31.07, было
«не трогать никогда»): свежий рендерер со 100% — это живое окно, работает, **не трогать**.
Рендерер, который горит И живёт больше 12 часов, — **залипший, его надо снять**: приложение
перерисует окно, чаты лежат на сервере. Замер 29.07: рендерер жёг ядро **4 суток 16 часов**
(91 минута чистого CPU), Load Average был 37 — снял, стало 18. **Патруль v2 делает это сам**
(`RESTARTABLE_HOG` + порог `STUCK_AGE_H=12`), руками лезть уже не нужно.
- ⛔⛔ **СИСТЕМНЫЕ ПРОЦЕССЫ, КОТОРЫЕ ПАТРУЛЬ НАЗЫВАЕТ «ЗАЛИПШИМИ», НО УБИВАТЬ НЕЛЬЗЯ.**
Порог `STUCK_AGE_H=12` бессмыслен для демонов, которые ДОЛЖНЫ жить всю сессию:
| Процесс | Патруль скажет | Что будет при kill |
|---|---|---|
| `WindowServer` | «ЗАЛИП 56ч, 82%cpu» | **выкинет из системы**, все несохранённое потеряно |
| `launchd` (pid 1) | — | паника ядра |
| `fileproviderd` | «108%cpu» | сорвёт синк GDrive/iCloud на полпути |
| `syncthing` | «обжора» | остановит доставку по флоту |
Их горение — СИМПТОМ чужой нагрузки (много окон/сессий/файлов на синке), лечится
устранением нагрузки, а не killом. Замер 31.07: `WindowServer` жёг 77% ядра — корнем были
**36 одновременных сессий Claude**, а не он сам.
- ⛔⛔ **НЕЗНАКОМОЕ ИМЯ В ТОПЕ — СПЕРВА АТРИБУЦИЯ, ПОТОМ ВЕРДИКТ** (добавлено 04.08).
Опознание занимает минуту и отвечает на главный вопрос — **чья это работа**:
```bash
ps -o pid,ppid,etime,command= -p <PID> # полная командная строка
ls -l $(which <имя> || echo /path/to/bin) # КОГДА поставлен = кто мог поставить
file /path/to/bin ; strings /path/to/bin | head -50 # что это вообще за штука
while [ "$P" != 1 ]; do ps -o ppid=,command= -p $P; P=$(ps -o ppid= -p $P|tr -d ' '); done
```
Замер 04.08: `[человек]` жёг **728% CPU и 1.05 ГБ** — выглядел как аварийный обжора. Минута
опознания: бинарь поставлен сегодня в 06:40, внутри — MCP-сервер и индексатор репозитория,
цепь ppid ведёт к **соседней живой Claude-сессии**, которая делала `[человек] init` в чужом
репозитории. Через 9 минут он завершился сам. Убить = оборвать чужую работу вслепую.
Правило: **чужая живая работа не снимается по одному лишь месту в топе** — сперва имя
владельца, потом решение; не смог атрибутировать — рапорт, а не kill.
- ⛔ **НЕ убивать**: живые сессии < 24ч; любой процесс,
чьё горение не подтверждено ДЕЛЬТОЙ. Патруль сам бьёт сирот-MCP и сессии >24ч каждые 30 мин
[[proc-patrol-robot]].
## Шаг 3б — ПОСЛЕ KILL SPOTLIGHT ЖДИ ПЕРЕИНДЕКСАЦИЮ, ЭТО НОРМА (добавлен 04.08)
Снятый индексатор возвращается — и первые минуты работает громче, чем залипший:
```
kill corespotlightd + spotlightknowledged (залипли на 128 часов)
→ через минуту: mds_stores 47%, installd 34%, новые corespotlightd/spotlightknowledged
→ через 10-20 минут: тишина
```
Два следствия, оба обязательные:
- **Вердикт «до/после» снимать не сразу**, а после затухания. Иначе честная уборка выглядит
провалом: «убил обжор, стало хуже» — на самом деле это они же, но уже за делом.
- ⛔ **Переиндексацию не убивать.** От kill-петли спасает порог `SYSTEM_STUCK_H = 2` —
свежие процессы иммунны by design. Руками лезть в них до истечения двух часов запрещено.
## Шаг 4 — ВЕРИФИКАЦИЯ (kill молчит — пересчитать выживших)
zsh не разбивает $VAR в for [[zsh-for-loop-no-word-split]] — kill мог стать no-op молча:
```bash
ps -p <PID1>,<PID2>,... | tail -n +2 # пусто = все убиты
ps -Ao state | grep -c '^Z' # сравнивать с БАЗОЙ, не с нулём (см. ниже)
uptime # load avg упал?
```
⚠️ **Критерий зомби — БАЗА, а не ноль.** Ноль недостижим и как критерий ЛОЖЕН: два зомби
ActivityWatch — постоянная норма (Шаг 2), поэтому «зомби == 0» провалит любую честную уборку.
Правильно: снять базу ДО работы, сравнить ПОСЛЕ, и отдельно проверить, что она не растёт.
```bash
# до уборки: BASE=$(ps -Ao state | grep -c '^Z')
# после: AFTER=$(ps -Ao state | grep -c '^Z') # ждём AFTER <= BASE
# не растёт: sleep 45; ps -Ao state | grep -c '^Z' # третье число == AFTER
```
Растёт от замера к замеру → это утечка, идти в Шаг 2. Стоит на месте → цель достигнута.
Вердикт Антону: до → после (load avg, число зомби, что убито и почему, что НЕ тронуто
и почему). Причина каждого kill = ✅ доказана дельтой/родословной, не 🤔 «показалось».
## Шаг 5 — «И НА ВСЕХ ДРУГИХ МАКАХ»: доставка ≠ применение (добавлен 04.08)
Приказ Антона 04.08 — прибираться **на всех маках семьи**. Скилл до сих пор кончался на своей
машине, и ровно здесь мы сели в лужу: посылку зарегистрировали, отчитались «раскатано», а на
целевом узле её никто не применил.
```bash
python3 ~/.claude/scripts/fleet_fix_audit.py --by-fix # кто отстал по каждому фиксу
grep -c . "$MACHINE_BUS_DIR/_deploy/PENDING-<узел>.jsonl" # сколько лежит непринятым
```
Замер 04.08 по [машина флота]: узел **живой** (heartbeat 14 минут назад), файлы доезжают,
но **92 посылки не применены, последний ACK — 31.07**. Робот применения встал четыре дня
назад, и патруль там не установится сам, сколько его ни отправляй.
⚠️ Две ловушки этого шага, обе оплачены в тот же день:
- **«Узел не виден в моём Syncthing» ≠ «узел офлайн».** Мак [коллега] ходит через хаб, а не
напрямую ко мне. Истина о живости — heartbeat в 03, а не мой список соединений.
- **«Посылка отправлена» ≠ «патруль работает».** Готово = `verify` вернул exit 0 на ТОМ узле
[[fleet-parity-board]]. Пока не так — в отчёте Антону узел называется поимённо как отставший.
## Что умеет патруль v2 (31.07) — чтобы не делать руками уже автоматическое
- **Меряет горение ДЕЛЬТОЙ** двух снимков CPU-времени, а не `ps %CPU`. Шаг 1 выше требует
делать дельта-замер руками — патруль теперь делает его сам на каждом тике.
- **Класс «ЗАЛИПШИЙ»**: горит >40% ядра И старше 12ч. Рендереры приложения снимает,
чужое (системные демоны, kernel_task) только рапортует с пометкой «ЗАЛИП Nч».
- **Считает одновременные сессии**: больше 20 → строка в тревогу, потому что каждая держит
свои MCP-процессы. Это, а не сироты, сейчас главная трата памяти.
- **v3 (01.08): закрывает сессии >5ч, которые СПЯТ** (приказ Антона «старше 5 часов закрываешь
сам»). Ключевой замер: спящая сессия всё равно тикает ~4-5% CPU (цикл приложения) — порог
«спит» = <10% по честной дельте; реально работающая жжёт 30-100%+ и живёт, сколько нужно.
Без замера дельты (Windows) действует только жёсткий потолок 24ч — ночной робот не погибнет.
Сессии восстановимы из истории, закрытие ничего не теряет.
- **v3: рапортует раздутые MCP** (>700МБ; telegram-mcp разъедается до ~900МБ). Только рапорт —
34 MCP-сервера висят прямо на живом Claude.app, слепой kill оборвал бы живой чат. Корень
(один сервер-владелец на машину вместо N копий, брокер-схема как wa_broker на хабе) — отдельная
сессия auto-[машина флота]-260801-tg-mcp-broker-root.
- **Имя узла берёт из реестра, а не из переменных среды.** Корень 31.07: под launchd `env` пуст,
узел метил себя `[машина флота]` вместо `[машина флота]` и заводил ВТОРОЙ state-файл, который тихо
протухал под носом у сторожа свежести. ⚠️ Правя ЛЮБУЮ деталь, которую запускает расписание,
прогоняй тест в голом окружении (`env -i HOME=$HOME ...`) — иначе фикс держится на переменной,
которой у робота нет [[fix-must-be-tested-in-bare-env]].
## v5 (04.08) — СИСТЕМНЫЙ ЯРУС: прибираемся и в процессах ОС
Приказ Антона 04.08: «нужно чтобы ты регулярно на этом маке прибирался, **в т.ч. в системных
процессах**, и на ВСЕХ ДРУГИХ МАКАХ в нашей семье». До v5 патруль системные демоны только
рапортовал строкой «ЗАЛИП Nч» — и они залипали неделями, потому что рапорт никто не читал.
**Что считается системным — ПУТЬ, не имя.** Список имён всегда неполон: в первой версии яруса
`airportd` не опознался, потому что его имени не было в перечне. Метки пути (`SYSTEM_PATH_MARKS`):
`/System/Library`, `/usr/libexec/`, `/Library/Apple/`, `/usr/sbin/`. Их не подделать.
**Три корзины, граница = ВЛАДЕЛЕЦ процесса:**
| Корзина | Что это | Что делает патруль |
|---|---|---|
| `SYSTEM_NEVER` | WindowServer, kernel_task, launchd, loginwindow, coreaudiod, Dock, Finder… | Никогда не трогает. Смерть = потеря сеанса/звука/ввода |
| `SYSTEM_SAFE_KILL` **и владелец = мы** | AppleSpell, mdworker, [человек], cloudd, searchpartyd, XprotectService, photoanalysisd… | Убивает — macOS поднимает такой сервис по первому запросу, потеря нулевая |
| всё остальное (чаще `root`) | airportd, чужие демоны | **Только рапорт + ГОТОВАЯ строка `sudo kill <pid>`** — пароль не набираю (§3.4), руки Антона |
**Порог отдельный: `SYSTEM_STUCK_H = 2` часа**, не 12 как у GUI-рендерера. Системный демон,
который жжёт ядро два часа, уже не «занят делом» — он в петле.
⚠️ **Главные грабли яруса, оплачены 04.08.** Я объявил Антону «антивирус XProtect завис и
5 суток грызёт процессор», взяв цифру `ps %CPU` = **91.4%**. Дельта-проба: `10:10.83 → 10:10.83`
за 5 секунд — процесс **спал**, диагноз был ложным на 100%. Это ровно та ловушка, о которой
предупреждает Шаг 1 этого же скилла, и я в неё наступил. **Перед любым словом о системном
процессе — дельта, а `ps %CPU` не показывать даже себе.** Ярус v5 поэтому судит ТОЛЬКО по
`cpu_delta_map`; на Windows, где дельта не реализована, ярус не срабатывает вовсе (честно
пусто, а не ноль). [[prichina-kak-claim]]
Тест яруса: 11 проверок внутри `_test_proc_patrol.py` (54/54 на 04.08) — свой демон чинится,
root не трогается и отдаётся строкой, WindowServer жив даже под своим uid, молодой демон
не трогается, allowlist сильнее яруса.
## Связанное
- Движок: `~/.claude/scripts/proc_patrol.py` (`--dry-run` = только доклад, `--verify` = exit 0 значит расписание живо)
- Раскатка изменений движка на флот: `/local-fleet-install` (scp → md5 → тест → apply → verify → реестр)
- Память: [[proc-patrol-robot]], [[ps-cpu-is-lifetime-average]], [[fix-must-be-tested-in-bare-env]], [[zsh-for-loop-no-word-split]], [[inbox-robot-hang-guard]]
- Windows-близнец: local-win-clean — НЕ построен, живёт задачей на хабе (Task Scheduler / taskkill / wmic — другая механика)