WEB board

всеканоны и докиворкеры↗ iOS↗ Легаси
WEB-478 · Дефект · — · web

Диспетчер волн A1: волны оставляют dev-серверы и podman-контейнеры после DONE (утёкший next dev 40 ч × 92% CPU) — нужны kill дерева при DONE и периодический свип

Закрыт P2 ведёт: Fable
Суть
## Суть одной фразой
Волны оставляют после DONE dev-серверы, podman-контейнеры, временные БД (*-pgdata) и кеши сборки — нужны kill дерева при DONE, периодический свип и безопасная уборка temp-БД/кешей, которая не может задеть живой стенд.

## Где мы сейчас (13.09.2026)
Диспетчер v3 посажен в l99 (32c2c885, ACCA2TRIO GO): при DONE убивает дерево процессов сессии и podman-контейнеры по label=wave (видно в dispatcher.log по волнам 3277/3280/3281/3282/3286/3287). Interim orphan-sweep (таймер 30 мин) добивает процессы с cwd «(deleted)» старше 2 ч. Дисковый дворник стоит на A1 (09.09, порог 14 ГБ) и A2 (12.09 23:03, порог 16 ГБ) — снимает неактивные деревья волн старше 60 мин. НЕ убирается никем: temp-БД (*-pgdata) и кеши сборки (b32-05 4.4 ГБ, .next) — намеренно выведены из автоматики (дворник не должен трогать стенды БД). Правило безопасной уборки специфицировано волной 3700 (JANITOR-SPEC.md): удалять только при владельце-волне мёртвой (маркер _DONE + нет в tmux-сокете диспетчера) + каталог не в манифесте стендов + нет живого держателя; возраст — вторичен (отсчёт от _DONE, не от mtime).

## Хронология
Interim sweep 02.09 → v3 (1364/1400→GO 1420) установлен 03.09 → P0 гонка setsid/SIGHUP + swap-death (MAXWAVES=3) → круги 3–5 (91564efc/3e754e4c/7dd74a03) → l99 посадка 32c2c885 (ACCA2TRIO GO) → дисковый дворник A1 (09.09) / A2 (12.09 23:03) → спецификация уборки temp-БД/кешей (волна 3700, 13.09).

## Карта документов и кода
infra/wave-dispatcher (wave-dispatcher.sh, wave-runner.sh); /usr/local/bin/wave-disk-janitor.sh; wave-orphan-sweep; waves/3700-wave-leftovers-janitor-out/JANITOR-SPEC.md; отчёты ACCWEB478DISPATCHER2, ACCA2TRIO.

## Остаток
- Реализовать правило уборки temp-БД/кешей в wave-disk-janitor.sh по JANITOR-SPEC.md (владелец + живость из tmux-сокета + манифест стендов) — волна.
- Записать в паспорт уборку temp-БД + дворники A1/A2 — координатор.

## Критерий закрытия
Правило уборки temp-БД/кешей реализовано в дворнике (удаляет только брошенное: владелец-волна мёртв, каталог не в манифесте стендов, нет живого держателя) и зафиксировано в паспорте; повторных простоев от утечек нет.
---
_история_ — прежний текст тела (до 13.09.2026) координатор сохраняет ниже этой строки без изменений (append-only по WEB-449).

---

## история (тело до 13.09.2026)

## Суть одной фразой
Диспетчер волн на A1 не убивал dev-серверы и podman-контейнеры после DONE — утечки процессов/диска.

## Где мы сейчас (13.09.2026)
Диспетчер v3 посажен в l99 (32c2c885, ACCA2TRIO GO): убивает сессии/деревья процессов и podman-контейнеры при DONE. Дисковый дворник стоит на A1 (09.09, порог 14 ГБ) и A2 (12.09 23:03, порог 16 ГБ). Осталось: уборка temp-БД (*-pgdata) после DONE покрыта исключением (ручная — стенды БД не трогаем) + записать в паспорт при следующей дельте.

## Хронология
Interim sweep 02.09 → v3 (1364/1400→GO 1420) установлен 03.09 → P0 гонка setsid/SIGHUP + swap-death (MAXWAVES=3) → круги 3–5 (91564efc/3e754e4c/7dd74a03) → l99 посадка 32c2c885 (ACCA2TRIO GO) → дисковый дворник A1 (09.09) / A2 (12.09 23:03).

## Карта документов и кода
infra/wave-dispatcher (wave-dispatcher.sh, wave-runner.sh); /usr/local/bin/wave-disk-janitor.sh; wave-orphan-sweep; отчёты ACCWEB478DISPATCHER2, ACCA2TRIO.

## Остаток
- Уборка temp-БД (*-pgdata) — покрыта исключением (ручная, стенды БД не трогаем) — координатор.
- Записать в паспорт при следующей дельте — координатор.

## Критерий закрытия
Паспорт обновлён (уборка temp-БД + дисковый дворник A1/A2 зафиксированы), повторных простоев от утечек нет.
---
_история_ — прежний текст тела (до 13.09.2026) координатор сохраняет ниже этой строки без изменений (append-only по WEB-449).

---

## история (тело до 13.09.2026)

Факт (A1, 02.09 15:15): весь день A1 показывал load 6–12 при 3–5 волнах; корень — утёкший `next-server` в dev-режиме (PORT=43341, NODE_ENV=development) от волны 1046-arcadegen (31.08): cwd `/home/ubuntu/waves/wt-1046-arc (deleted)`, cgroup `wave-dispatcher.service`, 40 часов на 92% CPU одного из 4 vCPU. Рядом — podman-остатки старых приёмок (rootlessport/conmon для wt-996-acc-base и wt-1027-e5d, контейнеры acce5rt2-996-20260831, e5realtime4-redis-1027). После kill: load 12 → 4.5 → 2.5. Следствия: транзиентный 503 на /api/ready/paid (14:56), ложные выводы о «перегрузке волнами», заморозки приёмок, задержки посадок.

Причина класса: диспетчер волн (`/home/ubuntu/wave-dispatcher.sh`) при DONE переносит бриф в done/ и закрывает tmux-окно, но не убивает дочерние процессы волны (next dev, тестовые серверы, podman-контейнеры), а удаление worktree координатором оставляет их с cwd (deleted).

