fix(discord-bridge): /cleanup urmareste descendentii, dar STRICT in cgroup-ul serviciului
Doua defecte, al doilea gasit la verificarea primului.
1. Procesul `claude` al unui fir isi porneste serverele MCP (npm/sh/node pentru
playwright), care nu se numesc `claude` si scapau cautarii. Masurat pe firul viu:
claude 300 MB + MCP 137 MB = ~440 MB per fir, nu 300. find_orphans merge acum pe
arbore, iar kill_orphans opreste copiii inaintea parintilor (altfel se reparenteaza
si scapa).
2. PERICULOS: definitia initiala a orfanului ("orice `claude` absent din state.json")
prindea sesiunile Claude interactive de pe container. Rularea seaca propunea 25 de
procese / ~3864 MB — sesiunile de lucru din tmux, inclusiv cea din care ar fi fost
data comanda. `/cleanup force:True` din Discord si-ar fi omorat propria sesiune.
Apartenenta la cgroup-ul `claude-discord.service` devine conditie necesara pentru
toate familiile, fail-closed la cgroup necitibil. Dupa fix: zero procese raportate.
Test de regresie pe instantaneul real (doua sesiuni in tmux-spawn-*.scope, una in
claude-discord.service): se raporteaza doar a treia. Verificat prin mutant ca testul
musca — cu filtrul scos pica 3 teste.
3. INTERFACES.md cerea pid_start_time in ticks, dar session_store scrie secunde (si
state.json viu contine secunde). Contractul era imprecis, nu codul: un consumator
care compara ticks nu s-ar potrivi niciodata, iar cum acea comparatie protejeaza
turul in desfasurare, esecul ar fi fost tacut si ar fi facut eligibil exact ce
trebuia protejat. Documentat, cu avertismentul explicit.
Bug colateral prins de agent: raportul cu rezultate de omorare ajungea la 2289 caractere,
peste limita Discord — mesajul ar fi fost respins exact la `/cleanup force:True`. Buget
de caractere adaugat.
Suita: 322 passed cu discord.py, 319 passed + 3 skipped fara. Rulare seaca pe container: curat.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7
This commit is contained in:
@@ -75,14 +75,26 @@ implicita. Subsolul fiecarui raspuns arata modelul, durata si costul.
|
||||
|
||||
`KillMode=control-group` opreste arborele serviciului la restart, dar **nu prinde ce s-a
|
||||
desprins**: un server pornit cu `&` intr-un tur, un `nohup`, un job lung reparentat la
|
||||
init. Alea raman si se aduna — 406 MB bucata, pe un container cu istoric de OOM.
|
||||
init. Alea raman si se aduna — ~440 MB bucata cu tot cu serverele MCP, pe un container cu istoric de OOM.
|
||||
|
||||
`/cleanup` cauta doua feluri de resturi: procese `claude` care nu apar in `state.json`,
|
||||
si copii reparentati la init ramasi in cgroup-ul serviciului. **Ruleaza sec (dry-run) in
|
||||
mod implicit** — intai vezi lista, apoi decizi. Ce e inregistrat in `state.json` si toti
|
||||
descendentii acelor procese (adica turul care ruleaza chiar acum) nu sunt niciodata
|
||||
atinse, iar potrivirea se face si pe `pid_start_time`, ca un PID reciclat sa nu duca la
|
||||
omorarea altui proces.
|
||||
**DOMENIUL e cgroup-ul serviciului `claude-discord.service` si numai el.** Pe LXC 171
|
||||
ruleaza permanent sesiuni Claude interactive (tmux, ttyd, agenti) care n-au nicio legatura
|
||||
cu puntea; acelea stau in `tmux-spawn-*.scope` si NU sunt raportate niciodata, nici macar
|
||||
in rularea seaca. Regula e fail-closed: daca cgroup-ul unui proces nu poate fi citit,
|
||||
procesul e considerat neeligibil — mai bine ratam un orfan decat sa oprim sesiunea cuiva.
|
||||
Fara aceasta limitare, `/cleanup force:True` dat din Discord si-ar opri propria sesiune
|
||||
impreuna cu tot ce ruleaza omul in tmux.
|
||||
|
||||
Inauntrul cgroup-ului, e orfan orice proces neprotejat care nu se leaga de `state.json`,
|
||||
indiferent cum se numeste — asa raman prinse si serverele MCP pornite de `claude`
|
||||
(`npm exec @playwright/mcp`, `node ...`), care altfel ar scapa fiindca nu se cheama
|
||||
`claude`. Un fir costa in jur de 440 MB cu tot cu MCP, nu 300.
|
||||
|
||||
**Ruleaza sec (dry-run) in mod implicit** — intai vezi lista, apoi decizi. Ce e inregistrat
|
||||
in `state.json` si toti descendentii acelor procese (adica turul care ruleaza chiar acum)
|
||||
nu sunt niciodata atinse, iar potrivirea se face si pe `pid_start_time`, ca un PID reciclat
|
||||
sa nu duca la oprirea altui proces. Copiii se opresc inaintea parintilor, altfel s-ar
|
||||
reparenta si ar scapa.
|
||||
|
||||
---
|
||||
|
||||
@@ -299,9 +311,9 @@ baza pe ele ca pe o bariera.
|
||||
(1s -> 5s), dar cand Discord franeaza nu apare niciun mesaj: raspunsul doar apare mai
|
||||
incet. E singura cale fara test din analiza modurilor de esec — acceptata, fiindca
|
||||
esecul e intarziere, nu pierdere.
|
||||
- **`/cleanup` nu e infailibil.** Prinde procese `claude` neinregistrate si copii
|
||||
reparentati la init ramasi in cgroup. Un proces care a iesit din cgroup *si* nu arata
|
||||
a `claude` (un `python -m http.server` desprins complet, de exemplu) ii scapa.
|
||||
- **`/cleanup` nu e infailibil.** Domeniul lui e strict cgroup-ul serviciului, deci un
|
||||
proces care a iesit complet din cgroup ii scapa — asta e pretul deliberat platit ca sa nu
|
||||
atinga niciodata sesiunile interactive de pe container.
|
||||
Lista `NEVER_KILL` din `cleanup.py` protejeaza infrastructura sesiunii (systemd, sshd,
|
||||
tmux, code-server) — deci un proces cu un asemenea nume in linia de comanda nu va fi
|
||||
oprit niciodata, chiar daca e orfan.
|
||||
|
||||
Reference in New Issue
Block a user