Files
ROMFASTSQL/proxmox/lxc171-claude-agent/discord-bridge/README.md
Claude Agent ed7eca4fc7 fix(discord): watchdog de gateway — procesul viu pe un gateway mort nu mai trece neobservat
Puntea a tacut ~7 ore fara ca nimic sa semnaleze: discord.py memoreaza
`resume_gateway_url` primit la ultimul READY si il refoloseste la fiecare
reconectare. Cand gatewayul regional (gateway-us-east-1a) a inceput sa dea
503 la handshake, botul a reincercat la infinit acelasi host mort, cu backoff
pana la ~15 minute. Procesul era viu, deci nici systemd nici dashboardul nu
aveau ce vedea, iar `gateway.discord.gg` raspundea normal tot timpul.

GatewayWatchdog numara de cat timp e legatura jos (on_connect / on_resumed /
on_ready o ridica, on_disconnect o coboara, iar reincercarile esuate nu
reseteaza ceasul). Peste `GATEWAY_WATCHDOG_S` (implicit 300s) alerteaza, iese
cu codul 3 si lasa systemd sa reporneasca — restartul e singurul lucru care
forteaza un IDENTIFY nou pe gateway.discord.gg. Un prag <= 0 il dezactiveaza.

O iesire la 300s ramane sub StartLimitBurst=5/5min, deci bucla de restart nu
poate ajunge sa lase unitul `failed`. `_hard_exit_after` acopera cazul in care
`close()` se blocheaza pe socketul mort.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
2026-09-01 07:37:38 +00:00

411 lines
22 KiB
Markdown

