chore: sterge handoff-urile temporare CONTEXT_HANDOVER_*

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oz3nLpbEDB1J8uyrFkxTA
This commit is contained in:
Marius
2026-10-01 17:14:20 +03:00
parent 15ab3a6029
commit 02c7eb2e33
4 changed files with 0 additions and 643 deletions

View File

@@ -1,198 +0,0 @@
# CONTEXT HANDOVER — Punte Discord → Claude Code (LXC 171)
**Data:** 2026-08-30 13:00 UTC · **Stare:** LIVRATA, in productie, pe `master` (impins)
**Commit curent:** `c8a5d41` · **Repo:** romfast/ROMFASTSQL · **Branch:** `master`
Nu e un task intrerupt. Puntea functioneaza si e folosita. Documentul asta exista ca o
sesiune noua sa poata prelua operarea si continuarea fara sa redescopere totul.
---
## 1. Ce e
Bot Discord care comanda LXC 171 (si, prin el, infrastructura) dintr-un canal privat, de pe
telefon. Bot subtire `discord.py` peste CLI-ul `claude`, cu **proces persistent per fir**
alimentat pe stdin cu `--input-format stream-json` — de aceea un mesaj trimis in timpul unui
tur functioneaza ca **steering**, nu deschide tur nou.
Plan sursa: `~/.gstack/projects/romfast-ROMFASTSQL/claude-master-plan-discord-bridge-20260830.md`
(15 taskuri, toate facute). Executat cu 5 agenti in 3 lane-uri paralele.
**Cod:** `proxmox/lxc171-claude-agent/discord-bridge/`
**Documentatie:** `discord-bridge/README.md` (operare completa), `discord-bridge/INTERFACES.md`
(contractul intre module — de citit inainte de orice modificare), `discord-bridge/security/README.md`.
---
## 2. Stare de rulare (verificata la 12:58 UTC)
| | |
|---|---|
| Serviciu | `systemctl --user status claude-discord` → **active**, `NRestarts=0` |
| Bot | `ClaudeAgent#7891`, app id `1543576449624186880` |
| Guild | Romfast `1430476698264145922` |
| Canal | **privat** `#claude-agent` `1543581063496859679` |
| User permis | `949388626146517022` (marius.mutu) |
| Director de lucru | `/workspace/claude-agent/` (repo git propriu) |
| Model | `sonnet` (default), `/model opus` per fir |
| Cost ziua curenta | $1.5456 (plafon `COST_CAP_USD_DAY=5.00`) |
| Comenzi slash | 7, inregistrate pe guild |
**Stare pe disc:** `~/.claude-discord/` — `env` (0600, token+allowlist), `state.json`,
`bot-settings.json` (hook), `approvals/`, `logs/`, `venv/`.
Cod versionat in repo; secretele NU.
### Comenzi (slash, nu prefix `!` — migrat)
`/new [fork]` · `/cd <cale>` · `/model <sonnet|opus>` · `/status` · `/stop` · `/cleanup [force]` · `/help`
Mesajele obisnuite (fara comanda) merg la Claude ca prompt.
---
## 3. Ce s-a verificat EMPIRIC in productie (nu doar teste)
Astea sunt afirmatii cu dovada, nu presupuneri:
| Ce | Dovada |
|---|---|
| Steering mid-tur | mesaj la 6s intr-un tool call de 25s a schimbat raspunsul final (ALFA→BETA), CLI 2.1.251 |
| Comenzi slash | 7 citite inapoi din API-ul Discord, nu doar din log |
| Confirmare operatiuni ireversibile | `12:24:25 PENDING` → butoane in canal → `12:24:39 ALLOW` → `/tmp/test-punte` sters |
| Fail-closed | acelasi `rm -rf` fara aprobare → `DENY` la timeout, directorul intact |
| Proces persistent | pid viu la 2 min dupa incheierea turului (reaper la 20 min) |
| `--resume` peste restart | aceeasi sesiune reluata dupa ce restartul a omorat procesul |
| `KillMode=control-group` | restart mid-sesiune, zero orfani |
| Parser tolerant | `rate_limit_event` aparut real in stream, ignorat cu un singur log |
| `/cleanup` dupa fix | rulare seaca pe container: „Niciun proces orfan" |
| Suita | **322 passed** cu discord.py, **319 passed + 3 skipped** fara |
Rulare teste: `cd proxmox/lxc171-claude-agent/discord-bridge && python3 -m pytest`
E2E (CLI real, ~40s, costa): `pytest -m e2e`
---
## 4. Capcane gasite pe parcurs — NU le reintroduce
Astea au costat timp si unele erau periculoase. Sunt fixate; sunt scrise aici ca sa nu
fie „simplificate" inapoi.
1. **`/cleanup` omora sesiunile de lucru.** Definitia initiala („orice proces `claude`
absent din `state.json`") prindea sesiunile Claude interactive de pe container:
rularea seaca propunea 25 de procese / 3864 MB, inclusiv sesiunea din care ar fi fost
data comanda. **Apartenenta la cgroup-ul `claude-discord.service` e conditie NECESARA
pentru toate familiile**, fail-closed la cgroup necitibil. Sesiunile de lucru stau in
`tmux-spawn-*.scope`. Exista test de regresie pe instantaneul real; verificat prin
mutant ca musca (fara filtru pica 3 teste).
2. **`pid_start_time` e in SECUNDE**, nu ticks (`session_store.py` converteste; `state.json`
viu contine secunde). Un consumator care compara ticks nu se potriveste niciodata — si
cum acea comparatie protejeaza turul in desfasurare, esecul e tacut si face eligibil
pentru omorare exact ce trebuia protejat. Documentat in `INTERFACES.md`.
3. **`/proc/uptime` e virtualizat de lxcfs, `starttime` nu.** Calculul naiv al varstei dadea
`4123168064` pentru un proces de 10 minute. Se foloseste `btime` din `/proc/stat`.
4. **Utilizatorul containerului se numeste `claude`**, deci potrivirea pe subsirul `/claude`
marca orice proces din home-ul lui. Potrivire pe basename per argument.
5. **Log dublat:** unitul redirecteaza stdout in `bot.log` (`StandardOutput=append:`) SI
codul avea `FileHandler` pe acelasi fisier. `FileHandler` ramane doar la rulare manuala
(`INVOCATION_ID` absent).
6. **`View Channel` lipsea** din permisiunile de invitatie — fara ea botul nu vede canalul,
oricat de permis ar fi in allowlist. Link corect (`permissions=309237763136`):
`https://discord.com/oauth2/authorize?client_id=1543576449624186880&scope=bot%20applications.commands&permissions=309237763136`
7. **Comenzile slash au nevoie de scope `applications.commands`** (nu doar `bot`) si de
**sync pe guild** (instantaneu; global ~1h). Fiecare comanda face `defer()` — altfel
Discord marcheaza interactiunea esuata dupa 3s desi comanda a rulat.
8. **`~/bin` trebuie sa fie in PATH-ul unitului**, altfel wrapperul `infra` e prezent pe
disc dar negasibil de bot (stratul 4 de securitate inert).
9. **Raportul `/cleanup`** depasea 2000 de caractere cu rezultatele atasate → mesaj respins
de Discord exact la `force:True`. Buget de caractere `MAX_REPORT_CHARS=1800`.
10. **Discord API respinge User-Agent-ul implicit al `urllib`** cu `error code: 1010`
(Cloudflare). Arata identic cu un refuz de permisiuni. Pune un User-Agent real.
---
## 5. Decizii de arhitectura — luate deliberat, NU le redeschide
Sunt in plan si au fost reconfirmate de utilizator. Un agent nou care le „descopera" ca
probleme pierde timp si risca sa strice lucruri.
- **`--permission-mode bypassPermissions` e intentionat.** Regulile `deny` sunt strat
cosmetic, NU bariera — verificat: `/usr/bin/ssh -V` si `bash -c "ssh -V"` trec pe langa.
Bariera reala e hook-ul PreToolUse.
- **Accesul larg la infrastructura si la `/workspace` e FUNCTIONALITATE ceruta**, nu bug.
De aceea: fara user separat `cdbot`, fara allowlist de proiecte pentru `/cd`.
- **Fara audit append-only** (stratul 3), respins constient. **Consecinta asumata:** la o
problema nu exista jurnal independent; logurile pot fi sterse de chiar procesul care le
scrie. Hook-ul si botul ruleaza sub acelasi user, deci un agent isi poate scrie singur
`"status":"allow"`.
- **Fara reluare automata a turului pierdut** — risc de dubla executie sub bypassPermissions.
- Fara Agent SDK, fara dashboard web, fara user separat, fara test golden pe JSONL inregistrat.
---
## 6. CE A RAMAS DE FACUT
### 6.1 Token Proxmox cu ACL (singurul lucru neterminat)
**Nu e conditie de functionare.** Puntea merge acum folosind cheile SSH root deja prezente
pe container — adica orice tur are drepturi depline pe cluster. Tokenul restrange asta.
Comenzile exacte sunt in `discord-bridge/security/README.md`. Pe scurt, ca root pe `pvemini`:
```
pveum user add claude-bridge@pve
pveum role add ClaudeBridge -privs "Datastore.Audit,Sys.Audit,Sys.Console,Sys.Syslog,VM.Audit,VM.Monitor,VM.Console,VM.PowerMgmt"
pveum acl modify /
pveum user token add claude-bridge@pve discord --privsep 1
pveum acl modify / --tokens 'claude-bridge@pve!discord'
```
`VM.Allocate` (creare/distrugere guest) e lasat afara INTENTIONAT.
Apoi: secretul (afisat o singura data) in `~/.claude-discord/env` ca `PVE_TOKEN_ID` /
`PVE_TOKEN_SECRET`, `chmod 600`. De decis daca ACL ramane pe `/` sau se restrange la un
subset de guest-uri.
### 6.2 Idei ramase, neprioritizate
- Atasamente Discord → Claude (download in /tmp + cale in prompt) — era v1.1 in plan.
- Rate limit-ul Discord la render e singura degradare tacuta ramasa: are tratare (interval
adaptiv 1s→5s), nu are test. Acceptat, fiindca esecul e intarziere, nu pierdere.
- `/cleanup` nu prinde procese iesite complet din cgroup — pret deliberat, ca sa nu atinga
niciodata sesiunile interactive.
---
## 7. Fisiere cheie
| Fisier | De ce conteaza |
|---|---|
| `discord-bridge/INTERFACES.md` | contractul intre module + proprietatea pe fisiere. **De citit primul.** |
| `discord-bridge/README.md` | operare, instalare, depanare, limitari oneste |
| `discord-bridge/runner.py` | proces persistent, reaper, respawn `--resume` |
| `discord-bridge/session_store.py` | state.json atomic, lock per fir, PID reuse |
| `discord-bridge/cleanup.py` | vezi capcana #1 — cea mai periculoasa zona |
| `discord-bridge/security/confirm_hook.py` | hook PreToolUse, fail-closed |
| `discord-bridge/security/infra` | wrapper cu lista explicita de hosturi (exit 3 la host necunoscut) |
| `discord-bridge/ops/claude-discord.service` | `KillMode=control-group`, MemoryHigh 3G / Max 6G, PATH |
| `~/.gstack/.../claude-master-plan-discord-bridge-20260830.md` | planul original, cu justificarile |
## 8. Comenzi de operare
```bash
systemctl --user status claude-discord
journalctl --user -u claude-discord -n 200
tail -f ~/.claude-discord/logs/bot.log
tail -f ~/.claude-discord/logs/confirm_hook.log # cine a cerut/primit aprobare
cd proxmox/lxc171-claude-agent/discord-bridge && python3 -m pytest
python3 cleanup.py # rulare seaca, sigura
```
**Memorie:** un fir costa ~440 MB (claude 300 + servere MCP 137). Plafon 4 procese → ~1,8 GB,
sub `MemoryHigh=3G`. Containerul are 16 GB.

View File

@@ -1,127 +0,0 @@
# Context handover — 2026-08-31
Sesiune lungă, patru fire de lucru terminate și unul în curs. Totul e comis și împins.
> **Infrastructura e deja documentată în acest repo** — nu o repet aici. Punctele de
> intrare: [`CLAUDE.md`](CLAUDE.md), [`proxmox/README.md`](proxmox/README.md) și, pentru
> chatboți, [`docs/chatboti-si-punti.md`](docs/chatboti-si-punti.md) (scris azi).
> Documentul ăsta e doar despre **ce s-a schimbat azi și ce a rămas**.
---
## 1. Ce rulează acum (starea la finalul sesiunii)
| | Stare |
|---|---|
| **Maria (WhatsApp + RAG)** | **LXC 171**, asociată la +40723197939, funcțională. 11 documente, sincronizare Drive la 10 min. |
| Prototipul Maria de pe LXC 104 | **oprit și dezactivat**. `llama-qwen35.service` (LLM-ul, port 8091) și `flowise.service` rămân pornite acolo. |
| Puntea Discord | activă, cu atașamente (imagini + fișiere text). |
| Reindexare RAG | **era în curs la finalul sesiunii** — vezi „De verificat întâi" mai jos. |
## 2. De verificat întâi, la reluare
Reindexarea pornită după corectarea `efactura-erori-rag.xml` s-ar putea să nu fi terminat.
```bash
pgrep -af "sync.py" # gol = gata
ls -la ~/.maria-bridge/rag_index.json # mtime recent = reconstruit
python3 -c "
import json,collections
e=json.load(open('/home/claude/.maria-bridge/rag_index.json'))
c=collections.Counter(x['source'] for x in e)
print(len(e),'chunk-uri,',len(c),'documente')
print(c.get('efactura-erori-rag.xml'),'din efactura-erori-rag.xml (8 = structurat corect, ~41 = inca text simplu)')
"
```
O reindexare completă durează **~18–21 de minute** (embedding CPU-only, ~7,3 s per chunk).
Nu e blocată — chiar durează atât. Dacă a eșuat, se reia cu:
`cd proxmox/lxc171-claude-agent/maria-whatsapp-bridge/rag && MARIA_BRIDGE_DIR=$HOME/.maria-bridge ~/.maria-bridge/venv/bin/python sync.py --force`
## 3. Ce s-a făcut azi
### Git și Gitea
- **`romfast/workspace` dădea 500** de pe 2026-04-06: patru gitlink-uri fără `.gitmodules`
făceau Gitea 1.24.3 să dea panic pe listarea rădăcinii. Scoase din index. **Nu era vina
botului**, cum se bănuia inițial.
- **`/workspace/claude-agent` e acum repo propriu** (`git@gitea.romfast.ro:romfast/claude-agent.git`),
scos din repo-ul `workspace` unde fusese comis din greșeală.
### Memorie și mod de lucru al botului Discord
- `~/.claude/projects/-workspace-claude-agent/memory` e **symlink** către memoria lui
`-workspace-romfastsql`. Firele Discord porneau amnezice fiindcă memoria e per director
de proiect. Copia veche: `memory.bak-2026-08-31`.
- **Regula nouă:** orice cerere de dezvoltare → citește `claude-agent/TODO.md`, planifică
cu `Agent(subagent_type: "Plan", model: "opus")`, execută pe Sonnet, actualizează
`TODO.md`. Scrisă în `claude-agent/CLAUDE.md` + `claude-agent/docs/flux-dezvoltare.md`.
Testată pe sesiuni reale: prima versiune era prea blândă și agentul sărea peste plan la
sarcini mici — întărită explicit cu „mărimea nu e criteriu".
- **`claude-agent/TODO.md`** e registrul viu al funcționalităților. Nimic blocat acum.
### Puntea Discord — atașamente
Imagini (png/jpeg/gif/webp, max 4, max 3,5 MB brut) → blocuri `image`; fișiere text →
inserate în prompt (max 4, trunchiate la 100 KB); restul doar numite. Merge și mid-tur ca
steering. 31 de teste noi; suita: 426 pass.
### Maria — mutare completă pe LXC 171
- **Sincronizare Drive** prin `rclone authorize` (fără cont de serviciu). Procedura
completă: [`docs/rclone-google-drive-headless.md`](docs/rclone-google-drive-headless.md).
Script: `maria-whatsapp-bridge/ops/setup-drive.sh`.
- **`.xml` acceptat și preferat peste `.md`** la același nume de bază; chunking per
problemă (un `<eroare_x>` = un chunk, cu mesajul și rezolvarea împreună).
- **Lacăt `flock` între reindexări** + scriere atomică a indexului. Descoperit când
timer-ul a pornit peste o rulare manuală și două procese scriau același fișier.
- **Fix LID**: după asociere nu răspundea nimic. WhatsApp livrează self-chat-ul ca
`51947713372214@lid`, iar filtrul compara doar cu `<număr>@s.whatsapp.net` — arunca tot,
tăcut. Reparat; mesajele respinse se loghează acum cu motivul.
- 26 de teste noi (prima suită a proiectului Maria).
### Documentație
- **[`docs/chatboti-si-punti.md`](docs/chatboti-si-punti.md)** — care chatbot e care.
Trei lucruri numite „Maria", doi boți pe Discord, două punți WhatsApp pe **același
număr**. A costat o oră de diagnostic azi: Maria răspundea de pe LXC 104, iar toate
verificările se făceau pe 171.
- **[`proxmox/lxc301-docker-template/README.md`](proxmox/lxc301-docker-template/README.md)** —
singurul guest nedocumentat. E un template Proxmox, nu un container oprit.
## 4. Ce a rămas de făcut
### a) LXC 301 — nimic urgent (corectat 2026-08-31)
Versiunea inițială a acestei secțiuni descria un conflict de IP activ cu VM 109. **Greșit:**
301 e marcat `template: 1`, deci nu poate fi pornit deloc și `onboot: 1` e ignorat. Nu
revendică 10.0.20.37 la reboot-ul lui pveelite, deci `pct set 301 -onboot 0` nu e necesar.
Verificat tot atunci: `basevol-301-disk-0@__base__` nu are niciun clon, deci `pct destroy 301`
ar fi sigur (~916 MB pe local-zfs). Opțional, la decizia utilizatorului — nu e urgent.
Singurul caz în care IP-ul redevine o problemă: dacă cineva convertește template-ul înapoi
în container. Atunci `-onboot 0` sau alt IP **înainte** de pornire.
### b) Auditul de infrastructură, dacă se dorește
Coverage-ul e bun — LXC 301 era singurul guest fără pagină proprie. Ce **nu** s-a făcut:
verificarea sistematică a fiecărui README existent față de starea live. Divergențele
găsite azi (prototipul Maria pe 104, LLM-ul pe 8091, Gitea rupt de 5 luni) au ieșit la
iveală accidental, nu dintr-un audit.
### c) Model de răspuns al Mariei
Rămâne `Qwen3.5-2B-Q4` pe LXC 104 — decizia utilizatorului a fost „păstrez deocamdată, cu
RAG-ul complet". Răspunsurile factuale (e-Factura) sunt acum corecte și ancorate în
documente; cele explicative sunt încă vagi. De reevaluat după câteva întrebări reale.
## 5. Capcane de reținut
- **Verifică întâi pe ce container ești.** `8099` și `11434` există și pe 104 și pe 171,
cu conținut diferit. Nu repeta ora pierdută azi.
- **Nu reporni prototipul de pe 104.** Ar răspunde în paralel, pe același număr, cu un
index de 24 de chunk-uri.
- **`rclone sync` e distructiv pe destinație.** Depozitul Mariei e oglindă a Drive-ului;
un document adăugat manual din dashboard dispare la următoarea sincronizare.
- **Maria nu are voie la infrastructură** — e chatbot pentru clienții ERP ROA. Nimic din
`proxmox/`, niciun IP sau credențial în depozitul ei. Regula e în `claude-agent/CLAUDE.md`.
- **Nu tăia ieșirea comenzilor de diagnostic cu `head`.** Concluzia „nu ascultă nimic pe
8091" a fost greșită exact din cauza asta și a trimis diagnosticul pe pistă falsă.
## 6. Comiteri
`ROMFASTSQL`: `b29b9f2..824b215` (14 comiteri) · `claude-agent`: `6d0bfe1..c97ba0c` (9 comiteri)
· `workspace`: `d24dca3`. Toate împinse pe gitea.romfast.ro.

View File

@@ -1,164 +0,0 @@
# Context handover — 2026-08-31, seara
Continuarea sesiunii din [`CONTEXT_HANDOVER_20260831.md`](CONTEXT_HANDOVER_20260831.md).
Tot ce e mai jos e comis și împins (`ROMFASTSQL`: `b447251..c514818`, `claude-agent`: două
comiteri noi).
---
## 1. Întrebarea care a declanșat handover-ul
> *„nu vreau ca reindexarea să refacă toate embeddings-urile de fiecare dată. de ce le face?"*
**Nu le mai face.** Reparat în `c514818` (parte din commit-ul `ca6b53e`), verificat pe viu:
```
[indexer] 12 documente, 169 chunk-uri (0 embeddings noi, 169 refolosite)
real 0.15s # înainte: ~20 de minute
```
### De ce le refăcea
`rag/indexer.py:build()` era scris ca o reconstrucție totală: pentru fiecare document, pentru
fiecare chunk, `embed(chunk)` — necondiționat. Nu exista nicio noțiune de „chunk-ul ăsta e
neschimbat". Indexul vechi era pur și simplu suprascris.
Costul: un embedding pe CPU (nomic-embed-text, fără GPU pe LXC 171) durează **~7 secunde**.
La 169 de chunk-uri asta înseamnă ~20 de minute — plătiți integral chiar și când se schimba
un singur document. Adăugarea dicționarului de erori Oracle (29 de chunk-uri noi) ar fi
costat 20 de minute în loc de 3.
Nu era o scăpare evidentă la citirea codului: `sync.py` avea deja o optimizare care *arăta*
ca și cum ar rezolva problema — amprenta pe nume+mtime+mărime, care sărea reindexarea când
nimic nu se schimbase. Dar aia decide **dacă** se reindexează, nu **cât** se reindexează.
Odată ce un singur fișier se atingea, se plătea totul.
### Cum se rezolvă acum
`rag/indexer.py:_vectori_existenti()` citește indexul precedent și îl transformă într-un
dicționar `text_chunk -> embedding`. La reconstrucție, fiecare chunk se caută întâi acolo.
Cheia e **textul chunk-ului**, nu numele documentului și nu mtime-ul. Motivul: dacă nu s-a
schimbat niciun caracter în chunk, vectorul e identic prin definiție. Un document reformatat
în Drive, ale cărui chunk-uri ies la fel, nu costă nimic. Un document în care s-a schimbat un
rând costă doar chunk-urile atinse.
**Sursa e chiar `rag_index.json`, nu un al doilea fișier de cache.** Deliberat: nu există un
al doilea fișier care să se poată desincroniza, iar dacă indexul lipsește se recalculează tot,
exact ca înainte. Logul spune de fiecare dată câte au fost calculate și câte refolosite.
### Când tot se recalculează (și e corect așa)
| Situație | Ce se întâmplă |
|---|---|
| `rag_index.json` lipsește sau e corupt | se recalculează tot — nu are de unde refolosi |
| Textul unui chunk s-a schimbat, oricât de puțin | acel chunk se re-embedează (corect: alt text = alt vector) |
| Se schimbă **strategia de chunking** (`indexer.py`) | toate chunk-urile ies altfel → se recalculează tot |
| Se schimbă `EMBED_MODEL` în env | **capcană: NU se recalculează**, deși ar trebui |
Ultima linie e singura problemă rămasă și e reală: cheia de cache e doar textul, fără modelul
care l-a produs. Dacă cineva schimbă `EMBED_MODEL`, indexul rămâne un amestec de vectori din
două modele, iar căutarea dă rezultate aiurea fără niciun mesaj de eroare. Nu s-a întâmplat
(modelul e `nomic-embed-text` de la început), dar merită reparat — vezi „De făcut" mai jos.
**Fișiere:** `rag/indexer.py` (`_vectori_existenti`, `build`), teste în
`tests/test_store_si_chunking.py` (`test_vectorii_din_indexul_vechi_se_refolosesc`,
`test_textul_modificat_se_reembedeaza`).
---
## 2. Ce s-a mai făcut în sesiune
Toate în `proxmox/lxc171-claude-agent/`. Documentația completă e în
[`maria-whatsapp-bridge/README.md`](proxmox/lxc171-claude-agent/maria-whatsapp-bridge/README.md) —
aici doar ce nu se vede din cod.
### Asociere WhatsApp prin cod, nu doar QR
`/pair` exista dar nu era folosibil: fără descriptor `browser` explicit WhatsApp refuză codul,
iar după introducerea lui corectă serverul cere un restart (515) care consuma din bugetul de
reîncercări și putea opri puntea la a cincea asociere. Cod valabil ~3 minute, cu marcarea
expirării în dashboard. Folosit pe bune pentru re-legarea lui Echo.
### Capturi de ecran cu erori (OCR)
tesseract `ron+eng`; modelul de răspuns e strict text, deci OCR nu e o opțiune de calitate.
**Căutarea în index folosește doar liniile care arată a eroare**, nu toată fereastra — un ecran
de meniuri diluează embedding-ul. Modelul primește captura întreagă, marcată ca OCR.
### Rank hibrid (`rag/rank.py`)
Embeddings + BM25, fuzionate prin RRF. Motivul, măsurat: intervalele de cosinus **se suprapun**
— întrebări bune 0,600–0,816, străine 0,534–0,685. Codurile (`ORA-01722`, `D406`, `CIF`) sunt
exact ce ratează căutarea semantică. Praguri calibrate cu `ops/calibrate-rank.py`, **19/19**.
### Escaladare la suport
Când nu există acoperire, modelul **nu mai e întrebat deloc**. Referință (`M-260831-FC34`),
confirmare onestă („am trimis" doar dacă notificarea chiar a plecat), captura se retrimite ca
imagine. Jurnal în `~/.maria-bridge/escalations/`, vizibil în dashboard.
### Dicționar de 29 de erori Oracle
`maria-whatsapp-bridge/knowledge/oracle-erori-uzuale.xml`, scris pentru clienți — fără nume de
servere sau pași de administrare.
### Două bug-uri găsite în log
- Fiecare mesaj din self-chat sosea **de două ori** (`@s.whatsapp.net` și `@lid`) → Maria
răspundea dublu. Dedup pe `key.id` în punte.
- Documentele adăugate din dashboard ajungeau în oglinda Drive și **dispăreau la următorul
`rclone sync`**. Acum există `~/.maria-bridge/documents-local/`.
### Echo re-legat la WhatsApp
Dispozitivul `:14`. S-a lămurit de ce nu răspund ambele punți în chatul „Eu": Echo aruncă tot
ce e `fromMe && !isGroup`, iar în self-chat tot ce scrii e `fromMe`. **Echilibrul e accidental** —
detalii în [`docs/chatboti-si-punti.md`](docs/chatboti-si-punti.md).
---
## 3. De făcut
### a) `EMBED_MODEL` în cheia de cache (mic, real)
Vezi tabelul de mai sus. Cea mai simplă variantă: pune modelul în fiecare intrare de index
(`"model": config.get("EMBED_MODEL")`) și refolosește doar intrările cu modelul curent.
Intrările vechi, fără câmp, se tratează ca „model necunoscut" → se recalculează o dată.
### b) `SUPPORT_JID` — decizia utilizatorului
E setat pe **propriul număr** (`40723197939@s.whatsapp.net`), pentru testare. De schimbat cu
numărul sau grupul echipei de suport, în `~/.maria-bridge/env`.
### c) Dicționarul Oracle trăiește doar pe container
E în `documents-local/`, deci supraviețuiește sincronizărilor, dar **nu-l vede nimeni altcineva**.
Locul lui pe termen lung e `document_store` din Drive. Când ajunge acolo, șterge copia locală —
altfel rămân două versiuni și câștigă tăcut cea din Drive.
### d) Recalibrare când se schimbă documentele
Pragurile din `env` sunt măsurate pe indexul de **acum**. La schimbări mari de conținut:
```bash
cd proxmox/lxc171-claude-agent/maria-whatsapp-bridge
MARIA_BRIDGE_DIR=$HOME/.maria-bridge ~/.maria-bridge/venv/bin/python ops/calibrate-rank.py
```
Adaugă în `CAZURI` (în capul scriptului) întrebările reale la care Maria a greșit — e singurul
mod în care calibrarea rămâne onestă.
### e) Rămase din handover-ul precedent
LXC 301 (nimic urgent), auditul de infrastructură, modelul de răspuns al Mariei.
---
## 4. Stare la finalul sesiunii
| | |
|---|---|
| Index RAG | 169 chunk-uri, 12 documente (din care `oracle-erori-uzuale.xml` e local) |
| Servicii | `maria-whatsapp`, `maria-rag`, `claude-discord-dashboard` — active |
| WhatsApp | Maria conectată (`40723197939`), Echo re-legat ca dispozitiv `:14` |
| Teste | **73 pass** (26 la începutul zilei) |
| `SUPPORT_JID` | propriul număr — de schimbat |
## 5. Capcane (în plus față de handover-ul precedent)
- **`systemctl` pe LXC 110 are nevoie de `--user`.** Serviciile lui Echo sunt unități de
utilizator (`moltbot`, uid 1000). Ca root, `systemctl is-active echo-core` răspunde
`inactive` deși botul rulează.
- **Nu adăuga documente în `~/.maria-bridge/documents/`.** E oglinda Drive-ului; `rclone sync`
le șterge. Folosește `documents-local/`.
- **Nu ridica pragurile de rank „ca să fie sigur".** Escaladarea prea agresivă e la fel de
proastă ca răspunsul generic — rulează calibrarea, nu intuiția.
- **Fiecare embedding costă ~7 secunde.** Orice schimbare în strategia de chunking înseamnă
recalcularea întregului index; nu e o schimbare de făcut în grabă.

View File

@@ -1,154 +0,0 @@
# Context handover — 2026-09-01, seara
> **REZOLVAT in `653f5cb`** (2026-09-01, noaptea). Pasul (1) facut: setul are 31 de
> cazuri. Pasul (2) facut, dar cauza s-a dovedit ALTA decat se banuia aici:
> politetea din ancora nu era ce strica verdictul, ci faptul ca termenii se
> comparau pe **cuvantul intreg** — documentul scrie „token"/„tokenuri", omul scrie
> „tokenul". Reparat prin potrivire pe primele 5 litere (codurile raman intregi) +
> varianta (a), `rank.assess(dovada=...)`. Varianta (b), lista de formule de
> politete, a fost scrisa si apoi **scoasa**: masuratoarea n-a justificat-o.
> 31/31 cu reparatia, 30/31 fara. Restul documentului se pastreaza ca istoric —
> §7 (eroarea „CUI cumparator incorect" lipsa din documente) e inca deschis.
**Ce e de făcut:** două lucruri legate între ele, în `proxmox/lxc171-claude-agent/maria-whatsapp-bridge/`:
1. adaugă **cazuri de continuare** în `ops/calibrate-rank.py` (azi setul n-are decât două, ambele cu cod de eroare);
2. repară **termenii distinctivi**: `rank.rare_terms()` judecă acoperirea pe *interogarea combinată* (ancora firului + mesajul nou), nu pe ce a întrebat omul acum.
(1) există ca să poți face (2) fără să ghicești. **Nu începe cu (2).**
Tot ce e mai jos e comis și împins: `ROMFASTSQL` `5e60394..af04d97`.
---
## 1. De unde vine
Sesiunea de azi a plecat de la: *„am trimis 2 mesaje pe grupul maria test whatsapp și ambele răspunsuri sunt eronate"*. Cauza reală s-a dovedit dublă și e reparată (vezi §5), dar reparația a scos la iveală problema care rămâne deschisă.
Din firul de test real (escaladarea `M-260901-06EB`, `~/.maria-bridge/escalations/1788291399-M-260901-06EB.json`):
```
[consumer] rank: efactura_knowledge.md(0.836), … | cosinus 0.836, iar termenii
distinctivi (buna, luna, august, trimitere, auto) nu apar in documente
```
**„buna" e tratat drept termen distinctiv.** E rar în corpusul de ERP, deci `rare_terms()` îl consideră discriminant, deși e o formulă de salut.
De ce contează acum și nu conta înainte: de azi, la o continuare se caută după **ancoră + mesajul nou** (`consumer.py:574` → `fir.interogare()`), iar ancora e primul mesaj al omului, cu tot cu „Buna . … Poti te rog sa ma ajuti?". Ancora nu se șterge cât trăiește firul (2h), deci **fiecare continuare de pe firul ăla e judecată și după „buna/luna/august"**, cuvinte care nu vor apărea niciodată în documente.
### Efectul
`assess()` cere ca cel puțin `RANK_MIN_RARE_RATIO` (0,5) din termenii distinctivi să apară chiar în chunk-uri. Cu 3 cuvinte de politețe care nu apar niciodată, raportul scade artificial la fiecare replică → **escaladare în plus**.
Direcția e sigură (un om vede întrebarea, nu primește un răspuns inventat), deci **nu e urgent**. Dar face Maria inutilă exact pe discuțiile lungi, unde ar trebui să fie cea mai bună.
În cazul testat n-a stricat nimic — răspunsul chiar nu era în documente, escaladarea a fost corectă. Deci **nu ai încă nicio dovadă că regresia se produce în practică**; asta e chiar ce trebuie măsurat la pasul (1).
---
## 2. Reparația propusă (nu implementată)
**Separă cele două roluri ale interogării:**
| Rol | Ce folosește azi | Ce ar trebui |
|---|---|---|
| **regăsire** (ce chunk-uri aduc) | ancoră + mesaj nou | rămâne așa — ancora chiar ajută, „da, mă blochează" singur nu găsește nimic |
| **acoperire** (răspund sau escaladez) | ancoră + mesaj nou | doar mesajul nou (+ OCR-ul capturii), adică ce a întrebat omul acum |
Concret, în `consumer.py:131 search()`, `rank.assess()` (`consumer.py:144`) primește azi același `query` ca și regăsirea. Ar trebui să primească separat interogarea de dovadă.
**Capcana, și motivul pentru care n-am făcut-o azi:** o continuare scurtă („da, mă blochează complet", „eram la salvarea unei facturi") **n-are termeni distinctivi proprii**. Judecată singură, escaladează — exact regresia pe care `fir.py` a fost scris ca s-o prevină (vezi tabelul din docstring-ul lui `rag/fir.py`, cazurile măsurate). Deci reparația poate strica mai mult decât repară, și nu se poate valida pe setul actual de cazuri.
Variante de luat în calcul, în ordinea preferinței:
- **a)** acoperirea se judecă pe mesajul nou, dar dacă acesta n-are termeni distinctivi proprii se cade înapoi pe interogarea combinată (continuările scurte se comportă ca azi);
- **b)** `rare_terms()` ignoră formulele de politețe — listă mică de stopwords („buna", „salut", "buna ziua", „multumesc", „va rog", „poti", „ajuti"). Whack-a-mole, dar o linie și zero risc;
- **c)** ancora se curăță la creare: se păstrează doar liniile care arată a eroare, cum face deja `ocr.retrieval_query()` pentru capturi (`rag/ocr.py`).
(b) e cel mai ieftin și poate fi făcut singur, imediat. (a) e cel corect. Nu le confunda: (b) ascunde simptomul măsurat azi, (a) rezolvă clasa.
---
## 3. Ce trebuie adăugat în `ops/calibrate-rank.py` — pasul (1)
Setul (`CAZURI`, o listă de `(intrebare, are_raspuns_in_documente)`) are azi **26 de cazuri, 26/26 corecte**. Continuări are doar două, și **ambele conțin un cod de eroare**, deci trec pe regula codurilor și nu spun nimic despre termenii distinctivi:
```python
("ORA-06550: line 1, column 7 PLS-00906 object invalid\nda, ma blocheaza complet", True),
("ORA-12154 TNS could not resolve the connect identifier\neram la salvarea unei facturi", True),
```
**Lipsesc continuările fără cod de eroare** — exact cazul în care termenii distinctivi decid. De adăugat, în forma pe care o produce `fir.interogare()` (ancoră `\n` mesaj nou):
- ancoră politicoasă + continuare scurtă, cu răspuns în documente → `True`
(ex. `"Buna ziua . Am o problema la trimiterea in SPV . Puteti sa ma ajutati?\nimi zice ca tokenul e expirat"`)
- ancoră politicoasă + continuare scurtă, fără răspuns în documente → `False`
- aceeași continuare **fără** formula de salut în ancoră → același verdict ca varianta cu salut
(dacă verdictele diferă, ai măsurat exact regresia)
- ancoră + a treia, a patra replică (fir lung) → verdictul nu trebuie să se degradeze cu lungimea
- continuare pe un fir deschis de o **captură** (ancora = OCR, plin de zgomot: „Total", „Lit", „Selecteaz!")
Scrie-le pornind de la fire reale, nu inventate: `~/.maria-bridge/escalations/*.json` are câmpul `fir` cu schimburile, iar `~/.maria-bridge/conversations/*.json` are ancora exactă.
**Atenție:** fiecare caz nou costă un embedding (~7s pe CPU, fără GPU pe LXC 171). Se pun în cache automat în `~/.maria-bridge/calibrare-cache.json`, deci doar prima rulare e lentă.
### Cum se rulează
```bash
cd /workspace/romfastsql/proxmox/lxc171-claude-agent/maria-whatsapp-bridge
MARIA_BRIDGE_DIR=$HOME/.maria-bridge ~/.maria-bridge/venv/bin/python ops/calibrate-rank.py
MARIA_BRIDGE_DIR=$HOME/.maria-bridge ~/.maria-bridge/venv/bin/python ops/calibrate-rank.py --sweep
python3 -m pytest -q # 103 teste; venv-ul n-are pytest, se ruleaza cu python3 de sistem
```
`--sweep` mătură `RANK_WEAK_COSINE` × `RANK_MIN_RARE_RATIO` (nu mai există prag „strong", vezi §5).
**Criteriu de acceptare pentru (2):** setul extins rămâne 100% corect *și* cu reparația, *și* fără ea nu e — altfel n-ai dovedit că reparația face ceva.
---
## 4. Fișiere care contează
| Fișier | De ce |
|---|---|
| `rag/rank.py:168 rare_terms()` | inima problemei: `len(t) >= 4 and df <= 25% din corpus` → „buna" trece |
| `rag/rank.py:212 assess()` | decizia acoperirii; docstring-ul explică de ce nu mai există prag pe cosinus |
| `rag/consumer.py:131 search()` | aici `assess()` primește același query ca regăsirea — punctul de separat |
| `rag/consumer.py:574` | `search_query = fir_mod.interogare(fir, search_query)` — unde se combină |
| `rag/fir.py:140 interogare()` | ancoră + mesaj nou |
| `rag/fir.py:126 extinde_ancora()` | ancora crește cu OCR-ul capturii (adăugat azi) |
| `rag/fir.py` docstring | tabelul cu răspunsurile greșite care justifică existența ancorei — citește-l înainte să scoți ancora din ceva |
| `ops/calibrate-rank.py` | setul de cazuri + `--sweep` |
| `tests/test_rank.py`, `tests/test_fir.py` | testele care prind regresiile |
| `README.md` §„Cum se ordonează rezultatele (rank)" | documentația de actualizat la final |
---
## 5. Ce s-a reparat azi (nu reface)
Trei comiteri, toate împinse, serviciile repornite, verificate pe viu în grupul „Maria Test".
**`45b3659`** — două răspunsuri inventate: mesaj vag (cosinus 0,768) și captură cu `errorMessage="CUI cumparator incorect"` (0,778, eroare inexistentă în documente). Cauza: `RANK_STRONG_COSINE=0,70` însemna „cosinusul singur e destul", dar toate chunk-urile eFactura seamănă între ele. Plus `LLM_TEMPERATURE=0` (llama.cpp folosea 0,8).
**`47110d7`** — două probleme de fond:
- gardul „mesaj prea vag" (`triaj.prea_vag`) cerea text sub 12 cuvinte; mesajul real avea 14. Acum nu mai numără cuvinte și se aplică **după căutare, doar când nu există acoperire**, o singură dată pe fir (`fir["detalii_cerute"]`);
- `fir.este_continuare()` rupea firul la **orice** imagine. Cazul frecvent e tocmai text + captură trimise una după alta. Acum o captură în `FIR_IMAGINE_MIN` (5 min) continuă firul, OCR-ul intră în ancoră, iar pe o escaladare deschisă captura pleacă la aceeași referință;
- **`RANK_STRONG_COSINE` a dispărut de tot.** Motivul e important pentru (2): *cosinusul crește cu lungimea interogării*. Aceeași captură: 0,778 singură, **0,836** împreună cu mesajul de dinainte. Orice prag fix de sus e trecut de o discuție destul de lungă. Acoperirea se sprijină acum exclusiv pe dovada lexicală — adică pe chiar `rare_terms()`, ceea ce face (2) mai important decât părea.
**`af04d97`** — găsite citind logul: `requests` nu ridică excepție la 4xx/5xx, deci escaladarea se scria `notified: true` chiar când puntea răspundea 503; puntea nu loga nimic la trimitere; mesajele primite se logau tăiate la 80 de caractere fără semn (m-a dus la o concluzie greșită în diagnostic).
---
## 6. Constrângeri de mediu
- **Modelul de chat e Qwen3.5-2B Q4** pe LXC 104 (`LLM_URL=http://10.0.20.161:8091`). Inventează dacă îl lași. Poarta de acoperire e singura apărare reală — de asta merită reparată bine.
- **Nu încerca un model mai mare pe LXC 171.** Testat azi: `qwen3:4b-instruct` prin Ollama, 4 nuclee, fără GPU → 600s timeout fără niciun token; un prompt de 15 tokeni a scos 6 tokeni în 10 min 20s. Modelul a fost șters. Fără GPU sau API extern (vezi `docs/supliment-glm-zai.md`, neimplementat) nu e altă variantă.
- **Ollama pe 171 are doar `nomic-embed-text`**, pentru embeddings. Un embedding ≈ 7s.
- Servicii: `systemctl --user restart maria-rag maria-whatsapp`; loguri în `~/.maria-bridge/logs/{rag,whatsapp}.log`.
- Grup de test: `120363409761730101@g.us` („Maria Test"), `SUPPORT_JID` e chiar numărul propriu, deci escaladările se văd.
- **Șterge firele vechi înainte de a testa** (`rm -f ~/.maria-bridge/conversations/*.json`), altfel primul mesaj e luat drept continuare a discuției precedente — TTL 120 min.
---
## 7. De reținut, separat de task
**„CUI cumparator incorect" nu există în niciun document** (`grep -ri "cumparator incorect" ~/.maria-bridge/documents*` → zero). Maria escaladează corect, dar ar putea răspunde. E o eroare eFactura reală și frecventă. N-am scris textul — nu inventez procedură ROA. De pus în `document_store` (Google Drive), de către cineva care știe procedura.