Требования (волна, потом посадка при безопасном рестарте диспетчера — память dispatcher-restart-kills-tmux-waves): (1) при DONE/убийстве волны — `kill -TERM` всему дереву процессов pane_pid (pstree), через 10 с `-KILL`; `podman ps --filter label=wave=<name>` → rm -f (волнам предписать помечать контейнеры label); (2) периодический свип (таймер раз в 30 мин): процессы с cwd под /home/ubuntu/waves/* «(deleted)» старше 2 ч → kill, `podman system prune -f`; журнал в queue/dispatcher.log; (3) в шаблон брифа: «не оставлять серверы/контейнеры после DONE; `next dev` запрещён вне явного разрешения»; (4) метрика: число процессов в cgroup wave-dispatcher.service и их CPU в OPS-STATUS/тике.

Свидетельства: OPS-STATUS-LIVE.md 15:15 A1; память a1-wave-queue-dispatcher (рецепт-свип).

## ЭВОЛЮЦИЯ 04.09.2026 (карта для нулевого агента)
**Якорь прода:** живая линия **l103**, коммит **d91ebefe740ae83484c1de16710f2e6a68610432**, флип **13:51Z 04.09**. Раньше в этот же день: l101 `d825eb0d`, l102 `1b081c5a`. Ветка линии в репозитории-базе `/home/ubuntu/nc` на A1: `l103-line` (проверено `git branch -a --contains d91ebefe...` → `l103-line`).
**Доска:** живая доска — `web-board.sqlite` + HTTP API `http://127.0.0.1:8787/api/web/*` на M1 (`ssh poolpooly@192.168.1.74`). НЕ `board.sqlite` (это iOS/легаси, таблицы `issues`/`ios_issues`, 2225 строк, ключи другие).

### НОВЫЙ ФАКТ 04.09 — ПОРОГ ДИСПЕТЧЕРА СТЕРЕЖЁТ НЕ ТО
**Порог свободного места у диспетчера волн управляет ПАМЯТЬЮ, а не диском. Гейта по диску нет вообще.**
Именно поэтому боксы **тихо умирали при заполненном диске**: диспетчер продолжал раздавать волны, потому что смотрел на память и видел запас.

### ПОЧЕМУ ЭТО ЛОЖИТСЯ ИМЕННО В ЭТОТ ТИКЕТ
Тикет уже описывает утечки волн (dev-серверы, podman-контейнеры после DONE). Утечки едят диск, а гейта по диску нет — два дефекта складываются в тихую смерть бокса.

### СЛЕДУЮЩЕЕ ДЕЙСТВИЕ
1. Добавить в диспетчер **отдельный гейт по свободному диску** (не переиспользовать порог памяти): при падении ниже порога — не стартовать новые волны и писать в `queue/dispatcher.log`.
2. Развести имена/логи двух порогов, чтобы следующий читатель не принял один за другой.
3. Свип утечек (kill процессов с `cwd` под `/home/ubuntu/waves/*` со статусом `(deleted)` старше 2 ч, `podman system prune -f`) оставить как есть — он лечит симптом, гейт лечит причину.

### ЛОВУШКИ
- Не считать существующий порог «уже дисковым» — он не дисковый; это ровно та ошибка, из-за которой боксы падали молча.
- Волны root не имеют: правка диспетчера как хостовой службы — работа координатора.

## ДЛЯ НУЛЕВОГО АГЕНТА (обновлено 2026-09-05 UTC)
- **Суть одной строкой:** Диспетчер волн A1: волны оставляют dev-серверы и podman-контейнеры после DONE (утёкший next dev 40 ч × 92% CPU) — нужны kill дерева при DONE и периодический свип
- **Текущее состояние:** статус: in_progress; Факт (A1, 02.09 15:15): весь день A1 показывал load 6–12 при 3–5 волнах; корень — утёкший next-server в dev-режиме (PORT=43341, NODEENV=development) от волны 1046-arcadegen (31.08): cwd /home/ubuntu/waves/wt-1046-arc (deleted), cgroup wave-dispatcher.service, 40 часов на 92% CPU одного из 4 vCPU.… | Причина класса: диспетчер волн (/home/ubuntu/wave-dispatcher.sh) при DONE переносит бриф в done/ и закрывает tmux-окно, но не убивает дочерние процессы волны (next dev, тестовые серверы, podman-контейнеры), а удаление worktree координатором оставляет их с cwd (deleted). | Требования (волна, потом посадка при безопасном рестарте диспетчера — память dispatcher-restart-kills-tmux-waves): (1) при DONE/убийстве волны — kill -TERM всему дереву процессов panepid (pstree), через 10 с -KILL; podman ps --filter label=wave=<name → rm -f (волнам предписать помечать контейнеры… | Свидетельства: OPS-STATUS-LIVE.md 15:15 A1; память a1-wave-queue-dispatcher (рецепт-свип). | Якорь прода: живая линия l103, коммит d91ebefe740ae83484c1de16710f2e6a68610432, флип 13:51Z 04.09. Раньше в этот же день: l101 d825eb0d, l102 1b081c5a. Ветка линии в репозитории-базе /home/ubuntu/nc на A1: l103-line (проверено git branch -a --contains d91ebefe... → l103-line). | Именно поэтому боксы тихо умирали при заполненном диске: диспетчер продолжал раздавать волны, потому что смотрел на память и видел запас. | Тикет уже описывает утечки волн (dev-серверы, podman-контейнеры после DONE). Утечки едят диск, а гейта по диску нет — два дефекта складываются в тихую смерть бокса. | 3. Свип утечек (kill процессов с cwd под /home/ubuntu/waves/ со статусом (deleted) старше 2 ч, podman system prune -f) оставить как есть — он лечит симптом, гейт лечит причину.
- **Кто работал:**
- 2026-09-02T15:28:19.601Z — Fable — [A1 15:30 Фабл] Интерим без рестарта диспетчера: /home/ubuntu/wave-orphan-sweep.sh + systemd wave-orphan-sweep.timer (каждые 30 мин, OnBootSec 10 мин): kill процессов с cwd под /home/ubuntu/waves/ «(deleted)» старше 2 ч 
- 2026-09-02T21:15:36.857Z — Fable — [A1 21:20 Фабл] Волна 1364-web478dispatcher поставлена (A1, Luna high): single-instance lock, подсчёт бегущих по процессам, уборка dev-серверов/podman после DONE, детекция исчерпанного капа Codex → PAUSE+ALERT и возврат 
- 2026-09-02T21:52:43.279Z — Fable — [A1 22:30 Фабл] Волна 1364-web478dispatcher сдана: коммит 01d319cdd3 (infra/waves/dispatcher: v3 — flock single-instance, подсчёт по процессам, уборка утёкших процессов/podman после DONE, детекция капа → PAUSE+ALERT и во
- 2026-09-02T22:36:25.069Z — Fable — [A1 01:10 Фабл] Приёмка 1380-accweb478dispatcher = NO-GO (отчёт A1 /home/ubuntu/waves/ACCWEB478DISPATCHER-REPORT.md): B1 flock не освобождается сразу после kill -9 (дескриптор наследуется дочерними процессами); B2 DISPAT
- 2026-09-02T23:44:13.788Z — Fable — [A1 05:15 Фабл] Волна 1400-web478dispatcher2 сдана: коммит 71cdff31f7 (flock без наследования, fallback после cap, маркер как в v2). Отчёт: A1 /home/ubuntu/waves/WEB478DISPATCHER2-REPORT.md. Приёмка 1420 (A1, Sol high, в
- 2026-09-03T00:36:48.600Z — Fable — [A1 09:05 Фабл] Приёмка 1420-accweb478dispatcher2 = GO (Sol): flock без наследования, fallback на claude после cap, маркер как в v2, уборка утёкших процессов, cap→PAUSE+ALERT. Устанавливаю v3 на A1 в окне PAUSE (cp + dro
- 2026-09-03T00:37:51.687Z — Fable — [A1 09:15 Фабл] Диспетчер v3 УСТАНОВЛЕН на A1 (окно PAUSE 2 мин, бегущие волны не затронуты): /home/ubuntu/wave-dispatcher.sh + wave-runner.sh + unit + drop-in v3-fallback.conf; лог «dispatcher v3 start (max=5 minfree=8G
- 2026-09-03T01:56:54.209Z — Fable — [A1 11:50 Фабл] P0 диспетчер v3 в бою: launch через exec setsid wave-runner в tmux-панели — гонка (setsid форкает, родитель выходит, tmux шлёт SIGHUP до setsid() ребёнка) → все 5 волн после 00:38 UTC мертвы на старте (pi
- 2026-09-03T02:31:08.519Z — Fable — [A1 Фабл] P0 №2 за утро: при MAXWAVES=5 две волны одновременно гоняли по полному tsc --noEmit (×3) + pnpm install → A1 (4 OCPU/24 ГБ) ушёл в swap-death, load 71, ПРОД НЕ ОТВЕЧАЛ ~15 мин (paid 000 снаружи). Восстановлен k
- 2026-09-03T16:16:10.013Z — coordinator — 03.09 21:45 IST. Круг 3 роздан: волна 1578-dispatcherv3r3 идёт на A2 (старт 16:14:14Z). В бриф вошла исходная задача круга (лимит тяжёлых процессов: волны оставляют dev-серверы и podman-контейнеры) плюс три ловушки, вскр
- 2026-09-03T17:53:19.177Z — coordinator — Круг 3 сдан: коммит 91564efc «ops: harden dispatcher v3 heavy-process cleanup». Приёмка = волна 1588-accmebridgedisp на A2. В задании отдельно требуется закрыть три сегодняшние ловушки: потеря доступа координатора к очер
- 2026-09-03T19:23:21.213Z — coordinator — Приёмка 1588 круга 3 = NO-GO. ACL после рестарта закрыт в снапшоте, но найдена цепочка: wavenamefromlabel() срезает числовой префикс (478-active-dev → active-dev), launchwave() пишет name=active-dev, activewavenames() чи
- 2026-09-03T22:36:19.783Z — coordinator — Круг 4 сдан: коммит 3e754e4c «fix dispatcher wave label identity and completion markers». Приёмка = волна 1611-accdispatcherv3r4 на A2, в изолированной песочнице (боевой wave-dispatcher-a2 не трогается). Проверяется: нор
- 2026-09-03T22:59:33.204Z — coordinator — Приёмка 1611 = NO-GO по 3 пунктам из 5 (лимит тяжёлых процессов GO, счёт бегущих волн GO). Блокеры: (1) нормализация метки — running marker с label=478-active-dev и name=active-dev после нормализации не совпадает сам с с
- 2026-09-04T00:09:42.314Z — coordinator — Круг 5 сдан: коммит 7dd74a03. Приёмка = волна 1627 на A2. Проверяется в том числе то, из-за чего сегодня терялись результаты: маркер DONE должен писаться по абсолютному пути, а не внутрь worktree; нормализация метки не д
- 2026-09-04T00:57:19.442Z — coordinator — Приёмка 1627 = GO для 7dd74a03. Кандидат 3/3 PASS (v3r3 + v3r4 + v3r5) против базы 2/3, где v3r5 даёт 4 ожидаемых падения; отдельная проверка 1/1 на кандидате и базе. Приёмщик подтвердил обработку клона вне wt- через rea
- 2026-09-04T04:03:18.368Z — coordinator — ПОСАДКА l99 ВЫПОЛНЕНА (волна 1638-l99merge2 → коммит 32c2c885551b2a6f235fa80fd4f1010ee82a4ade, приёмка ACCA2TRIO=GO по WEB-478). Артефакт me2-standalone-linux-arm64-32c2c885-20260904T025504Z.tar.gz, sha256 440c0ce1de0cf8
- 2026-09-04T15:08:17.251Z — coordinator — 04.09 координатор: в body добавлен блок «ЭВОЛЮЦИЯ 04.09.2026» — состояние, причина с доказательством, что село, что осталось, ловушки.
- 2026-09-05T14:36:22.860Z — coordinator — [05.09 14:36Z координатор] Повтор 05.09: волна 1989 (bigdocpersist2, A1) умерла ~13:35Z, а её node --import tsx tests/e2e/web495/live-dev-server.mts (cwd /home/wave/waves/bigdocpersist2-work) остался сиротой под systemd 
- **Ветки/бандлы/отчёты:** /home/ubuntu/waves/wt-1046-arc, /home/ubuntu/waves/WEB478DISPATCHER-REPORT.md., /home/ubuntu/waves/ACCWEB478DISPATCHER-REPORT.md, /home/ubuntu/waves/WEB478DISPATCHER2-REPORT.md.; sha: 20260831, d91ebefe740ae83484c1de16710f2e6a68610432, d825eb0d, 1b081c5a, d91ebefe, 01d319cdd3, 01d319cd, 71cdff31f7
- **KNOWN ISSUES / ТРАБЛШУТИНГ:**
- Причина класса: диспетчер волн (/home/ubuntu/wave-dispatcher.sh) при DONE переносит бриф в done/ и закрывает tmux-окно, но не убивает дочерние процессы волны (next dev, тестовые серверы, podman-контейнеры), а удаление worktree координатором оставляет их с cwd (deleted).
- Якорь прода: живая линия l103, коммит d91ebefe740ae83484c1de16710f2e6a68610432, флип 13:51Z 04.09. Раньше в этот же день: l101 d825eb0d, l102 1b081c5a. Ветка линии в репозитории-базе /home/ubuntu/nc на A1: l103-line (проверено git branch -a --contains d91ebefe... → l103-line).
- Тикет уже описывает утечки волн (dev-серверы, podman-контейнеры после DONE). Утечки едят диск, а гейта по диску нет — два дефекта складываются в тихую смерть бокса.
- 3. Свип утечек (kill процессов с cwd под /home/ubuntu/waves/ со статусом (deleted) старше 2 ч, podman system prune -f) оставить как есть — он лечит симптом, гейт лечит причину.
- - Не считать существующий порог «уже дисковым» — он не дисковый; это ровно та ошибка, из-за которой боксы падали молча.
- [A1 15:30 Фабл] Интерим без рестарта диспетчера: /home/ubuntu/wave-orphan-sweep.sh + systemd wave-orphan-sweep.timer (каждые 30 мин, OnBootSec 10 мин): kill процессов с cwd под /home/ubuntu/waves/ «(deleted)» старше 2 ч (TERM → KILL), podman rm exited-контейнеров, журнал в queue/dispatcher.log («…
- [A1 01:10 Фабл] Приёмка 1380-accweb478dispatcher = NO-GO (отчёт A1 /home/ubuntu/waves/ACCWEB478DISPATCHER-REPORT.md): B1 flock не освобождается сразу после kill -9 (дескриптор наследуется дочерними процессами); B2 DISPATCHFALLBACK=claude-opus после cap не применяется — бриф перезапускается route=…
- [A1 11:50 Фабл] P0 диспетчер v3 в бою: launch через exec setsid wave-runner в tmux-панели — гонка (setsid форкает, родитель выходит, tmux шлёт SIGHUP до setsid() ребёнка) → все 5 волн после 00:38 UTC мертвы на старте (pid=0 в .run, логи 0 байт), слоты заняты 1.5 ч, очередь стояла. Воспроизведено …
- **Эволюция:**
- 2026-09-02 → [A1 15:30 Фабл] Интерим без рестарта диспетчера: /home/ubuntu/wave-orphan-sweep.sh + systemd wave-orphan-sweep.timer (каждые 30 мин, OnBootSec 10 мин): kill процессов с cwd под /home/ubuntu/waves/ «(deleted)» старше 2 ч (TERM → KILL), podman rm exited-контейне
- 2026-09-02 → [A1 21:20 Фабл] Волна 1364-web478dispatcher поставлена (A1, Luna high): single-instance lock, подсчёт бегущих по процессам, уборка dev-серверов/podman после DONE, детекция исчерпанного капа Codex → PAUSE+ALERT и возврат брифа в очередь (факт 02.09 20:44: все в
- 2026-09-02 → [A1 22:30 Фабл] Волна 1364-web478dispatcher сдана: коммит 01d319cdd3 (infra/waves/dispatcher: v3 — flock single-instance, подсчёт по процессам, уборка утёкших процессов/podman после DONE, детекция капа → PAUSE+ALERT и возврат брифа, маршруты claude/spark, тест
- 2026-09-02 → [A1 01:10 Фабл] Приёмка 1380-accweb478dispatcher = NO-GO (отчёт A1 /home/ubuntu/waves/ACCWEB478DISPATCHER-REPORT.md): B1 flock не освобождается сразу после kill -9 (дескриптор наследуется дочерними процессами); B2 DISPATCHFALLBACK=claude-opus после cap не прим
- 2026-09-02 → [A1 05:15 Фабл] Волна 1400-web478dispatcher2 сдана: коммит 71cdff31f7 (flock без наследования, fallback после cap, маркер как в v2). Отчёт: A1 /home/ubuntu/waves/WEB478DISPATCHER2-REPORT.md. Приёмка 1420 (A1, Sol high, в очереди) → установка вместе с wave-user
- 2026-09-03 → [A1 09:05 Фабл] Приёмка 1420-accweb478dispatcher2 = GO (Sol): flock без наследования, fallback на claude после cap, маркер как в v2, уборка утёкших процессов, cap→PAUSE+ALERT. Устанавливаю v3 на A1 в окне PAUSE (cp + drop-in + systemctl restart wave-dispatcher
- 2026-09-03 → [A1 09:15 Фабл] Диспетчер v3 УСТАНОВЛЕН на A1 (окно PAUSE 2 мин, бегущие волны не затронуты): /home/ubuntu/wave-dispatcher.sh + wave-runner.sh + unit + drop-in v3-fallback.conf; лог «dispatcher v3 start (max=5 minfree=8G tmux-socket=/run/wave-dispatcher/tmux.s
- 2026-09-03 → [A1 11:50 Фабл] P0 диспетчер v3 в бою: launch через exec setsid wave-runner в tmux-панели — гонка (setsid форкает, родитель выходит, tmux шлёт SIGHUP до setsid() ребёнка) → все 5 волн после 00:38 UTC мертвы на старте (pid=0 в .run, логи 0 байт), слоты заняты 1
- 2026-09-03 → [A1 Фабл] P0 №2 за утро: при MAXWAVES=5 две волны одновременно гоняли по полному tsc --noEmit (×3) + pnpm install → A1 (4 OCPU/24 ГБ) ушёл в swap-death, load 71, ПРОД НЕ ОТВЕЧАЛ ~15 мин (paid 000 снаружи). Восстановлен kill -9 тяжёлых процессов. Установлено MA
- 2026-09-03 → 03.09 21:45 IST. Круг 3 роздан: волна 1578-dispatcherv3r3 идёт на A2 (старт 16:14:14Z). В бриф вошла исходная задача круга (лимит тяжёлых процессов: волны оставляют dev-серверы и podman-контейнеры) плюс три ловушки, вскрытые сегодня на живом A1: (1) рестарт wa
- Факт (A1, 02.09 15:15): весь день A1 показывал load 6–12 при 3–5 волнах; корень — утёкший next-server в dev-режиме (PORT=43341, NODEENV=development) от волны 1046-arcadegen (31.08): cwd /home/ubuntu/waves/wt-1046-arc (deleted), cgroup wave-dispatcher.service, 40 часов на 92% CPU одного из 4 vCPU.…
- ЭВОЛЮЦИЯ 04.09.2026 (карта для нулевого агента)
- Якорь прода: живая линия l103, коммит d91ebefe740ae83484c1de16710f2e6a68610432, флип 13:51Z 04.09. Раньше в этот же день: l101 d825eb0d, l102 1b081c5a. Ветка линии в репозитории-базе /home/ubuntu/nc на A1: l103-line (проверено git branch -a --contains d91ebefe... → l103-line).
- НОВЫЙ ФАКТ 04.09 — ПОРОГ ДИСПЕТЧЕРА СТЕРЕЖЁТ НЕ ТО
- [A1 21:20 Фабл] Волна 1364-web478dispatcher поставлена (A1, Luna high): single-instance lock, подсчёт бегущих по процессам, уборка dev-серверов/podman после DONE, детекция исчерпанного капа Codex → PAUSE+ALERT и возврат брифа в очередь (факт 02.09 20:44: все волны умерли молча), маршруты claude/s…
- [A1 Фабл] P0 №2 за утро: при MAXWAVES=5 две волны одновременно гоняли по полному tsc --noEmit (×3) + pnpm install → A1 (4 OCPU/24 ГБ) ушёл в swap-death, load 71, ПРОД НЕ ОТВЕЧАЛ ~15 мин (paid 000 снаружи). Восстановлен kill -9 тяжёлых процессов. Установлено MAXWAVES=3 (решение owner-а 26.08). Тре…
- **Следующий шаг:** Причина класса: диспетчер волн (/home/ubuntu/wave-dispatcher.sh) при DONE переносит бриф в done/ и закрывает tmux-окно, но не убивает дочерние процессы волны (next dev, тестовые серверы, podman-контейнеры), а удаление worktree координатором оставляет их с cwd (deleted). | СЛЕДУЮЩЕЕ ДЕЙСТВИЕ
Лента
2026-09-02T15:28:19.601Z · Fable
[A1 15:30 Фабл] Интерим без рестарта диспетчера: /home/ubuntu/wave-orphan-sweep.sh + systemd wave-orphan-sweep.timer (каждые 30 мин, OnBootSec 10 мин): kill процессов с cwd под /home/ubuntu/waves/* «(deleted)» старше 2 ч (TERM → KILL), podman rm exited-контейнеров, журнал в queue/dispatcher.log («sweep: …»). Первый прогон 15:24 — чисто. Полный фикс (kill дерева при DONE, label контейнеров) — волна при безопасном рестарте диспетчера.
2026-09-02T21:15:36.857Z · Fable
[A1 21:20 Фабл] Волна 1364-web478dispatcher поставлена (A1, Luna high): single-instance lock, подсчёт бегущих по процессам, уборка dev-серверов/podman после DONE, детекция исчерпанного капа Codex → PAUSE+ALERT и возврат брифа в очередь (факт 02.09 20:44: все волны умерли молча), маршруты claude/spark.
2026-09-02T21:52:43.279Z · Fable
[A1 22:30 Фабл] Волна 1364-web478dispatcher сдана: коммит 01d319cdd3 (infra/waves/dispatcher: v3 — flock single-instance, подсчёт по процессам, уборка утёкших процессов/podman после DONE, детекция капа → PAUSE+ALERT и возврат брифа, маршруты claude/spark, тесты на шимах). Отчёт: A1 /home/ubuntu/waves/WEB478DISPATCHER-REPORT.md. Дальше: приёмка 1380-accweb478dispatcher (A1, Sol high, в очереди) → установка в окне PAUSE (вместе с 1379 wave-user, если оба GO).
2026-09-02T22:36:25.069Z · Fable
[A1 01:10 Фабл] Приёмка 1380-accweb478dispatcher = NO-GO (отчёт A1 /home/ubuntu/waves/ACCWEB478DISPATCHER-REPORT.md): B1 flock не освобождается сразу после kill -9 (дескриптор наследуется дочерними процессами); B2 DISPATCH_FALLBACK=claude-opus после cap не применяется — бриф перезапускается route=codex; B3 нормализация DONE-маркера (дефис→«_») несовместима с живым v2 (дефисы удаляются). Положительно: single-instance сообщение, cap→PAUSE+ALERT+возврат брифа, уборка. Круг 2 = волна 1400-web478dispatcher2 (A1, Luna high, от 01d319cd).
2026-09-02T23:44:13.788Z · Fable
[A1 05:15 Фабл] Волна 1400-web478dispatcher2 сдана: коммит 71cdff31f7 (flock без наследования, fallback после cap, маркер как в v2). Отчёт: A1 /home/ubuntu/waves/WEB478DISPATCHER2-REPORT.md. Приёмка 1420 (A1, Sol high, в очереди) → установка вместе с wave-user (1399 → приёмка) в окне PAUSE.
2026-09-03T00:36:48.600Z · Fable
[A1 09:05 Фабл] Приёмка 1420-accweb478dispatcher2 = GO (Sol): flock без наследования, fallback на claude после cap, маркер как в v2, уборка утёкших процессов, cap→PAUSE+ALERT. Устанавливаю v3 на A1 в окне PAUSE (cp + drop-in + systemctl restart wave-dispatcher), откат = .bak-codex-20260902.
2026-09-03T00:37:51.687Z · Fable
[A1 09:15 Фабл] Диспетчер v3 УСТАНОВЛЕН на A1 (окно PAUSE 2 мин, бегущие волны не затронуты): /home/ubuntu/wave-dispatcher.sh + wave-runner.sh + unit + drop-in v3-fallback.conf; лог «dispatcher v3 start (max=5 minfree=8G tmux-socket=/run/wave-dispatcher/tmux.sock)», lock /run/wave-dispatcher.lock, DISPATCH_FALLBACK=claude-opus, MAXWAVES=5. Откат: /home/ubuntu/wave-dispatcher.sh.bak-codex-20260902 + unit .bak + отключить drop-in (рецепт в отчёте 1420). Уборка утёкших процессов и детекция капа теперь автоматические.
2026-09-03T01:56:54.209Z · Fable
[A1 11:50 Фабл] P0 диспетчер v3 в бою: launch через `exec setsid wave-runner` в tmux-панели — гонка (setsid форкает, родитель выходит, tmux шлёт SIGHUP до setsid() ребёнка) → все 5 волн после 00:38 UTC мертвы на старте (pid=0 в .run, логи 0 байт), слоты заняты 1.5 ч, очередь стояла. Воспроизведено пробой (bogus route: с setsid — лог пуст; с env / setsid -w — пишет). Фикс: drop-in Environment=SETSID_BIN=env (без правки принятого кода), диспетчер перезапущен, 6 волн (1407, 1424–1428, 1433) возвращены в очередь. Для круга 3 диспетчера: (1) launch без setsid либо setsid -w; (2) pid=0 после new-session = start-failed, не running; (3) вахта: лог 0 байт через N мин после started → пометить dead и вернуть в очередь.
2026-09-03T02:31:08.519Z · Fable
[A1 Фабл] P0 №2 за утро: при MAXWAVES=5 две волны одновременно гоняли по полному tsc --noEmit (×3) + pnpm install → A1 (4 OCPU/24 ГБ) ушёл в swap-death, load 71, ПРОД НЕ ОТВЕЧАЛ ~15 мин (paid 000 снаружи). Восстановлен kill -9 тяжёлых процессов. Установлено MAXWAVES=3 (решение owner-а 26.08). Требование к кругу 3 диспетчера: лимит тяжёлых процессов (tsc/next build/pnpm install) — не более одного одновременно на A1 через flock-обёртку в runner (PATH-shim для tsc/next/pnpm), плюс memory-guard: при MemAvailable < 3 ГБ — SIGSTOP самой молодой волны, при < 1.5 ГБ — kill.
2026-09-03T16:16:10.013Z · coordinator
03.09 21:45 IST. Круг 3 роздан: волна 1578-dispatcherv3r3 идёт на A2 (старт 16:14:14Z). В бриф вошла исходная задача круга (лимит тяжёлых процессов: волны оставляют dev-серверы и podman-контейнеры) плюс три ловушки, вскрытые сегодня на живом A1: (1) рестарт wave-dispatcher возвращает /home/wave и /home/wave/waves к 0750 wave:wave, координатор теряет доступ к очереди через симлинк и scp брифа падает с «dest open … Failure» — сегодня чинилось руками через setfacl -m u:ubuntu:x /home/wave и setfacl -m u:ubuntu:rx /home/wave/waves; требование — доступ обязан переживать рестарт; (2) MAXWAVES реально читается из /etc/wave-dispatcher.env, а drop-in с именем 9x-*.conf не действует, потому что systemd сортирует drop-in по алфавиту и v3-fallback.conf идёт последним и перетирает значение — требование: один источник истины и печать в лог, откуда взято значение; (3) волны пишут маркеры _DONE внутрь worktree, уборщик дерево удаляет (cleaned=1) и маркер гибнет — сегодня из-за этого коммиты доставались из клонов бандлами; требование: runner отказывает такой волне либо переносит маркер в /home/ubuntu/waves/ до уборки.
2026-09-03T17:53:19.177Z · coordinator
Круг 3 сдан: коммит 91564efc «ops: harden dispatcher v3 heavy-process cleanup». Приёмка = волна 1588-accmebridgedisp на A2. В задании отдельно требуется закрыть три сегодняшние ловушки: потеря доступа координатора к очереди после рестарта диспетчера (нужен setfacl на /home/wave и /home/wave/waves), MAXWAVES фактически из /etc/wave-dispatcher.env (drop-in 9x-*.conf проигрывает v3-fallback.conf по алфавиту) и маркеры _DONE, которые волны пишут внутрь удаляемого worktree.
2026-09-03T19:23:21.213Z · coordinator
Приёмка 1588 круга 3 = NO-GO. ACL после рестарта закрыт в снапшоте, но найдена цепочка: wave_name_from_label() срезает числовой префикс (478-active-dev → active-dev), launch_wave() пишет name=active-dev, active_wave_names() читает это поле, а orphan_worktree_for_pid() из cwd получает 478-active-dev — строки не совпадают никогда, защита от уборки живого worktree не работает. Круг 4 = волна 1597-dispatcherv3r4 на A2.
2026-09-03T22:36:19.783Z · coordinator
Круг 4 сдан: коммит 3e754e4c «fix dispatcher wave label identity and completion markers». Приёмка = волна 1611-accdispatcherv3r4 на A2, в изолированной песочнице (боевой wave-dispatcher-a2 не трогается). Проверяется: нормализация метки (478-active-dev против active-dev — из-за этого защита живого worktree не работала), маркеры завершения вне worktree (сегодня многократно терялись при уборке), лимит тяжёлых процессов, дубликат брифа не перезапускает лейбл, и что счёт волн не включает сессию самого диспетчера (на M4 он посчитал себя третьей волной и перестал брать работу).
2026-09-03T22:59:33.204Z · coordinator
Приёмка 1611 = NO-GO по 3 пунктам из 5 (лимит тяжёлых процессов GO, счёт бегущих волн GO). Блокеры: (1) нормализация метки — running marker с label=478-active-dev и name=active-dev после нормализации не совпадает сам с собой, защита от уборки живого рабочего дерева не срабатывает; (2) completion markers — реальный launch_wave меняет согласованный путь маркера, живое подтверждение сегодня: DOCNERBUDGET4_DONE записан только в /home/wave/waves/, в /home/ubuntu/waves/ его нет, поэтому координаторский опрос результатов не видит; (3) дубликат брифа в корне очереди запускается сразу после завершения первой копии — сегодня так дважды перезапустились завершённые волны 1509 и 1541. Круг 5 = волна 1617 на A2.
2026-09-04T00:09:42.314Z · coordinator
Круг 5 сдан: коммит 7dd74a03. Приёмка = волна 1627 на A2. Проверяется в том числе то, из-за чего сегодня терялись результаты: маркер _DONE должен писаться по абсолютному пути, а не внутрь worktree; нормализация метки не должна ломать защиту живого worktree; уборщик не сносит клоны волн вне wt-*.
2026-09-04T00:57:19.442Z · coordinator
Приёмка 1627 = GO для 7dd74a03. Кандидат 3/3 PASS (v3r3 + v3r4 + v3r5) против базы 2/3, где v3r5 даёт 4 ожидаемых падения; отдельная проверка 1/1 на кандидате и базе. Приёмщик подтвердил обработку клона вне wt-* через readlink-шим. Строка переноса: l99: cherry-pick 3e754e4c..7dd74a03. Линия готова к следующей посадке.
2026-09-04T04:03:18.368Z · coordinator
ПОСАДКА l99 ВЫПОЛНЕНА (волна 1638-l99merge2 → коммит 32c2c885551b2a6f235fa80fd4f1010ee82a4ade, приёмка ACCA2TRIO=GO по WEB-478).

Артефакт me2-standalone-linux-arm64-32c2c885-20260904T025504Z.tar.gz, sha256 440c0ce1de0cf8f03ee61a09e7c70acd711953252043541eb626fb29c3a87967. Сборка на A2 прошла все гейты (P19 совместимость миграций, гейт расхождения схемы, свидетельства активации, паритет NEXT_PUBLIC, повторный гейт схемы после сборки), CHAIN_EXIT=0. Аттестация сборки эфемерным ключом, подпись ключом владельца выполнена на ноуте, отпечаток доверенного ключа da2e5642ccfa5fb8 совпал с pem в рабочем каталоге.

Стадии посадки на A1: распаковка (834 МБ), окружение (ключи идентичны прошлому выпуску, все sha подтянуты из аттестации), загрузочный контракт (VERDICT: boot proceeds, ни одной проблемы), сухой прогон на 3011 (health 200, paidReady true, posture enforce_ready, release.sourceCommit 32c2c885), миграции (No pending migrations to apply — l99 схему не меняет), переключение.

После переключения: nc-a1 active, health 200, /api/ready ready=True release.id=32c2c885551b, /api/ready/paid 200 без отказов, публичные https://nb.wool2.online/api/health и /api/ready/paid по 200, /login 200. Пост-QA: метрики растут (3292 → 3294 за полторы минуты), heartbeat воркера индексации свежий, файл heartbeat на месте.

ВАЖНО ПО ТЕЛЕФОНИИ: рестарт ag-sip-native из стадии flip вырезан по указанию координатора (телефония стабилизирована ночью после правки падения шлюза). Проверено перед переключением, что односторонний lease-фенс WEB-476 больше не применим: в живом каталоге gateway/ есть gatewayLeaseMonitor.js с самовосстановлением по счётчику удачных проверок. После переключения: readiness шлюза 4080=200, строк LEASE_LOST ноль, Asterisk не перезапускался (аптайм 1 ч 55 мин), приложение live-8014-snoop-ingress-spike зарегистрировано, обе учётки на месте.

Резервный контур: выпуск опубликован, release_id=l99-32c2c885, remote /home/app-artifact/l99-32c2c885, 228122994 байта, retention 5 (снят l93-02d032e4). HETZBK_ENV_LINE и HETZBK_ENV_ATTESTATION_FILE переключены на l99, копия настроек сохранена. Упаковка окружения committed: env-l99-32c2c885-20260904T035858Z.tar.enc, sha256 1cd5e4e67200fca5c9b907753a511ff0cac9a88fb6f3e5e3c2bb952f5f5e4b73, retention 10.

Замечание по содержанию: diff l99 против l97 — только infra/wave-dispatcher (10 файлов, 1507 строк), кода приложения нет, поэтому изменения поведения не ожидалось и не наблюдается.
2026-09-04T15:08:17.251Z · coordinator
04.09 координатор: в body добавлен блок «ЭВОЛЮЦИЯ 04.09.2026» — состояние, причина с доказательством, что село, что осталось, ловушки.
2026-09-05T14:36:22.860Z · coordinator
[05.09 14:36Z координатор] Повтор 05.09: волна 1989 (bigdocpersist2, A1) умерла ~13:35Z, а её `node --import tsx tests/e2e/web495/live-dev-server.mts` (cwd /home/wave/waves/bigdocpersist2-work) остался сиротой под systemd с RSS 15,7 ГБ из 23 — диспетчер A1 час не стартовал волны (память на полу), плюс в очереди лежал файл PAUSE (владелец ubuntu, 13:35Z — кто положил, не установлено). Лечение руками 14:28Z: kill сироты, mv PAUSE → PAUSE.lifted-…, 1989 возвращена в очередь; за минуту стартовали 1982/1984. Нужен в диспетчере: (1) при done/stale-marker убивать всё дерево процессов сессии (KillMode=control-group для tmux-сессии волны или pkill по cwd рабочего каталога волны); (2) писать в dispatcher.log причину простоя (PAUSE-файл / memory floor) каждые N минут, а не молчать.
2026-09-08T11:16:55.965Z · coordinator
[08.09 11:16Z координатор] **08.09 подтверждено ещё раз, и это стоило часа простоя.** Диспетчер A2 не стартовал НИ ОДНОЙ волны при полной очереди, живом диспетчере и чистых флагах PAUSE/CODEX_CAP. Причина — свободного диска 3.9 ГБ при пороге `MINFREE_GB=8`; в этом состоянии диспетчер молчит, ничего не пишет в журнал.

Место съели именно утечки этого тикета: `waves/.*-pgdata` от ЗАВЕРШЁННЫХ волн на 9.2 ГБ плюс осиротевший `postgres`, до сих пор державший `artifacts-2051-.../pgdata` (волна 2051 давно закончилась). На A1 тот же класс: 34 ГБ в отработанных деревьях волн, диск дошёл до 0.

**ТРАБЛШУТИНГ:** «очередь стоит при нуле волн» → смотреть `df` ПЕРВЫМ, не капы. Чистить безопасно: каталог `.*-pgdata` без живого процесса и дерево волны, у которой бандл уже сохранён. Ловушка при проверке занятости: `pgrep -f "<путь>"` матчит СОБСТВЕННУЮ команду проверки — брать живые волны из `tmux ls` сокета диспетчера, а не из pgrep.
2026-09-10T14:32:41.721Z · coordinator
ЭВОЛЮЦИЯ 10.09 — течь ЖИВА, но масштаб измерен и уборка частично работает.

Что работает: диспетчер A1 при закрытии волны честно убивает сессию tmux и всё дерево процессов
(`cleanup: killed pid=… signal=TERM`), а также контейнеры podman по метке волны.
Видно в /home/wave/waves/queue/dispatcher.log за сегодня по волнам 3277, 3280, 3281, 3282, 3286, 3287.

Что НЕ убирается: временные базы и кеши сборки остаются после закрытия волны.
Измерено при чистке дисков 10.09:
- на A2 висел /tmp/p16-3277-pgdata (64 МБ) от волны, закрытой в 10:45Z;
- на A1 в приватном /tmp службы диспетчера лежали два дерева зависимостей b32-05 на 4.4 ГБ суммарно;
- мёртвые деревья волн на A1 занимали 11.4 ГБ (снято сегодня).

Связь с простоем парка: именно этот мусор довёл A1 до 5 ГБ при пороге диспетчера 12 ГБ,
и очередь встала молча. То есть течь не косметическая — она напрямую останавливает работу.
2026-09-10T18:55:03.546Z · coordinator
BOARD-WASH-20260910:WAVE-3333
По поручению владельца 18:43Z назначена исполнительская волна 3333 (dispatcher), Codex gpt-5.6-luna xhigh, A1. Полная история карточки и база l115c 1ad52e16b доставлены, brief-guard и проверка Git-базы пройдены. Задача: проверить существующую сдачу, устранить остатки, передать бандл и доказательства. Финальная приёмка и посадка остаются за координатором. Запуск группы подтверждён в журнале диспетчера.
КАРТА ДОКУМЕНТОВ: ноут /Users/annakorin/nc-ops-scripts/board-wash-20260910/3333-wash-dispatcher-brief.md; A1 /home/wave/waves/inputs/board-wash-20260910/dispatcher-tickets.json; ожидаемый отчёт /home/wave/waves/3333WASHDISPATCHER-REPORT.md. Правила обогащения: WEB-449.
2026-09-10T18:55:03.826Z · coordinator
WAVE-SLOT-FIX-20260910T1853
10.09 18:53Z: устранены два дефекта счётчика слотов. A1: command substitution удалял переводы строк после # <wave>, поэтому process_labels читал 3332-wash-billingWhen, а marker — 3332-wash-billing; три работника считались шестью. runner теперь использует printf -v; диспетчер совместим с уже запущенными старыми prompt. Регресс 3/3 + newline 1/1; на живой A1 count 6→3, три tmux-сессии сохранены при restart (KillMode=process). После исправления запущены 3335 и 3339, теперь пять групп. Neo: счётчик включал сам wq-dispatcher и съедал один из двух слотов; имя исключено в обеих точках подсчёта, перезапущена только панель диспетчера, wash-roles сохранён. Бэкапы: A1 dispatcher.sh.pre-label-20260910 и wave-runner.sh.pre-label-20260910; Neo wave-dispatcher-neo.sh.pre-label-20260910. Исходники и тест: ноут /Users/annakorin/nc-ops-scripts/board-wash-20260910/. Это хостовые исправления; остальные критерии WEB-478 ещё проверяет 3333.
2026-09-12T22:17:55.093Z · coordinator
[12.09 22:17Z координатор] МОЙКА 12.09 (3566-wash-g1-infra): диспетчер v3 посажен в l99, процессы/контейнеры при DONE убивает. Но течь жива: временные БД (*-pgdata) и кеши сборки остаются, дискового гейта нет (MINFREE_GB сторожит память). Остаток: дисковый гейт + уборка temp-БД/кешей. Статус не меняю.
Остаток: Дисковый гейт (MINFREE_GB сторожит память, не диск) — бокс молча умирает при заполнении диска.; Уборка temp-БД (*-pgdata) и кешей сборки после DONE.; Волна 3333 назначена 10.09 — остатки в работе.
Отчёт: /Users/milamarty/waves/3566WASH*-REPORT.md (M4), копия nc-ops-scripts/shift-20260912-resume/wash-20260912/collected/. Проверка по исходнику прода l115g (9c8a9762).
2026-09-12T22:26:08.450Z · coordinator
[12.09 22:26Z координатор] Поправка к мойке 12.09 (3566): остаток «дискового гейта нет» устарел. На A1 с 09.09 стоят три таймера: wave-disk-janitor.timer (каждые 10 мин; /usr/local/bin/wave-disk-janitor.sh: при свободном диске <14 ГБ снимает неактивные деревья волн старше 60 мин, лог /var/log/wave-disk-janitor.log), wave-janitor.timer (очередь, каждые 5 мин), wave-orphan-sweep.timer (WEB-478 interim sweep, каждые 30 мин). Диспетчер (wave-dispatcher.service v3) держит MINFREE по памяти, дворник — по диску; в dispatch-a1-tree.py координатора добавлен preflight «диск ≥14 ГБ». Сейчас на A1 свободно 22 ГБ при 2 волнах. Что реально остаётся по тикету: подтвердить уборку temp-БД (*-pgdata) и кешей сборки после DONE на A2 (там дворника нет — A2 87% диска, 14 ГБ), и записать в паспорт.
2026-09-12T23:03:56.796Z · coordinator
[12.09 23:03Z координатор] Координатор 23:1xZ: на A2 поставлен тот же дворник диска, что на A1 — /usr/local/bin/wave-disk-janitor.sh + wave-disk-janitor.timer (каждые 10 мин; порог 16 ГБ; снимает только неактивные деревья волн старше 60 мин в /home/ubuntu/waves; не трогает inputs/evidence/queue, стенды *-stand, базы .NNNN-* и линейные wt-l1*). Сейчас на A2 свободно 23 ГБ (снесены артефакты l115b–e, wt-l115d, wt-l115g/.next). Остаток тикета: уборка temp-БД после DONE волн — покрыта исключением (базы стендов не трогаем, снимать вручную после сдачи), записать в паспорт при следующей дельте.
2026-09-13T10:45:16.995Z · coordinator
[13.09 10:45Z координатор] Мойка доски, волна 3653. RETURN: диспетчер v3 в l99 (32c2c885, ACCA2TRIO GO) убивает процессы/podman при DONE, дисковый дворник стоит на A1 (14 ГБ) и A2 (16 ГБ). Остаток мелкий: уборка temp-БД (*-pgdata) — ручное исключение (стенды БД не трогаем) + запись в паспорт при следующей дельте (координатор). Статус не меняю.
2026-09-13T12:36:48.506Z · coordinator
[13.09 12:36Z координатор] Волна 3700. RETURN: диспетчер v3 в l99 (32c2c885, ACCA2TRIO GO) убивает процессы/podman при DONE, дисковый дворник стоит на A1 (14 ГБ) и A2 (16 ГБ). Не убирается никем: temp-БД (*-pgdata) и кеши сборки. Волной 3700 специфицировано безопасное правило уборки (JANITOR-SPEC.md: владелец + живость из tmux-сокета + манифест стендов, возраст только вторичен). Остаток: реализовать правило в дворнике (волна) + записать в паспорт (координатор). Статус не меняю.
2026-09-13T15:36:07.798Z · coordinator
[13.09 15:36Z координатор] Дворник за волнами реализован волной 3725 (GO) по спецификации 3700. Главное правило соблюдено: ни одно удаление невозможно без признака «брошено», а возраст сам по себе признаком не считается — иначе дворник снёс бы живой стенд, который стоит часами.
Сухой режим по умолчанию, реальное удаление только по явному флагу, выключается одним движением. Тесты с отрицательными контролями: живой стенд не удаляется ни при каких условиях, брошенное удаляется, при сомнении дворник сообщает, а не удаляет.
Что запускать на боевых машинах и с какой периодичностью — отдельным файлом волны.
2026-09-13T17:37:06.170Z · coordinator
[13.09 17:37Z координатор] ПОСАЖЕНО В ПРОД. Линия l115k, источник `ac4eb8c68d70de829608bcae5a7acef693ab85fb`, артефакт `me2-standalone-linux-arm64-ac4eb8c68-20260913T165733Z` sha256 `eafbeb5146ddf0b498f82d608ddd30d15053d23156b43603dfb88dfe71aaebed`, релиз `arm64-l115k-20260913T165733Z`, переключение 13.09 17:27:44Z.

Проверено после переключения: публичный `https://app.sixbyy.com/` 200, `/api/ready` `ready:true` с коммитом линии, проба базы пройдена, `paidReady:true`, поза `enforce_ready`, обе копии приложения (3010 и 3012) и рабочие таймеры на новом релизе, упавших юнитов нет. Артефакт опубликован на Hetzner: `release_id=l115k-ac4eb8c6`, 244545299 байт, удержание 5.

Откат: `sudo python3 /home/ubuntu/transition-l115k.py rollback` → l115j (`133fe00c`).

Особенность этой посадки: впервые с 12.09 поехали настоящие миграции, 206 → 209 (`web651_source_text_chunk`, `web575_erasure_tombstone`, `web575_embed_share_audience`), расписка `prod/shared/l115k-migration/apply-receipt.json`, личность применения `web_migrator`.
2026-09-14T12:07:35.440Z · coordinator
[14.09 12:07Z координатор] WEB-478 — дворник диска сносит рабочие каталоги под живой работой
Обновление от 2026-09-14. Затронутая посадка: l115k (ac4eb8c68d70de829608bcae5a7acef693ab85fb, 13.09 17:27:44Z) — тикет в числе получивших комментарий о посадке и проверенных пост-QA линии.
Карточка написана для человека, который открывает её впервые и не имеет ни журнала смены, ни переписки.

1. ЧТО БЫЛО СЛОМАНО

Работа исчезала прямо во время исполнения. Долгий процесс (нагрузочный прогон, стенд) вдруг обрывался без объяснения, а его рабочий каталог оказывался удалён. Со стороны это выглядело как «волна умерла сама» или «процесс упал» — и месяцами объяснялось именно так.

2. КАК НАШЛИ И ПОЧЕМУ НЕ ПОЙМАЛИ РАНЬШЕ

Нашли 13.09 18:20Z, разбирая оборвавшийся нагрузочный прогон. Цепочка восстановлена по журналам:
  `wave-disk-janitor.service` сработал в 18:09:08Z (диск 14.8 ГиБ при пороге 16) и УДАЛИЛ дерево измерительного стенда ПРЯМО ВО ВРЕМЯ ПРОГОНА.
  Через две минуты сторож диспетчера увидел процессы, чей текущий каталог ведёт в удалённый путь, и поубивал их все — шесть процессов: сам раннер прогона, генератор нагрузки, сэмплер, индексирующий воркер, звуковой демон.
ПОЧЕМУ ЗАЩИТА НИКОГДА НЕ РАБОТАЛА. В скрипте дворника была проверка занятости каталога живым процессом: `ls -l /proc/*/cwd | grep -q "$d"`. Но `$d` приходит из цикла `for d in "$root"/*/` — то есть С КОНЦЕВЫМ СЛЭШЕМ, а `/proc/PID/cwd` указывает на путь БЕЗ слэша. СОВПАДЕНИЯ НЕ БЫЛО НИКОГДА: защита «каталог занят» не работала с момента написания. Исключения по именам (`*-stand*`, `*wt-l1*`) дерево стенда тоже не покрывали — оно называлось иначе.
Формулировка из журнала: это, весьма вероятно, и есть часть тех «семи смертей» прогона, которые списывались на волны.

3. ЭВОЛЮЦИЯ, ВКЛЮЧАЯ ТУПИКИ

3.1. ТУПИК — ПОЧИНИЛИ ОДИН СКРИПТ И СЧЛИ ПРОБЛЕМУ ЗАКРЫТОЙ. Дерево стенда снесли СНОВА, в 01:04 следующей ночи. На этот раз виноват был ДРУГОЙ, ОТДЕЛЬНЫЙ сторож — `/usr/local/bin/a2-waves-disk-guard.sh`, о существовании которого никто не знал.
ТА ЖЕ БОЛЕЗНЬ, ДРУГОЙ СКРИПТ: он сносит любое рабочее дерево старше 45 минут, а занятость проверял как `pgrep -f "$b"`, где `$b` — имя каталога. Но процессы стенда запускаются командой вида `node --conditions=react-server --import tsx scripts/…` и имени каталога в своей командной строке НЕ СОДЕРЖАТ — они лишь РАБОТАЮТ в нём. Проверка не срабатывала никогда.
ВЫВОД КЛАССА, записанный тогда же: искать ВСЕ чистильщики, а не первый попавшийся.

3.2. ТРЕТИЙ ЧИСТИЛЬЩИК, на третьей машине: `~/bin/waves-disk-guard.sh` на маке. Сносит любое рабочее дерево старше 60 минут или чья волна завершилась, и ПРОВЕРКИ ЗАНЯТОСТИ У НЕГО НЕТ ВООБЩЕ — даже нерабочей.
Здесь принято ДРУГОЕ решение, и оно осознанное: не чинить третий скрипт ночью, а ВЫНЕСТИ долгоживущую работу из зоны его действия — каталог пересоздан там, где сторож не сканирует. Чинить сам сторож — отдельная задача, и она не должна блокировать работу.
ИТОГ ПО КЛАССУ: ТРИ независимых чистильщика на ТРЁХ машинах, все три сносят рабочие каталоги под живой работой; у двух проверка занятости была сломана по-разному, у третьего её не было.

3.3. ЧЕТВЁРТАЯ ПОТЕРЯ — И ВИНОВНИКА НЕ УДАЛОСЬ УСТАНОВИТЬ. Дерево харнесса исчезло в интервале между двумя известными моментами, оба уборщика к тому времени имели исключение для этого класса каталогов, и ОБА ЛОГИРОВАЛИ ТОЛЬКО СЧЁТЧИК: «снято деревьев=1». УДАЛЕНИЕ БЕЗ ИМЕНИ ДЕЛАЕТ ПРОПАЖУ НЕРАССЛЕДУЕМОЙ — это отдельный дефект, и он был исправлен: оба теперь пишут путь, свободное место, признак занятости и возраст.

3.4. СМЕЖНОЕ, НО ВАЖНОЕ: УБОРЩИК, КОТОРЫЙ СТАЛ ДЕТЕКТОРОМ ПОТЕРЬ.
Отдельный уборщик рабочих каталогов написан с ОДНИМ принципом: удалять каталог волны, только если её работа уже лежит в зеркале. Первый же прогон удалил пять каталогов и ОТКАЗАЛСЯ удалять шесть — их работы в зеркале не было, она существовала только на диске одной машины. Среди отказавшихся оказались пост-QA линии l115l (по которому закрыты WEB-188 и WEB-195) и приёмка, остановившая боевое восстановление документов. Бандлы у всех шести были на месте и с верными суммами — их просто никто не втянул в зеркало.
Ценность уборщика оказалась НЕ В ДИСКЕ: каждый прогон уборки заодно перечисляет всю работу, существующую в ОДНОМ ЭКЗЕМПЛЯРЕ.
В ту же смену найдена и ошибка в самом уборщике, до того как она навредила: при пустом номере волны условие сопоставления совпадало с любой строкой из-за двойного пробела, и каталоги пропускались с ложным «волна работает».

4. ЧТО СДЕЛАЛИ В ИТОГЕ

  `wave-disk-janitor.sh`: концевой слэш снят, сравнение стало точным (путь и граница), добавлено исключение для каталогов измерительного стенда. Резервная копия рядом со скриптом.
  `a2-waves-disk-guard.sh`: проверка занятости переведена на настоящий признак (текущий каталог процесса) плюс явные исключения; старая проверка по имени оставлена как дополнительная. Резервная копия рядом.
  Сторож на маке: не чинился; долгоживущая работа вынесена из зоны его действия.
  Оба уборщика на A2 теперь ЛОГИРУЮТ ИМЯ удаляемого каталога, а не только счётчик.
  Уборщик рабочих каталогов установлен постоянно и удаляет только то, что уже есть в зеркале.
Тикет получил комментарий о посадке линии l115k (`ac4eb8c68d70de829608bcae5a7acef693ab85fb`, 13.09 17:27:44Z) и вошёл в её полный пост-QA.

5. ЧЕМ ДОКАЗАНО И ГРАНИЦЫ ЗАЯВЛЕНИЯ

ДОКАЗАНО:
  Пост-QA линии l115k (волна 3777): `wave-disk-janitor.test.mjs` гоняет РЕАЛЬНЫЙ `.sh` в песочнице, 11 из 11. Это не проверка по описанию, а прогон настоящего скрипта.

ГРАНИЦЫ — читать обязательно:
  ДОКАЗАН ОДИН ПОЧИНЕННЫЙ СКРИПТ. НЕ ДОКАЗАНО, что на машинах парка нет ЧЕТВЁРТОГО чистильщика того же класса. Три были найдены по одному, каждый после новой потери.
  Сторож на маке НЕ ПОЧИНЕН — работа просто вынесена из зоны его действия. Если туда попадёт новый долгоживущий каталог, он будет снесён.
  Четвёртая потеря дерева ТАК И НЕ ОБЪЯСНЕНА: на момент, когда её разбирали, логи не называли имён.

6. ЧТО ОСТАЛОСЬ ОТКРЫТЫМ

  Сторож рабочих каталогов на маке.
  Полная инвентаризация чистильщиков по всем машинам — она делается командой, а не доказана однократно.

7. TROUBLESHOOTER — ЕСЛИ РАБОЧИЙ КАТАЛОГ СНОВА ИСЧЕЗ

Шаг 1. Перечислить ВСЕ чистильщики на машине, а не искать первый попавшийся:
    systemctl list-timers | grep -iE "janitor|guard|clean"
  На маках дополнительно смотреть `~/bin` — там сторож живёт не как systemd-юнит.

Шаг 2. В журнале диспетчера искать записи об убийстве процессов с удалённым рабочим каталогом. ЭТО СЛЕДСТВИЕ, А НЕ ПРИЧИНА: сначала кто-то удалил каталог, потом сторож добил процессы. Не тратить время на диспетчер.

Шаг 3. Проверить проверку занятости у каждого найденного чистильщика. Два известных способа ошибиться:
  сравнение с `/proc/*/cwd` С КОНЦЕВЫМ СЛЭШЕМ — не совпадёт НИКОГДА;
  `pgrep -f <имя каталога>` — не сработает для процессов, которые просто РАБОТАЮТ в каталоге, но не несут его имени в командной строке.

Шаг 4. Если чистильщик пишет в журнал только счётчик — сначала научить его писать ИМЯ удаляемого каталога, иначе следующая пропажа снова будет нерасследуемой.

Шаг 5. Прежде чем решить, что работа потеряна, проверить, есть ли её бандл рядом с зеркалом. Шесть волн за смену имели целые бандлы с верными суммами, которые никто не втянул. Работа была цела, но существовала в одном экземпляре.
2026-09-14T13:21:58.610Z · coordinator
[14.09 13:21Z координатор] VERDICT=GO

# WEB-478 — волны оставляют dev-серверы и podman-контейнеры

Волна: `3856-web478-wave-leftovers` · ветка `wave/3856-web478-wave-leftovers`
Коммит: `6cec449656d3a9b5d8e3863ad9ec7497f306b107`
Дата сдачи: 2026-09-14

## 0. Что сдано

| Артефакт | Путь | Состояние |
| --- | --- | --- |
| Отчёт | `/Users/milamarty/waves/3856WEB478WA-REPORT.md` | этот файл |
| Бандл | `/Users/milamarty/waves/3856WEB478WA.bundle` | `git bundle verify` = okay, `list-heads` = `6cec4496…` |
| Контрольная сумма бандла | `/Users/milamarty/waves/3856WEB478WA.bundle.sha256` | `5d4e9320…` |
| Доказательства | `/Users/milamarty/waves/3856WEB478WA-evidence/` | `SHA256SUMS` внутри, все `OK` |
| Маркер | `/Users/milamarty/waves/3856WEB478WA_DONE` | создан после всех трёх |

Сам фикс (в бандле): `infra/wave-dispatcher/bin/wave-leftovers-sweep.sh` (новый),
роутинг `sweep` в `bin/wave-leftovers.sh`, 10 тестов
`tests/wave-leftovers-sweep.test.mjs`, раздел в `README.md`.

---

## 1. Факты: что остаётся после волн

Правило этого отчёта: каждое число либо получено мной лично, либо помечено
**«не измерено»**; утверждения из кода/README/промпта помечены **«заявлено»**,
а то, что я проверил чтением кода или запуском теста, — **«доказано»**.

### 1.1 Что доказано чтением кода (причина появления остатков)

Диспетчер v3 (`infra/wave-dispatcher/wave-dispatcher-a2.sh`) убирает за волной
только три класса ресурсов, и все три имеют узкие условия:

1. **«Тяжёлые» процессы.** `is_heavy_command` (`wave-dispatcher-a2.sh:227-233`)
   считает «тяжёлым» только командную строку, содержащую один из четырёх
   подстрок: `next dev`, `pnpm start`, `pnpm run start`, `pnpm exec next dev`.
   `cleanup_orphan_heavy_processes` (`:766-787`) убивает процесс только если его
   cwd прошёл `orphan_worktree_for_pid` (`:749-763`), то есть cwd лежит под
   `$COORDINATOR_WAVES_ROOT|$WAVES_ROOT|$WAVE_HOME`/`wt-*` **и** волна не в
   `active_wave_names`. **Доказано**: любой dev-сервер с другой командной
   строкой — `vite`, `tsx watch`, `npm run dev`, `npm start`, `next start`,
   `uvicorn`, `nest start`, `remix dev`, `webpack serve`, `nodemon`,
   `node .next/standalone/server.js` — этим кодом не тронут вообще. Равно как и
   dev-сервер, чей cwd уже не `wt-*` (например, `$WAVES_ROOT`).

2. **Контейнеры.** `cleanup_orphan_heavy_containers` (`:789-804`) трогает только
   контейнеры с меткой `wave=<label>` (`--filter label=wave`) и только если волна
   не активна. **Доказано**: контейнер без метки `wave` остаётся навсегда.

3. **Леджер ресурсов.** Тип `dev-server` зарегистрирован в леджере
   (`$WAVES_ROOT/.wave-resources/<label>/resources.tsv`), но **доказано** grep'ом:
   слово `dev-server` встречается только в `README.md`, в промпте
   `bin/wave-runner-real.sh:109` («Register every … dev-server PID …») и в
   `bin/wave-resource-cleanup.sh` (`:102-103,204,378`). В `tests/` слова
   `dev-server` нет ни разу. То есть регистрация dev-server — **только просьба
   в промпте к агенту волны**: если агент не зарегистрировал PID, диспетчеру
   нечего убирать по леджеру, а его орфан-свип, как показано выше, таких
   процессов не видит.

4. **EXIT-ловушка раннера.** `cleanup_on_exit`
   (`bin/wave-runner-real.sh:112-128`) вызывает
   `wave-resource-cleanup.sh cleanup "$label" "$WAVE_RESOURCE_MANIFEST" "" ""` —
   два последних аргумента (session и sid) переданы **пустыми строками**. Чистка
   раннера опирается целиком на леджер, а не на tmux-сессию/группу процессов.

5. **Нет реального setsid.** `wave-dispatcher-a2.service:25` — `SETSID_BIN=env`,
   `:32` — `KillMode=process`. На A1/A2 `setsid` нет, группа процессов не
   создаётся, поэтому «убить по sid» нельзя: дочерние dev-серверы живут своей
   группой и после смерти лидера.

### 1.2 Что заявлено, но мной не измерено (нет доступа)

Заявлено в брифе: «волны оставляют dev-серверы и podman-контейнеры, которые
жрут память и порты машины A1». Это **заявлено**. Я не могу это замерить:
запрещён SSH на другие машины, prod, sudo. Поэтому:

- конкретный список живых процессов/контейнеров на A1/A2 — **не измерено**;
- из каких именно волн они остались — **не измерено**;
- сколько живут (время жизни) — **не измерено**;
- сколько памяти и какие порты заняты — **не измерено**.

Единственный dispatcher-лог, доступный на этой машине, —
`~/waves/queue/dispatcher.log` (старый лог M4-координатора, не боевой A1/A2).
Боевой `/home/ubuntu/waves/queue/dispatcher.log` с A1/A2 без SSH недоступен.

**Что нужно, чтобы это измерить** (пошагово, на A1/A2):
1. `ps -eo pid,ppid,args= | grep -E 'next|vite|pnpm|uvicorn|nodemon|tsx'` — список
   подозрительных dev-серверов.
2. `ls -l /proc/<pid>/cwd` по каждому — из какого `wt-*` (какая волна).
3. `podman ps -a` — контейнеры без метки `wave` и с меткой.
4. `ls -la --time-style=+%FT%T` на `wt-*` и `stat` на PID — время рождения.
5. `cat queue/dispatcher.log` + `done/` — когда волна реально закрылась.
Затем: разность «время смерти волны» − «время запуска процесса/контейнера» и
есть время жизни остатка.

### 1.3 Вывод по фактам

Механизм появления остатков **доказан** (узкие паттерны `is_heavy_command` +
только `wt-*` cwd + только метка `wave` + регистрация dev-server лишь промптом).
Живой размер бедствия на A1/A2 **не измерен** (нет доступа), и в отчёте он
честно помечен как «не измерено», а не подменён числом.

---

## 2. Место без гарантированной очистки

Две дыры, обе подтверждены чтением кода:

- **`wave-dispatcher-a2.sh:766-787` (`cleanup_orphan_heavy_processes`)** — место,
  где запуск сервера не имеет гарантированной уборки: сервер, не совпавший с
  `is_heavy_command`, или с cwd вне `wt-*`, здесь **не обрабатывается**. Никакого
  второго прохода «по портам» или «по остальным паттернам argv» в диспетчере нет.
- **`bin/wave-runner-real.sh:112-128`** — EXIT-ловушка передаёт пустые session/sid,
  то есть «убить всё, что запустил процесс волны», структурно невозможно:
  полагаемся только на леджер, который агент должен был заполнить сам.

---

## 3. Чистильщик с проверкой перед удалением

Новый `infra/wave-dispatcher/bin/wave-leftovers-sweep.sh` (417 строк) закрывает
обе дыры по конвенции дискового дженитора `wave-disk-janitor.sh`:
**единственный источник правды о живости — tmux-сокет**, метка времени никогда
не считается доказательством покинутости, а всё, что чистильщик НЕ удалил,
печатается как `decision=left … because=<причина>`.

Запуск:
```bash
/home/ubuntu/bin/wave-leftovers.sh sweep          # dry-run (по умолчанию, только отчёт)
/home/ubuntu/bin/wave-leftovers.sh sweep --apply  # удаление доказанно-покинутого
```

### 3.1 Что он считает dev-сервером (шире диспетчера)

`DEV_SERVER_PATTERNS` (переопределяется переменной окружения):
`next dev`, `pnpm start`, `pnpm run start`, `pnpm exec next dev`, `vite`,
`tsx watch`, `npm run dev`, `npm start`, `next start`, `uvicorn`, `nest start`,
`remix dev`, `webpack serve`, `nodemon`, `node .next/standalone/server.js`.
Второй источник кандидатов — `lsof -nP -iTCP -sTCP:LISTEN -F p`: слушатель порта,
не совпавший с паттерном argv, всё равно отчитывается/убирается, если его
владелец доказанно мёртв.

### 3.2 Проверка перед удалением (fail-closed)

Для процесса порядок проверок в `reap_candidate_pid`:
1. **belongs-to-live-wave** — pid или его предок (обход `ppid`) является pid
   живой tmux-панели → `left … because=belongs-to-live-wave`.
2. **cwd** не читается (`/proc/<pid>/cwd`) → `left … because=owner-unknown
   reason=cwd-unreadable`.
3. **owner-unknown** — cwd не под `wt-*` корнем → `left … because=owner-unknown`.
4. **liveness-unavailable** — tmux-сокет не ответил (`live_query_ok != 1`) →
   `left … because=liveness-unavailable` (fail-closed: без источника правды не
   удаляем ничего).
5. **owner-live** — метка волны жива в tmux **или** есть живой running-маркер
   (`label_has_live_marker`, защита от «координатор удалил волну, которая сама
   восстановилась») → `left … because=owner-live`.
6. Только после всех проверок: dry-run → `decision=would-remove …`; `--apply` →
   повторная TOCTOU-проверка `pid_belongs_to_live_pane`, затем TERM → 0.1s → KILL
   (`signal_pid`). Неудача сигнала → `left … because=signal-failed`.

Для контейнера в `reap_candidate_container`:
1. **no-wave-label** — нет метки `wave` → `left … because=no-wave-label
   reason=unattributable` (контейнер без метки **никогда** не удаляется — его
   владельца доказать нельзя).
2. **liveness-unavailable** — тот же fail-closed.
3. **owner-live** — метка жива → `left … because=owner-live`.
4. Иначе dry-run → `decision=would-remove`, `--apply` → `podman rm -f`;
   неудача → `left … because=remove-failed`.

Каждое не-удаление пишется в stdout/`ACTION_LOG` и в `DISPATCHER_LOG` строкой
`decision=left … because=…` — оператор всегда видит причину «оставлен, потому
что …». `--apply` — единственный разрушающий режим; аварийный выключатель —
`/etc/wave-dispatcher/wave-leftovers-sweep.disable` (одним движением).

### 3.3 Доказательство работы

`node --test tests/wave-leftovers-sweep.test.mjs` — **10/10 pass, RC=0**
(вывод полностью в `3856WEB478WA-evidence/sweep-tests.txt`). Среди них:
- dry-run по умолчанию только отчитывается (`would-remove`), ничего не убивает;
- живая волна-владелец оставляется с `because=owner-live` даже под `--apply`;
- процесс, чей предок — живая tmux-панель, сохраняется (`belongs-to-live-wave`);
- мёртвая волна с живым running-маркером сохраняется (self-healing guard);
- недоступный tmux → всё остаётся на месте (`liveness-unavailable`);
- cwd не `wt-*` → `owner-unknown`, никогда не убивается;
- `--apply` реально сигналит живой процесс (тест поднимает `sleep`, проверяет
  `exitCode === null` и `signalCode ∈ {SIGTERM, SIGKILL}`);
- контейнер с меткой мёртвой волны удаляется; контейнер без метки — нет;
- disable-файл останавливает свип до любого осмотра.

---

## 4. Чего чистка НЕ покрывает

- Процессы с cwd вне `wt-*` корней (`owner-unknown`) — оставляются, т.к. владельца
  доказать нельзя. Чтобы их накрыть, нужен отдельный источник привязки
  (например, cgroup на волну), которого сейчас нет.
- Контейнеры без метки `wave` (`no-wave-label`) — оставляются навсегда. Это
  осознанно: удалять «анонимный» контейнер — значит угадывать владельца, что
  запрещено правилом «не убивать живых».
- Порты, не зарегистрированные за PID: `lsof` даёт слушателя, но «занятый порт
  без процесса» не существует; если PID слушателя умер, порт освободился сам.
- Свип **сам себя не планирует**: это команда, а не timer. Диспетчер продолжает
  свой орфан-свип, а этот инструмент нужно вызывать (вручную или из systemd
  timer) — установка таймера в объём не входила.
- Метаданные мёртвых волн в `queue/done/`, леджеры, wt-* каталоги — их убирает
  существующий дисковый дженитор, не этот свип.
- Портируемость: свип написан под Linux (нужны `/proc`, `lsof`, `podman`,
  `tmux` с `-S` и форматами). На macOS он не заработает; тесты в репозитории —
  Linux-only (см. KNOWN ISSUES).
- Живые A1/A2 замеры (память, порты, время жизни остатков) — не входят; это
  «не измерено» из-за запрета SSH/prod.

---

## 5. ДЛЯ ТИКЕТА (полный след для агента с нуля)

### 5.1 Симптом словами пользователя
«Волны на A1/A2 завершаются, но их dev-серверы (`next dev`, `vite`, `pnpm start`,
`uvicorn`, …) и podman-контейнеры остаются жить. Они занимают память и порты,
машина медленеет, новые волны не могут поднять свой сервер или получают занятый
порт. Периодически всё приходится чистить руками».

### 5.2 Как я это искал (шаги, по порядку)
1. Прочитал `infra/wave-dispatcher/wave-dispatcher-a2.sh` целиком — нашёл три
   орфан-чистилки: процессы (`:766`), контейнеры (`:789`), манифесты (`:806`).
2. Увидел `is_heavy_command` (`:227`) — только 4 подстроки. Понял: остальной
   зоопарк dev-серверов диспетчер не видит.
3. Увидел `orphan_worktree_for_pid` (`:749`) — только cwd под `wt-*`. Понял:
   сервер, «уехавший» из ворктри в `$WAVES_ROOT`, тоже невидим.
4. Прочитал `bin/wave-runner-real.sh` (`:109-128`) — регистрация ресурсов идёт
   **промптом**, EXIT-ловушка передаёт пустые session/sid.
5. Прочитал `wave-dispatcher-a2.service` — `SETSID_BIN=env`, `KillMode=process`:
   групп процессов нет, «убить по sid» нельзя.
6. grep `dev-server` по репозиторию — слово есть только в README, промпте и
   `wave-resource-cleanup.sh`; в `tests/` его нет → тип `dev-server` никем не
   протестирован.
7. Попытался собрать живые факты с A1/A2 — упёрся в запрет (SSH/prod/sudo).

### 5.3 Тупики (что не сработало и почему)
- **Замер живых A1/A2** — тупик по запрету: без SSH нет ни `ps`, ни `podman ps`,
  ни боевого `dispatcher.log`. Честно помечено «не измерено».
- **Искать в старом M4-логе `~/waves/queue/dispatcher.log`** — там виден
  антипаттерн «`done(по молчанию лога, маркер не найден)`»: старый координатор
  закрывал волну по тишине лога. Это ровно та ошибка «по таймстампу», которую
  новый дженитор запрещает. Лог не про A1/A2, поэтому фактом по WEB-478 не стал,
  но попал в «как нельзя делать».
- **Прогнать house-тесты `dispatcherv3r5.test.mjs` на macOS** — падают на GNU
  `stat -c` / `realpath -e` (нет на macOS). Это до-существующий факт, не
  регрессия моего изменения; свип-тесты я писал на шимах, поэтому они проходят.

### 5.4 Что сделано (со ссылками)
- `infra/wave-dispatcher/bin/wave-leftovers-sweep.sh` — fail-closed свип
  dev-серверов и контейнеров, «оставлен, потому что …», `--dry-run`/`--apply`,
  disable-файл, защита от само-восстанавливающихся волн.
- `infra/wave-dispatcher/bin/wave-leftovers.sh` — роутинг `sweep`.
- `infra/wave-dispatcher/tests/wave-leftovers-sweep.test.mjs` — 10/10 pass.
- `infra/wave-dispatcher/README.md` — раздел «One-command dev-server / container
  sweep».
- Коммит `6cec449656d3a9b5d8e3863ad9ec7497f306b107`, бандл
  `/Users/milamarty/waves/3856WEB478WA.bundle` (verify = okay),
  доказательства в `/Users/milamarty/waves/3856WEB478WA-evidence/`.

### 5.5 Что доказано и где граница
- **Доказано**: дыра в коде (узкие паттерны + только `wt-*` + только метка `wave`
  + регистрация dev-server промптом + пустые session/sid + `SETSID_BIN=env`).
- **Доказано**: свип по правилам «проверь перед удалением» работает на шимах,
  10/10.
- **Граница**: живой масштаб бедствия на A1/A2 (список процессов/контейнеров,
  время жизни, память, порты) — **не измерен**; чтобы доказать, нужен доступ на
  A1/A2 (шаги в §1.2).

### 5.6 Что открыто
1. Замерить живые остатки на A1/A2 (§1.2) — нужен доступ.
2. Решить, кто вызывает `sweep`: systemd timer или диспетчер — сейчас это
   операторская команда.
3. Процессы с cwd вне `wt-*` и контейнеры без метки остаются по дизайну; если их
   нужно накрывать — нужна cgroup/метка на волну (§4).

### 5.7 Траблшутер
- `sweep` ничего не удаляет и пишет `sweep=disabled file=…` → сотри
  `/etc/wave-dispatcher/wave-leftovers-sweep.disable` (аварийный выключатель).
- Пишет `liveness-unavailable` → tmux-сокет `$TMUX_SOCKET` не отвечает; свип
  осознанно fail-closed и ничего не трогает — чини сокет, потом `--apply`.
- Пишет `owner-unknown` → cwd процесса не `wt-*`; выясняй владельца вручную.
- Пишет `no-wave-label` → контейнер без метки `wave`; выясняй владельца вручную,
  свип его не удалит.
- Пишет `signal-failed`/`remove-failed` → права или процесс уже ушёл; смотри
  `$ACTION_LOG`.

---

## 6. KNOWN ISSUES

1. **House-тесты диспетчера Linux-only.** `dispatcherv3r5.test.mjs` (и
   `dispatcherv3r3-r6.bash`) падают на macOS: им нужны GNU `stat -c`, `realpath
   -e`, `/proc`, `lsof`, `podman`. Это до-существующее ограничение, не регрессия
   данной волны. Мой свип-тест написан на bash-шимах и на macOS проходит 10/10.
2. **`wave-leftovers-sweep.sh` рассчитан на Linux.** На macOS без `/proc`/`lsof`/
   `podman`/`tmux -S` он не работает. Продакшен-цель — A1/A2 (Linux), поэтому это
   приемлемо, но локальная проверка «вживую» невозможна.
3. **Fail-closed означает «лучше оставить».** При недоступном tmux-сокете свип не
   удалит ничего (это фича, но оператор должен понимать: нет сокета — нет уборки).
4. **Планировщик не установлен.** Свип сам не запускается; интеграция с
   systemd-таймером — за пределами объёма волны.
5. **`SETSID_BIN=env` остаётся как есть.** Без реального `setsid` диспетчер не
   может убивать группы процессов; свип компенсирует это обходом ppid, но полную
   гарантию «весь вывод волны умрёт» даёт только cgroup — это отдельный тикет.
6. **«не измерено» по A1/A2.** Живой список остатков/памяти/портов не получен из-за
   запрета SSH/prod; в отчёте он не подменён оценкой.
2026-09-14T15:14:57.795Z · coordinator
[14.09 15:14Z координатор] VERDICT=NO-GO

# 3871-acc-web478 — независимая приёмка WEB-478

## Итог

Работа не принимается в релиз-кандидаты. Я проверял ровно ref
`refs/waves/3856/wave/3856-web478-wave-leftovers`, который указывает на
`6cec449656d3a9b5d8e3863ad9ec7497f306b107`. Подменённую похожую ветку не
использовал.

Есть три блокирующих факта:

1. Документированный вызов не запускается: новый
   `wave-leftovers-sweep.sh` закоммичен с mode `100644`, а wrapper делает
   прямой `exec`. Фактический результат — `Permission denied`, RC=1; прямой
   вызов имеет RC=126.
2. Обязательный `node --test` не проходит на машине приёмки: 10 тестов,
   1 pass и 9 fail. Причина — строка `declare -A` в Bash 3.2. Тесты автора
   сами запускают `bash`, поэтому это не внешний искусственный способ запуска.
3. Негативный тест нашёл ошибку привязки владельца: живой
   `wave-200-shared` заставляет sweep оставить процесс владельца
   `100-shared` как `owner-live`. Numeric id владельца теряется при сравнении
   `short_label`. Это значит, что доказательство «мёртвый владелец удаляется,
   живой не трогается» не является точным для всех label.

## Что было проверено

### Дерево и материалы

Создано отдельное дерево:

`/Users/limamarty/waves/wt-3871-acc-web478`

и ветка:

`acceptance/3871-acc-web478`

Статус дерева после создания был clean. Tip меняет только четыре пути:

- `infra/wave-dispatcher/README.md`;
- `infra/wave-dispatcher/bin/wave-leftovers-sweep.sh`;
- `infra/wave-dispatcher/bin/wave-leftovers.sh`;
- `infra/wave-dispatcher/tests/wave-leftovers-sweep.test.mjs`.

Отчёта автора и evidence-директории в этом ref нет. Поэтому числа и
утверждения автора из commit message считаются заявками, а не доказанными
фактами. Локальная доска с телом тикета и комментариями отсюда не видна:
«доска отсюда не видна».

### Тесты

Запущен только разрешённый встроенный раннер:

`node --test infra/wave-dispatcher/tests/wave-leftovers-sweep.test.mjs`

Так как команда `timeout` в окружении отсутствует, синхронный запуск был
ограничен Perl alarm-обёрткой. Это не изменило тесты.

Результат: Node сообщил `tests 10`, `pass 1`, `fail 9`, RC=1. Девять
сценариев остановились на:

`declare: -A: invalid option`

Единственный прошедший тест — kill-switch, который выходит раньше этой
строки. Поэтому я не засчитываю остальные девять сценариев как выполненные.

### Проверка главного риска: не убить живое

В изолированном shim-сценарии sweep видел:

- live tmux session `wave-4000-live`;
- live pane;
- кандидат `pid=4242`, команда `next dev --port 3000`;
- cwd владельца `wt-4000-live`;
- режим `--apply`.

Результат был:

`decision=left dev-server pid=4242 because=owner-live owner=4000-live`

и `removed=0`. Это показывает, что ветка защиты живого владельца в логике
есть. Но запуск был квалифицированным: из-за Bash 3.2 пришлось временно
заменить только объявление associative array на индексированный массив,
чтобы добраться до алгоритма. Штатный авторский тест этого сценария не
дошёл до проверки и потому не является доказательством.

### Негативный тест

Сделано условие:

- live owner: `200-shared`;
- кандидат: `100-shared`;
- команда кандидата: `next dev --port 3000`;
- режим: `--apply`.

Корректное правило должно различать два numeric id. Фактический результат
полного shim-сценария:

`decision=left dev-server pid=4242 because=owner-live owner=100-shared`

Это ложное сохранение. Источник дефекта — сравнение `short_label`, которое
снимает numeric prefix и считает одинаковыми разные владельцы с одним
суффиксом. Отдельный запуск самих функций вернул
`NEGATIVE_TEST=FAIL` и RC=1.

### Числа и источники

Все числа в этом отчёте получены из собственных команд:

- hash ref — `git show-ref --verify`;
- состав изменения — `git diff-tree` и `git show --stat`;
- число тестов/pass/fail — вывод `node --test`;
- RC — суффикс `echo RC=$?`;
- modes — `git ls-tree` и фактический вызов wrapper.

Числа из commit message автора не использованы как доказательство.

## KNOWN ISSUES

- `wave-leftovers-sweep.sh` должен иметь executable mode `100755`, либо
  wrapper должен запускать его через явный совместимый interpreter. После
  исправления нужен повтор документированной команды wrapper.
- Нужно определить и обеспечить минимальную версию Bash. На этой машине
  `/bin/bash` — `3.2.57`; submitted tests используют `declare -A`, а также
  Bash process substitution. Нужен повтор именно `node --test` в поддержанном
  окружении, без обхода строки в штатном тесте.
- `label_is_live` должен сравнивать канонический полный owner label, а не
  только `short_label`. Негативный сценарий `200-shared`/`100-shared` должен
  перестать давать `owner-live`.
- В ref отсутствуют авторский report и evidence. Нужна повторная сдача с
  отчётом, источниками чисел, воспроизводимыми логами и SHA256SUMS.
- Podman-путь на этой машине не проверен: `podman` не установлен. Для
  закрытия этого пункта нужен безопасный локальный shim/тест с labelled dead
  container, labelled live owner, unlabelled container и ошибкой источника.

## Для тикета — детским языком

Волна добавила дворника. Он ищет старые dev-серверы и контейнеры. В режиме
dry-run он только рассказывает, что сделал бы. В apply он может удалять.

Я поставил рядом живой процесс. В отдельном безопасном тесте дворник его не
трогает и пишет `owner-live`. Это хорошая часть, но штатный тест до неё не
дошёл из-за ошибки Bash.

Потом я дал дворнику два разных владельца с одинаковым хвостом: `200-shared`
и `100-shared`. Он перепутал их и оставил старый процесс. Значит, доказано не
всё.

Ещё команда из README не запускается из-за отсутствующего executable-бита.
Поэтому работа пока не может идти в линию.

## Граница проверки и открытое

Проверены только локальный ref, локальное acceptance-дерево, локальные shimы,
Node test runner и статический просмотр изменённых файлов. Не проверялись
production, прод-БД, SSH, deployment, release build, `next build`, full tsc,
чужие деревья или чужие процессы. Podman runtime не запускался.

Чтобы повторно проверить после исправлений, нужен такой сценарий:

1. В чистом acceptance worktree проверить точный ref и mode
   `wave-leftovers-sweep.sh`.
2. Запустить `node --test infra/wave-dispatcher/tests/wave-leftovers-sweep.test.mjs`
   штатно; все тесты должны завершиться успешно.
3. Запустить wrapper `wave-leftovers.sh sweep --dry-run --report` и затем
   `--apply` только на локальных shim-ресурсах.
4. Повторить live-process сценарий с процессом, который реально живёт в
   worktree живой волны; проверить отсутствие TERM/KILL и строку `left`.
5. Повторить негативный сценарий с разными numeric owner id и одинаковым
   suffix; проверить, что dead owner больше не считается live.
6. Отдельно прогнать Podman shim-матрицу и сохранить исходные логи.
7. Только после этого заново собрать report, evidence, bundle и SHA256SUMS.

Подробные собственные логи находятся в
`/Users/limamarty/waves/3871ACC478-evidence/`.
2026-09-14T15:16:33.208Z · coordinator
[14.09 15:16Z координатор] # WEB-478 - блок для вставки

Источники, проверенные отсюда: тело тикета из live API; все 32 комментария из локального `web-board.sqlite`, последний комментарий `id=5004` от 2026-09-14T13:21:58.610Z. Статус на live API при финальной сверке: `review`. A1/A2/Mac cleaners live отсюда не проверялись.

## Что болит словами пользователя

Волны завершаются, а их dev-серверы, podman-контейнеры, временные БД и рабочие каталоги продолжают жить или исчезают в неподходящий момент. Машины замедляются, порты заняты, а долгие прогоны обрываются так, будто “волна сама умерла”.

## Что уже сделано и чем доказано

Диспетчер v3 и disk janitor уже были посажены раньше, но поздние комментарии показали, что это не закрывало весь класс. Комментарий `id=4987` фиксирует три независимых чистильщика на трёх машинах и конкретную ошибку с trailing slash в `/proc/*/cwd`. Комментарий `id=5004` даёт новую сдачу wave `3856-web478-wave-leftovers`, commit `6cec449656d3a9b5d8e3863ad9ec7497f306b107`: добавлен `infra/wave-dispatcher/bin/wave-leftovers-sweep.sh`, routing `sweep`, тест `tests/wave-leftovers-sweep.test.mjs`, заявлено `10/10 pass`.

## Что осталось

Сам `id=5004` оставляет границы: live size на A1/A2 не измерен из-за запрета SSH/prod; sweep сам себя не планирует; процессы с cwd вне `wt-*` и контейнеры без label остаются fail-closed; mac cleaner не починен, работа была вынесена из его зоны. Поэтому это не “done”, а review по принятой сдаче и оставшимся операционным решениям.

## Противоречия между комментариями

`id=4775` говорил, что остаток мелкий: temp-БД/cache janitor и паспорт. Поздний `id=4987` отменяет эту простоту: проблема шире, есть несколько cleaners, часть защиты никогда не работала. Старое “диспетчер v3 убивает процессы/podman” уточнено `id=5004`: старый код видел только узкий набор команд, только `wt-*` cwd и только containers с label.

## С ЧЕГО НАЧАТЬ НУЛЕВОМУ АГЕНТУ

Смотреть: commit `6cec449656d3a9b5d8e3863ad9ec7497f306b107`, `infra/wave-dispatcher/bin/wave-leftovers-sweep.sh`, `infra/wave-dispatcher/bin/wave-leftovers.sh`, `infra/wave-dispatcher/tests/wave-leftovers-sweep.test.mjs`, а на хостах - `/usr/local/bin/wave-disk-janitor.sh`, `/usr/local/bin/a2-waves-disk-guard.sh`, `~/bin/waves-disk-guard.sh`.

Первый шаг: принять или доработать 3856, затем решить, кто вызывает `sweep` (manual/systemd/dispatcher) и прогнать dry-run на A1/A2 с логом `decision=left|would-remove`.

Готово: sweep установлен или documented как операторская команда; live dry-run показывает причины решений; нет удаления live work; паспорт/ops docs обновлены; оставшиеся owner-unknown cases явно вынесены.

Нельзя: запускать `--apply` без tmux liveness, удалять по возрасту, удалять anonymous containers, чинить только первый найденный cleaner, трогать каталоги, существующие в одном экземпляре.

Размер: смена. Кодовая сдача есть; большим остаток делает host-интеграция, live dry-run и несколько разных cleaners.

## Закрытие и связи

Не считать полностью закрытым: `id=5004` сам перечисляет open boundaries. Дублей внутри набора не доказано.
2026-09-15T14:42:30.422Z · coordinator
[15.09 14:42Z координатор] Post-QA 4044 дал VERDICT=REOPEN: real dispatcher DONE-cleanup и orphan-sweep прошли на run-owned процессах, но установленный `/usr/local/bin/wave-disk-janitor.sh` оказался старым broad-delete вариантом без `_DONE`, stand-manifest, pgdata и canonical/trailing-slash guards. Evidence 5/5 SHA256. Координатор сравнил exact l115o blob с тестированной guarded-копией: оба sha256 `cad66e9a…`. Guarded exact-l115o файл установлен атомарно 15.09 14:41Z, старый hash `114d8593…` сохранён в timestamped backup; bash syntax и штатный systemd dry-run success/0, receipt `4051WEB478DEPLOY-RECEIPT.txt`. Независимый post-QA A1/4051 поставлен на installed-hash, 11/11 fixture и изолированную apply-матрицу. Статус review→in_progress до полного DONE+marker+SHA.
2026-09-15T15:17:48.828Z · coordinator
[15.09 15:17Z координатор] 4051 post-QA = DONE на установленном A1. Исполняемый дворник побайтно совпадает с l115o; repository fixture 11/11, изолированная apply-матрица 15/15. Реальный systemd timer отработал dry-run с removed=0, failed=0. Полный SHA256 manifest проверен. WEB-478 закрыт.
2026-09-15T22:05:58.155Z · coordinator
ENRICH-4132-WEB-478
```markdown
## ДЕЛЬТА ОБОГАЩЕНИЯ — 2026-09-15T21:54Z, M1/4132 (DeepSeek Flash 4.1, read-only аудит), VERDICT=DRAFT_DELTA

### 1. Вердикт
Карточка ЗАКРЫТА 15.09 15:17Z, но тело этого не знает: «Остаток» всё ещё требует
«реализовать правило уборки temp-БД/кешей в wave-disk-janitor.sh», а «Где мы сейчас»
датировано 13.09.2026. Нулевой агент, читающий тело, считает работу открытой — а она
закрыта с приёмкой. Это ровно тот случай, когда append-only поправка обязательна.

### 2. КАРТА ДОКАЗАТЕЛЬСТВ
- Доска WEB-478 id 5367 (2026-09-15T14:42:30.422Z): post-QA 4044 = REOPEN. Настоящий
  dispatcher DONE-cleanup и orphan-sweep прошли, НО установленный
  `/usr/local/bin/wave-disk-janitor.sh` оказался старым broad-delete вариантом без `_DONE`,
  stand-manifest, pgdata и canonical/trailing-slash guards. Evidence 5/5 SHA256.
  Координатор сравнил exact l115o blob с тестированной guarded-копией: оба sha256 `cad66e9a…`.
  Guarded exact-l115o файл установлен атомарно 15.09 14:41Z, старый hash `114d8593…` сохранён
  в timestamped backup; bash syntax и штатный systemd dry-run — успех/0;
  receipt `4051WEB478DEPLOY-RECEIPT.txt`. Статус review → in_progress.
- Доска WEB-478 id 5377 (2026-09-15T15:17:48.828Z): 4051 post-QA = **DONE** на установленном A1.
  Исполняемый дворник побайтно совпадает с l115o; repository fixture 11/11; изолированная
  apply-матрица 15/15; реальный systemd timer дал dry-run `removed=0, failed=0`; полный
  SHA256 manifest проверен. **WEB-478 закрыт.**
- Тело, «Остаток» (строки 13–16) и «Критерий закрытия» (строка 18) — для сверки формулировки.
- HANDOFF-LIVE.md строка 174 (тот же результат, независимая формулировка).

### 3. ЭВОЛЮЦИЯ / ПОПРАВКИ (append-only)
- 13.09 — тело переписано, правило уборки специфицировано волной 3700 (`JANITOR-SPEC.md`),
  дворник A1 (порог 14 ГБ) и A2 (12.09 23:03, порог 16 ГБ) стоят, temp-БД и кеши сборки
  намеренно вне автоматики.
- 15.09 14:42Z — REOPEN: установленный дворник оказался старым опасным broad-delete вариантом.
  Это ровно тот класс дефекта, который правило 15 ловит: «принято» было заявлено, а на
  машине лежал другой файл.
- 15.09 14:41Z — координатор атомарно установил guarded exact-l115o скрипт (`cad66e9a…`),
  сохранив старый (`114d8593…`) в timestamped backup.
- 15.09 15:17Z — независимая 4051: DONE; карточка закрыта.
- Критерий закрытия тела выполнен: правило уборки реализовано в дворнике и проверено
  (fixture 11/11, изолированная apply-матрица 15/15, systemd dry-run removed=0 failed=0).

### 4. KNOWN ISSUES / ТРАБЛШУТИНГ
1. **На машине может стоять НЕ тот дворник, который принят.**
   - Симптом: приёмка «зелёная», а установленный файл — старая опасная версия.
   - Проверка за 2 минуты: `sha256sum /usr/local/bin/wave-disk-janitor.sh` и сравнить с
     ожидаемым `cad66e9a…` → ожидание совпадения; расхождение = немедленно остановить
     автоматику дворника и доложить.
   - Причина: установка на хост — отдельное действие от приёмки кода; между ними файл на
     машине мог остаться прежним.
   - Лечение/статус: установлен guarded скрипт; backup сохранён. Проверять hash после
     каждой замены.
   - Ссылки: доска WEB-478 id 5367; receipt `4051WEB478DEPLOY-RECEIPT.txt`.
2. **Старый вариант дворника удалял широко.** Отсутствовали guards: `_DONE`, stand-manifest,
   pgdata, canonical/trailing-slash. Проверка за 2 минуты: `grep -c '_DONE' /usr/local/bin/wave-disk-janitor.sh`
   → ожидание непустого числа. Лечение: заменён; при откате — восстановить из backup
   `114d8593…`, но только осознанно.

### 5. ТЕКУЩИЙ ОСТАТОК (ответственный)
1. Карточка закрыта. Остатка по существу нет.
2. Постоянный контроль: сверять hash установленного дворника с принятым при каждом
   обновлении хоста — координатор.
3. Тело — привести в соответствие с закрытием этой дельтой (не переписывая старое).

### 6. ПЕРВЫЙ ШАГ НУЛЕВОГО АГЕНТА (без агентов)
Проверить, что на A1 стоит именно принятый дворник: `sha256sum /usr/local/bin/wave-disk-janitor.sh`
→ ожидание `cad66e9a…`. Совпало — карточка закрыта корректно, больше ничего не делать.
Не совпало — остановить таймер дворника и доложить координатору, не «дочиняя» файл вручную.
```

---
Воркер
не проверен 3333-wash-dispatcher A1 движение в панели: неизвестно
Подключиться и смотреть/перехватить руками. Колесо мыши листает; клавишами — Ctrl-b затем [, выход из прокрутки q. Отсоединиться — Ctrl-b затем d:
Прочитать историю панели без подключения — листается и ищется (/ поиск, q выход), воркеру не помешает:
Обновлён
2026-09-15T15:17:54.447Z