# Punte Discord -> Claude Code (LXC 171)
> **Care chatbot e care?** Exista trei lucruri numite „Maria", doi boti pe Discord si
> doua punti WhatsApp legate la acelasi numar. Inainte de a depana, citeste
> [`docs/chatboti-si-punti.md`](../../../docs/chatboti-si-punti.md) — spune care instanta raspunde de fapt.
Un bot Discord subtire care duce mesajele dintr-un guild privat catre CLI-ul `claude`
care ruleaza pe containerul de dezvoltare **LXC 171 (claude-agent, 10.0.20.171)**, si
aduce raspunsurile inapoi. Practic: acelasi Claude Code cu care lucrezi in terminal,
comandat de pe telefon.
Nu e un chatbot separat. Nu are memorie proprie, nu are baza de date proprie: sesiunile
sunt chiar sesiunile Claude Code din `~/.claude/projects/`, iar un fir de Discord este
o sesiune.
> Nu confunda cu **MoltBot** (LXC 110) sau cu OpenClaw — acelea sunt alti agenti, cu
> alt scop. Puntea asta ruleaza pe masina de dezvoltare si are accesul ei.
---
## Arhitectura
```
Discord (guild privat)
| on_message
v
bot.py -- allowlist (guild / canal / utilizator) [doar adaptor Discord]
|
+-- session_store.py state.json {thread_id: {sid, cwd, model, inflight, pid}}
+-- runner.py proces persistent per fir, alimentat pe stdin
+-- stream.py parser tolerant de JSONL
+-- render.py chunker + un loop de editare per canal
+-- limits.py max procese, timeout tur, rate limit, plafon de cost
+-- security/ hook PreToolUse (confirmari) + wrapper `infra`
+-- alerts.py alerte email [ops]
+-- cleanup.py procese lasate in urma (`/cleanup`) [ops]
|
v
claude -p --input-format stream-json --output-format stream-json --verbose
--resume <sid> --permission-mode bypassPermissions
--settings ~/.claude-discord/bot-settings.json --model sonnet --autocompact auto
```
Cateva alegeri care nu se vad din diagrama:
- **Proces persistent per fir**, nu unul per mesaj. Asta permite *steering* la mijlocul
turului: un mesaj trimis in timp ce Claude lucreaza ajunge la el si schimba raspunsul
(verificat: mesaj la 8s intr-un tur de 34.5s). Reaper la 20 min de inactivitate;
repornirea se face cu `--resume <sid>`, deci firul nu-si pierde contextul.
- **Un proces `claude` = ~406 MB RSS** (masurat). De aici toate limitele: maxim 4 procese
vii, `MemoryMax=6G` pe unit, si comanda `/cleanup`.
- **Model implicit `sonnet`.** Un tur banal pe opus a costat $0.1547 (masurat), deci
opus e optional, per fir, prin `/model model:opus`.
---
## Comenzi
| Comanda | Ce face |
|---------|---------|
| `/new` | Sesiune noua, curata, in firul curent |
| `/new fork:True` | Sesiune noua care porneste din contextul celei curente |
| `/cd cale:<cale>` | Schimba directorul de lucru al firului (ex. `/cd cale:/workspace/romfastsql`) |
| `/model model:<sonnet\|opus>` | Schimba modelul pentru firul curent |
| `/status` | Sesiune, director, model, fereastra de utilizare, cost cumulat, proces viu, ultimele linii de stderr |
| `/stop` | Opreste turul in desfasurare din firul curent |
| `/cleanup` | Listeaza procesele lasate in urma (rulare seaca). `/cleanup force:True` le opreste |
| `/permisiuni` | Ce s-a aprobat pentru tot firul. `/permisiuni revoca:True` sterge aprobarile |
| `/help` | Lista de mai sus, in fir |
Comenzile sunt **application commands** (`/`), inregistrate pe guild-urile din
`DISCORD_GUILD_IDS` la pornirea botului, deci apar in lista de comenzi a Discord. Vechiul
prefix `!` nu mai executa nimic: botul raspunde doar cu indiciul catre comanda `/`
echivalenta. Orice alt mesaj din canal pleaca la Claude ca prompt, ca inainte.
Un fir de Discord = o sesiune Claude. Canalul principal are si el sesiunea lui, cea
implicita. Subsolul fiecarui raspuns arata modelul, durata si costul.
### Atasamente
Pozele si fisierele text trimise pe Discord ajung la Claude in acelasi tur cu mesajul.
Un mesaj **doar** cu atasament, fara text, e valid — nu mai e respins ca gol. Merge si
mid-tur: o poza trimisa in timpul unui tur intra pe stdin ca steering, nu deschide tur nou.
| Tip | Ce se intampla | Limite |
|-----|----------------|--------|
| Imagini `png`, `jpeg`, `gif`, `webp` | Devin blocuri `image` — Claude le vede | max 4/mesaj, max 3,5 MB fiecare |
| Fisiere text (`.txt`, `.md`, `.log`, `.csv`, `.json`, `.sql`, `.py`, `.sh`, …) | Continutul e inserat in prompt | max 4/mesaj, max 100 KB fiecare (peste atat, trunchiat) |
| Orice altceva (PDF, Office, arhive, `svg`, `heic`) | Doar numit in prompt, cu motivul | — |
Nimic nu dispare tacut: ce n-a putut fi citit (prea mare, tip neacceptat, descarcare
esuata) apare la finalul promptului intr-o linie „Atasamente ignorate: …", deci Claude
stie ca ai trimis ceva si poate cere altceva. O imagine stricata nu anuleaza restul
mesajului.
Limita de 3,5 MB e pe octetii bruti fiindca base64 umfla cu ~4/3, iar API-ul refuza
imaginile peste ~5 MB codate. Marimea e verificata de doua ori: intai cea declarata de
Discord (ca sa nu descarcam degeaba), apoi cea reala, dupa descarcare.
### Despre `/cleanup`
`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 — ~440 MB bucata cu tot cu serverele MCP, pe un container cu istoric de OOM.
**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.
---
## Instalare
### Partea automata
```bash
cd /workspace/romfastsql/proxmox/lxc171-claude-agent/discord-bridge
./ops/install.sh
```
Scriptul e idempotent (poti sa-l rulezi de cate ori vrei) si face:
1. `~/.claude-discord/` cu drepturi `0700`, plus `logs/` si `approvals/`
2. `~/.claude-discord/env` cu `0600`, copiat din `ops/env.example` — **nu suprascrie
niciodata un env existent**
3. venv in `~/.claude-discord/venv` + dependintele din `requirements.txt`
4. `loginctl enable-linger claude` — fara asta serviciul de utilizator moare la logout
si nu porneste la boot
5. symlink `~/.config/systemd/user/claude-discord.service` -> `ops/claude-discord.service`,
apoi `daemon-reload` si `systemd-analyze verify`
6. intrare de crontab pentru `logrotate` (zilnic, 04:10)
Scriptul **nu porneste** serviciul. Dupa ce completezi env-ul:
```bash
./ops/install.sh --start
```
### Partea manuala (o faci tu, o singura data)
Nu se poate automatiza: cere un om logat in Discord.
1. **Creeaza aplicatia Discord.** https://discord.com/developers/applications ->
*New Application*. E o aplicatie **noua, dedicata** puntii — nu refolosi aplicatia
MoltBot/OpenClaw.
2. **Deschide sectiunea *Bot*.** Portalul creeaza user-ul bot odata cu aplicatia,
deci daca nu vezi un buton *Add Bot* e normal — intri direct in *Bot*.
3. **Ia token-ul.** *Bot* -> *Reset Token* -> copiaza. **Se arata o singura data.**
Il pui in `~/.claude-discord/env`, la `DISCORD_TOKEN=`. Fisierul e `0600` si nu e
versionat. Daca token-ul ajunge vreodata intr-un commit, reseteaza-l imediat din
portal — cine il are poate comanda infrastructura.
4. **Activeaza intents.** *Bot* -> *Privileged Gateway Intents* -> porneste
**MESSAGE CONTENT INTENT**. Fara el botul primeste mesajele goale si nu face nimic.
Dupa comutator apare jos bara **Save Changes** — daca nu o apesi, setarea NU se
salveaza (usor de ratat pe telefon). Reincarca pagina si confirma ca a ramas pornit.
Nu ai nevoie de aprobare de la Discord: verificarea e ceruta abia de la 100 de servere.
(*Server Members* si *Presence* nu sunt necesare — lasa-le oprite.)
5. **Invita botul intr-un guild PRIVAT** al tau. *OAuth2* -> *URL Generator* ->
scopes: `bot` **si** `applications.commands` -> permisiuni: *View Channel*, *Send Messages*, *Read Message History*,
*Create Public Threads*, *Send Messages in Threads*, *Attach Files*,
*Embed Links*, *Add Reactions*. Deschide URL-ul generat si alege serverul.
Bifele sunt greu de nimerit pe telefon; linkul echivalent, gata calculat
(`APPLICATION_ID` e in *General Information*):
```
https://discord.com/oauth2/authorize?client_id=APPLICATION_ID&scope=bot%20applications.commands&permissions=309237763136
```
`309237763136` = exact permisiunile de mai sus. Fara *View Channel* botul nu vede
canalul deloc, oricat de permis ar fi in allowlist. Fara scope-ul
`applications.commands` botul merge, dar inregistrarea comenzilor `/` esueaza cu
*403 Missing Access* (scrie in log linkul de reinvitare) si comenzile nu apar in lista.
Un bot deja invitat se re-invita cu acelasi link: se adauga doar scope-ul lipsa.
**Nu-l invita intr-un server cu alti oameni** — cine scrie in canalul permis
comanda direct containerul.
6. **Ia ID-urile pentru allowlist.** In Discord: *Settings* -> *Advanced* ->
**Developer Mode** pornit. Apoi click dreapta (pe telefon: apasare lunga) ->
*Copy Server ID* /
*Copy Channel ID* / *Copy User ID*. Le pui in `~/.claude-discord/env`:
`DISCORD_GUILD_IDS`, `DISCORD_CHANNEL_IDS`, `DISCORD_USER_IDS` (separate prin virgula).
**Allowlist gol = nimic permis** (fail-closed). Mesajele de la webhook-uri si de la
alti boti sunt ignorate din principiu.
7. **Pune destinatarul alertelor**: `ALERT_RECIPIENT=` in acelasi env.
8. **Verifica plafonul de cost**: `COST_CAP_USD_DAY=` (implicit `off`). Pe abonament
(Pro/Max) lasa `off`: costul raportat de CLI e doar pretul echivalent la API, nu o
factura. Pune un numar (ex. `5.00`) doar daca platesti per token.
9. Abia acum: `./ops/install.sh --start`.
---
## Operare
### Unde te uiti
```bash
systemctl --user status claude-discord # e viu?
journalctl --user -u claude-discord -n 200 # ce a facut ultima data
tail -f ~/.claude-discord/logs/bot.log # logul aplicatiei
tail -f ~/.claude-discord/logs/alerts.log # ce alerte s-au trimis / au esuat
systemctl --user restart claude-discord # repornire
```
Aceleasi lucruri, cu butoane, in **dashboard-ul de control**
([dashboard/README.md](dashboard/README.md)) — stare, restart, orfani, confirmari,
jurnale:
```
https://claude-agent.tailf7372d.ts.net/claude # din tailnet (ca /echo la moltbot)
# fara login: DASHBOARD_AUTH=off
```
Procesul e legat de `127.0.0.1:18790`; in tailnet il publica `tailscale serve`.
Fara Tailscale: `ssh -L 18790:127.0.0.1:18790 -N claude@10.0.20.171`.
Acelasi panou include si sectiunea **"Maria — WhatsApp + RAG"**, care controleaza
proiectul sibling `../maria-whatsapp-bridge/` (punte WhatsApp Baileys + consumer
RAG): start/stop/restart, cod QR de asociere, depozit de documente, reindexare
si sincronizare Google Drive. Decizie deliberata: un singur dashboard comun
pentru ambele punti de pe acest container, nu un panou separat per serviciu.
| Fisier | Ce e |
|--------|------|
| `~/.claude-discord/env` | token + allowlist + limite (0600) |
| `~/.claude-discord/state.json` | sesiuni, directoare, pid-uri, cost |
| `~/.claude-discord/logs/bot.log` | stdout/stderr al botului (rotit zilnic, 14 zile) |
| `~/.claude-discord/logs/alerts.log` | jurnalul alertelor |
| `~/.claude-discord/logs/dashboard.log` | stdout/stderr al dashboard-ului de control |
| `~/.claude-discord/alerts-dedup.json` | fereastra de dedup a alertelor |
| `~/.claude-discord/approvals/` | cereri de confirmare intre hook si bot |
### Cost
Costul se vede in trei locuri: in subsolul fiecarui raspuns (turul curent + cumulat pe
fir), in `/status`, si in `state.json` la cheia `cost`. E pretul echivalent la API
raportat de CLI: pe abonament nu se factureaza, deci ramane doar o masura de consum.
Plafonul zilnic e optional. `COST_CAP_USD_DAY=off` (implicit, si `0` sau gol) inseamna
fara plafon: cheltuiala se contorizeaza mai departe, dar nimic nu se opreste. Cu un
numar pozitiv, la atingerea lui botul nu mai accepta tururi noi si trimite email;
plafonul se reseteaza la schimbarea zilei.
### Alerte pe email
`alerts.py` urmeaza tiparul deja folosit in repo (vezi
`proxmox/vm109-windows-dr/scripts/pveelite-down-alert.sh`):
```
mail -s "[LEVEL] subiect" "$ALERT_RECIPIENT" # LEVEL: INFO | WARN | CRITICAL
```
Se trimite alerta pentru: proces mort neasteptat, crash loop, plafon de cost atins,
`state.json` corupt, orfani detectati la sweep.
Doua garantii care conteaza:
- **`alert()` nu arunca niciodata exceptii.** O alerta esuata nu are voie sa doboare
botul; orice eroare ajunge in `alerts.log` si atat.
- **Dedup 1h pe `dedup_key`**, persistat pe disc. Fara el, un crash loop ar trimite
sute de emailuri identice.
**Dependinta:** binarul `mail`. Pe LXC 171 e instalat pachetul **`bsd-mailx`**
(`/usr/bin/mail`), iar transportul e **postfix**, deja prezent (`/usr/sbin/sendmail`).
Pe o masina unde lipseste:
```bash
sudo apt-get install -y bsd-mailx
```
Daca `mail` lipseste, alertele **nu se pierd**: se degradeaza la scriere in
`~/.claude-discord/logs/alerts.log`, cu tot cu corpul mesajului, si `install.sh` te
avertizeaza la instalare. Dar nimeni nu mai primeste nimic pe email — deci trateaza
lipsa lui ca pe o defectiune, nu ca pe o optiune.
Test manual, fara sa pornesti botul:
```bash
ALERT_RECIPIENT=tu@romfast.ro python3 alerts.py INFO "test punte" "corp de test"
tail -2 ~/.claude-discord/logs/alerts.log
```
### Cand pica
| Simptom | Ce faci |
|---------|---------|
| Botul nu raspunde deloc in Discord | `systemctl --user status claude-discord`. Daca e `failed`, `journalctl --user -u claude-discord -n 100`. Cauza #1: token invalid sau **MESSAGE CONTENT INTENT** oprit. |
| Botul e viu dar tace, iar in log curg `WSServerHandshakeError: 503` | Gatewayul regional memorat ca `resume_gateway_url` a picat, iar discord.py il reincearca la infinit. Verifica: `curl -si --http1.1 -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' 'https://gateway.discord.gg/?v=10&encoding=json'` — un `101` acolo plus `503` pe hostul regional confirma. Watchdogul iese singur dupa `GATEWAY_WATCHDOG_S` (implicit 300s) si systemd reporneste; daca vrei mai repede, `systemctl --user restart claude-discord`. |
| Botul e viu dar ignora mesajele | Allowlist. Verifica `DISCORD_GUILD_IDS` / `DISCORD_CHANNEL_IDS` / `DISCORD_USER_IDS` din env. Respingerea e **tacuta**, intentionat. |
| Comenzile `/` nu apar in lista din Discord | Botul a fost invitat fara scope-ul `applications.commands`. In log: `sync de comenzi slash esuat` + linkul de reinvitare. Reinvita botul, apoi `systemctl --user restart claude-discord`. |
| Unitul se invarte in restart | Dupa 5 porniri esuate in 300s systemd renunta si lasa unitul `failed` (e voit). Repara, apoi `systemctl --user reset-failed claude-discord && systemctl --user start claude-discord`. |
| Firul e blocat pe hourglass | Botul a fost restartat la mijlocul unui tur. Turul **nu** se reia automat (risc de dubla executie sub `bypassPermissions`); sweep-ul de la pornire pune un avertisment in fir. Trimite mesajul din nou. |
| Memoria containerului creste | `/cleanup` (sec), apoi `/cleanup force:True`. Vezi si `systemctl --user show claude-discord -p MemoryCurrent`. |
| „Plafon de cost atins" | E limita zilnica, nu o eroare. Pe abonament pune `COST_CAP_USD_DAY=off` in env si reporneste; altfel ridica valoarea sau asteapta ziua urmatoare. |
| Un tur pare inghetat | Mesajul live arata `⏳ ruleaza de 2m30s` sub unealta curenta (din `tool_progress`); cat timp numarul creste, turul lucreaza. Daca sta pe loc, `/stop`. |
| „Limita de utilizare atinsa" in `/status` | Fereastra abonamentului (`rate_limit_event`), nu plafonul de cost. Randul `utilizare` arata tipul si ora resetarii; nu se poate ocoli, se asteapta. |
| Nu vin emailuri de alerta | `command -v mail`; `mailq`; `tail ~/.claude-discord/logs/alerts.log`. Un `NESENT` in log iti spune exact de ce. |
| Dupa reboot serviciul nu porneste | `loginctl show-user claude -p Linger` trebuie sa fie `yes`. Daca nu: `sudo loginctl enable-linger claude`. |
### Teste
```bash
cd /workspace/romfastsql/proxmox/lxc171-claude-agent/discord-bridge
python3 -m pytest -q # suita rapida, fara retea si fara Discord
python3 -m pytest -m e2e # testele care ating CLI-ul real (lente)
```
Testele de ops (`tests/test_alerts.py`, `tests/test_cleanup.py`) nu trimit email real si
nu omoara procese reale: folosesc un `mail` fals si copii de `sleep` pe care le pornesc
si le opresc ele insele.
---
## Securitate — ce e si ce nu e
Puntea ruleaza ca utilizatorul `claude`, cu `--permission-mode bypassPermissions`, si are
**exact accesul pe care il are omul in terminal**: `/workspace`, cheile SSH, nodurile
Proxmox, LXC-urile, VM-urile. Asta e **functionalitate ceruta**, nu scapare — puntea
exista tocmai ca sa poti administra infrastructura de pe telefon.
Ce apara efectiv:
1. **Control de acces pe canal.** Guild + canal + utilizator pe allowlist, gol = nimic
permis. Webhook-urile si botii sunt respinsi. Contul tau de Discord devine, practic,
o cheie de infrastructura — pune-i 2FA.
2. **Confirmare pentru operatiuni ireversibile.** Un hook `PreToolUse` opreste comanda si
posteaza butoane in fir; fara raspuns in fereastra de timp raspunsul e **deny**
(fail-closed). Verificat: a blocat un `rm -rf`, a asteptat aprobarea externa 20s si a
permis apoi executia, fara timeout. Butonul **Allow (tot firul)** memoreaza tiparul
`(regula, motiv)` — nu comanda — pentru firul curent, ca o sesiune care lucreaza pe
acelasi host sa nu ceara zece confirmari identice; `/new`, `/permisiuni revoca:True`
si TTL-ul de 12h il sterg. Detalii in [`security/README.md`](security/README.md).
3. **Wrapper `infra`** cu lista explicita de hosturi + token Proxmox cu ACL.
Ce **nu** apara: regulile `deny` din settings. Sub `bypassPermissions` ele sunt un strat
cosmetic — verificat, `/usr/bin/ssh -V` si `bash -c "ssh -V"` trec pe langa ele. Nu te
baza pe ele ca pe o bariera.
---
## Limitari cunoscute (asumate)
- **Nu exista jurnal de audit independent.** Stratul de audit append-only pe branch
dedicat a fost considerat si **respins constient**. Consecinta, asumata: la o problema
— o comanda distructiva care a trecut, o modificare pe care nimeni nu si-o aminteste —
**nu exista o inregistrare independenta care sa spuna ce s-a intamplat, cand si pe ce
host**. Ce ramane sunt loguri care pot fi sterse de chiar procesul care le scrie:
`bot.log`, `journalctl`, jurnalele de sesiune din `~/.claude/projects/` si istoricul
git al repo-urilor atinse. Daca vreodata conteaza „cine si ce", asta e golul de
acoperit primul.
- **Fara reluare automata a turului pierdut.** Un restart la mijlocul unui tur pierde
turul; nu se reia singur, fiindca sub `bypassPermissions` jumatate din comenzi sunt
deja executate si o reluare le-ar rula a doua oara.
- **Rate limit-ul Discord e o degradare tacuta.** Intervalul de editare se adapteaza
(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.** 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.
- **Fara voce.** Mesajele audio nu ajung la Claude. Imaginile si fisierele text
ajung (vezi „Atasamente" mai sus); PDF, Office, arhive si celelalte tipuri sunt
doar numite in prompt, nu citite.
- **Fara dashboard web** — exista deja pe MoltBot, puntea nu-l duplica.
- **Un singur container.** Daca LXC 171 e oprit, puntea e oprita. Nu are redundanta si
nu e in HA.
---
## Fisiere
| Fisier | Ce e | Lane |
|--------|------|------|
| `bot.py` | adaptorul Discord: allowlist, comenzi, butoane, atasamente | A |
| `session_store.py` | `state.json`: scriere atomica, lock per fir, PID reuse | A |
| `runner.py` | proces persistent per fir, stdin JSONL, reaper | A |
| `stream.py` | parser tolerant de stream JSONL | A |
| `render.py` | chunker + loop de editare per canal | A |
| `limits.py` | max procese, timeout, rate limit, plafon de cost | A |
| `commands_slash.py` | declararea si inregistrarea comenzilor `/` | A |
| `config.py` | citeste `~/.claude-discord/env` | A |
| `security/confirm_hook.py` | hook `PreToolUse`, fail-closed | B |
| `security/approvals.py` | canal de aprobari hook <-> bot | B |
| `security/infra` | wrapper cu lista de hosturi permise | B |
| `alerts.py` | alerte email, dedup 1h, nu arunca niciodata | C |
| `cleanup.py` | `/cleanup`: orfani, dry-run implicit | C |
| `ops/claude-discord.service` | unit systemd de utilizator | C |
| `ops/install.sh` | instalare idempotenta | C |
| `ops/env.example` | sablon de configurare | C |
| `ops/logrotate.conf` | rotatia logurilor | C |
| `dashboard/api.py` | dashboard de control (stare, restart, orfani, confirmari) | C |
| `dashboard/index.html`, `login.html`, `static/` | interfata dashboard-ului | C |
| `dashboard/claude-discord-dashboard.service` | unit systemd pentru dashboard | C |
| `INTERFACES.md` | contractul intre module | orchestrator |