# 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ă.