Files
echo-core/tasks/lessons.md
Marius Mutu a62e4d4ab7 fix(gitignore): tipare de secrete, ca auto-commit-ul să nu mai publice credențiale
Dashboard-ul face `git add -A` + commit + push fără intervenție umană
(dashboard/handlers/git.py:110). Regulile pe nume exact acopereau doar fișierele
observate, nu clasele lor: `bridge/whatsapp/auth/` era ignorat, dar
`auth.bak-*/` nu — așa a ajuns creds.json cu chei de sesiune WhatsApp pe Gitea.

Audit: din 21 de nume plauzibile testate (.env.local, credentials.json, token.json,
id_rsa, auth-old/, server.pem, cookies.txt, dump.sql, config.json.bak, nohup.out),
toate 21 treceau nestingherite. Acum toate sunt prinse.

- bridge/whatsapp/auth*/ acoperă orice variantă de director de stare Baileys
- .env* (cu excepție pentru .env.example), *.pem, *.key, id_rsa*, id_ed25519*
- *token*.json, *credential*.json, client_secret*.json, creds.json, cookies*.txt
- backup-uri/dump-uri: *.bak, *.backup, *.orig, *.dump, *.sql, *.tar.gz, *.zip,
  *.bundle — cu !memory/kb/**/*.bak pentru notițele legitime din KB
- nohup.out, core.[0-9]*

Verificat în ambele direcții: `git ls-files | git check-ignore --stdin` nu
întoarce nimic (niciun fișier tracked nu devine ignorat), iar cele 21 de nume
ipotetice sunt acum toate ignorate.

Restul auditului e curat: zero secrete în fișierele tracked, config.json fără
credențiale (keyring folosit corect), credentials/, dashboard/.env,
memory/echo.sqlite și bridge/whatsapp/auth/ deja acoperite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0188YmDTUAzVvZDVh8JL8xSa
2026-08-31 22:04:58 +00:00

18 KiB
Raw Blame History

Lessons Learned

Lecții capturate din corectările lui Marius. Citește acest fișier la începutul oricărei sesiuni de cod (înainte de plan mode) și aplică lecțiile relevante. Iterează neobosit pentru a evita rate drop-uri pe greșeli repetate.

Format per lecție:

## <titlu scurt>
**Data:** YYYY-MM-DD
**Context:** ce făceam când a apărut corectarea
**Greșeala:** ce am făcut greșit
**Regula:** ce să fac în schimb, în viitor
**Când se aplică:** trigger-uri concrete (fișiere, task-uri, situații)

La debugging cron, verifică ȘI crontab-ul de sistem, nu doar cron/jobs.json

Data: 2026-07-23 Context: Marius a întrebat de ce newsletter-ul cercetași a venit pe Discord dar nu pe WhatsApp. Am căutat doar în cron/jobs.json (scheduler-ul APScheduler intern), n-am găsit niciun job relevant, și am conchis greșit că nu există job automat — Marius m-a corectat ferm ("Nu este adevărat. Exista un job care rulează prima data joia"). Greșeala: Există o a doua sursă de cron în acest repo: crontab-ul real de sistem (crontab -l), separat de cron/jobs.json. Job-ul real era tools/check_newsletter_cercetasi.py, programat direct în crontab de sistem (0 17 * * 4,5,1), nu în scheduler-ul intern. N-am verificat crontab-ul de sistem înainte să declar "nu există job". Regula: La orice întrebare despre "de ce n-a rulat / de ce n-a trimis un job automat", verifică AMBELE surse înainte să tragi concluzii: (1) cron/jobs.json (APScheduler intern, gestionat de src/scheduler.py), (2) crontab -l (cron de sistem, scripturi standalone în tools/*.py cu shebang + subprocess). Grep după numele funcțональ ("newsletter", "cercetasi") în tools/*.py și crontab -l, nu doar în jobs.json. Când se aplică: Orice debugging de tip "de ce nu am primit X automat" / "unde e programat job-ul Y" în echo-core.

Transliterează și numele proprii pe turnuri [tts-lang:en] — un singur diacritic declanșează fallback pe Supertonic

Data: 2026-07-11 Context: Marius a întrebat vremea la Constanța (turn de voce, vocea activă = pocket-tts, marker [tts-lang:en]). Am scris răspunsul în engleză dar am păstrat "Constanța" cu ț original. Rezultatul audio a venit cu o voce Supertonic, nu cu Marius 4 (pocket-tts) cum se aștepta Marius. Greșeala: Am presupus greșit că "engleză" înseamnă doar propoziții englezești — am lăsat un nume propriu (Constanța) netranslit. tools/tts.py::looks_romanian() (folosit în src/voice/tts_stream.py:213) verifică prezența oricărui caracter din _RO_DIACRITICS (ă/â/î/ș/ț), indiferent de context — UN SINGUR caracter e suficient. Codul a funcționat corect conform design-ului: a detectat diacriticul, a presupus că tot blocul e română netradusă, și a căzut intenționat pe Supertonic (mai bine o voce RO decât tăcere totală) — vezi comentariul din tts_stream.py:213-222. Deci nu a fost bug de cod, ci scriere incompletă din partea mea. Regula: Pe orice turn cu marker [tts-lang:en], transliterează ȘI numele proprii/toponimele (Constanța → Constanta, Brașov → Brasov, etc.) — nu doar propozițiile. Verifică explicit că răspunsul nu conține NICIUN caracter din ăâîșțĂÂÎȘȚşţŞŢ înainte de a trimite, altfel guard-ul din tts_stream.py face fallback silențios pe Supertonic și vocea aleasă de Marius (pockettts) nu se aude deloc. Când se aplică: Orice răspuns pe un turn de voce cu vocea activă pockettts (Marius 1-4, Paula 1-3, alba) și marker [tts-lang:en] în prefix. Verifică inclusiv toponime, nume, abrevieri românești din text.

Intră în plan mode ÎNAINTE de a executa orice modificare de cod

Data: 2026-05-28 Context: Marius a descris o cerință de îmbunătățire a comenzii /audio cu URL (chunk by chunk). Am implementat direct fără plan mode. Greșeala: Am sărit peste pasul de planificare și am modificat fișierele fără aprobarea lui Marius. Regula: Pentru orice modificare de cod (nu doar task-uri cu 3+ pași), intră în plan mode, prezintă planul, și AȘTEAPTĂ aprobarea înainte de a atinge vreun fișier. Când se aplică: Orice cerere de cod/implementare, indiferent de simplitate aparentă. Dacă e tentant să implementezi direct pentru că pare simplu — e exact momentul să te oprești și să planifici.

Supertonic rejectează ghilimelele curly (Unicode) cu HTTP 500

Data: 2026-05-27 Context: Marius a dat o comandă audio pe Discord cu un URL, iar răspunsul lui Claude conținea „foo" (ghilimele românești curly). Supertonic a returnat HTTP 500: synthesis failed: Found 1 unsupported character(s): ['„'] și răspunsul nu s-a mai auzit. Fără retry logic vizibil în UX — pur și simplu tace. Greșeala: Am presupus că normalize_for_tts produce text deja "TTS-safe" pentru Supertonic. În realitate strip_markdown păstrează ghilimelele Unicode („ U+201E, " U+201D, — U+2014, … U+2026, etc.) pe care Supertonic le refuză. Regula: Înainte de orice apel HTTP la Supertonic, sanitizează punctuația Unicode la echivalentele ASCII („ " " → ", ' ' ‚ → ', – — → -, … → ..., « » → "). Funcția sanitize_punctuation în src/voice/normalize.py face asta și e apelată chiar după strip_markdown în pipeline. Dacă apar caractere noi care crapă Supertonic (ex: simboluri matematice, săgeți), adaugă-le în _TTS_PUNCT_MAP. Când se aplică: Orice cod care trimite text la Supertonic (tools/tts.py, src/voice/tts_stream.py). Inclusiv testare manuală cu curl — folosește text românesc realistic (include „foo", em-dash —, ellipsis …).

Mai multe threads ≠ mai rapid — fitează cpu_threads pe physical cores, nu logical

Data: 2026-05-27 Context: Benchmark tools/voice_bench.py pentru faster-whisper small int8 pe i7-6700T (4 physical / 8 logical cores). Marius a urcat VM-ul de la 2 → 4 → 6 cores online, așteptând că mai multe = mai rapid. Greșeala: Presupoziție implicită că cpu_threads=N scalează liniar cu N. La 6 threads small.p50 a regresat la 2.79s vs 2.25s la 4 threads (+24% MAI LENT). Era ușor de ratat dacă rulam doar un singur pass. Regula: Pentru workload-uri compute-bound (int8/fp16 ML inference, video encode, criptografie) setează cpu_threads = numărul de PHYSICAL cores, NU logical. Hyperthreads adaugă synchronization overhead și memory bandwidth contention fără paralelism real. Sweet spot tipic: min(num_physical_cores, $optimal_threads). Verifică cu lscpu (Core(s) per socket × Socket(s) = physical; CPU(s) = logical). Dacă faci benchmark, rulează SWEEP nu single point — 2/4/6/8 threads să vezi unde e curba reală. Când se aplică: Configurare cpu_threads, OMP_NUM_THREADS, MKL_NUM_THREADS, torch.set_num_threads(), ffmpeg -threads, sau orice runtime ML/inference. Mai ales pe Proxmox VM-uri unde "more cores online" sună ca îmbunătățire. Întreabă-te: e workload compute-bound (yes → physical only) sau IO-bound (yes → logical OK)?

Nu șterge crontab-uri din sistem fără confirmare explicită

Data: 2026-05-20 Context: Marius a cerut să șteargă "newsletter test din cron jobs". Am interpretat că check_newsletter_cercetasi.py din crontab de sistem face parte din "newsletter test". Greșeala: Am inclus în scop un crontab de sistem care nu fusese menționat explicit. "newsletter test" se referea doar la job-ul newsletter-test din cron/jobs.json. Regula: Crontab-ul de sistem (crontab -l) este separat de cron/jobs.json. Nu îl modifica fără instrucțiuni explicite. Dacă scope-ul nu e clar, întreabă înainte de a acționa pe crontab sistem. Când se aplică: Orice task care implică ștergerea sau modificarea cron jobs — distinge întotdeauna între cron/jobs.json (APScheduler) și crontab-ul de sistem.

Nu scrie manual în index.json — rulează update_notes_index.py

Data: 2026-04-29 Context: Salvam o notiță din Facebook reel în memory/kb/. Am adăugat manual o intrare în index.json cu schema greșită (id + path în loc de file), ceea ce a blocat notes.html pe "Se încarcă..." cu un TypeError în renderNoteCard. Greșeala: Am editat index.json direct, cu o schemă diferită față de ce produce update_notes_index.py. Regula: Niciodată nu scriei manual în memory/kb/index.json. Fluxul corect: (1) creezi fișierul .md în memory/kb/<categorie>/, (2) rulezi python3 tools/update_notes_index.py. Dacă ai nevoie să salvezi o notiță din Facebook/video, folosești scripts/transcribe_video.sh <URL> auto --save-kb --bg --notify <channel_id> care face totul corect (--bg e obligatoriu dintr-un turn de chat — vezi lecția despre timeout-ul de 300s). Când se aplică: Orice salvare de notiță în KB (Facebook, YouTube, coaching, insights, orice). Dacă ești tentat să json.dump în index.json — stop, rulează scriptul.

Verifică că modelul/tool-ul numit chiar are capabilitatea ÎNAINTE de a planifica în jurul lui

Data: 2026-06-27 Context: Marius a cerut să folosesc gemma4:31b-cloud (Ollama) pentru decodare audio ca alternativă la Whisper. Am verificat pe pagina oficială Ollama: variantele cloud (31b) suportă doar Text+Image — audio există DOAR pe E2B/E4B (edge, local), iar acela e stricat de o regresie upstream deschisă (issue #16584). Premisa cererii era infezabilă. Greșeala (evitată): Dacă planificam direct integrarea fără să verific pagina modelului, scriam cod de cablare Ollama audio care n-ar fi funcționat niciodată. Search-ul generic spunea „Gemma 4 are audio" — adevărat la nivel de familie, fals pentru modelul cloud specific cerut. Regula: Când userul numește un model/serviciu specific pentru o capabilitate (audio, vision, tool-use, context lung), verifică pagina/docs ACELUI model exact înainte de a planifica. Capabilitățile diferă per variantă (cloud vs edge, sizes). Fetch pagina oficială, nu te baza pe search agregat la nivel de familie. Când se aplică: Orice task care pornește de la „folosește modelul X pentru Y". Confirmă X→Y pe sursa primară înainte de plan mode.

Corecția post-STT de text e cosmetică dacă consumatorul e un LLM — fixează la sursă (model), nu cu dicționar

Data: 2026-06-27 Context: Plan inițial pentru curățarea transcrierii Whisper avea 4 piese, inclusiv dicționar de restaurare diacritice + canonicalizare wake-word. Două review-uri independente (/autoplan CEO+Eng) au arătat: textul transcris merge la Claude, care citește română fără diacritice perfect; NU există wake-word gate în cod (on_segment_done dispatch necondiționat); singurul consumator precis (detect_voice_change) e deja fuzz-hardenat. Un spike a confirmat că modelul RO-finetuned (mikr/whisper-small-ro-cv11) înjumătățește WER (24%→10%) și fixează numerele la SURSĂ, +0.33s latență. Greșeala (evitată): Construirea unui strat de corecție hand-curat (întreținere perpetuă, risc de regresie pe cuvinte ambigue) când fix-ul real era un model finetuned cu același cost de inferență. Regula: Înainte de a peticit output-ul unui model ML cu post-procesare rule-based, întreabă: (1) cine e CONSUMATORUL textului? (un LLM tolerează erori; un parser regex nu); (2) există un model finetuned care fixează la sursă cu același cost? Spike-uiește modelul ÎNAINTE de a scrie straturi de corecție. Verifică unde merge output-ul prin cod, nu presupune un gate care „pare" că există. Când se aplică: Orice îmbunătățire de calitate STT/OCR/ML output. Tool de spike: tools/voice_stt_spike.py.

Fallback-urile nu trebuie să schimbe identitatea percepută (vocea) — degradează în interiorul aceleiași identități

Data: 2026-07-11 Context: Fix pentru voice mode: când modelul scăpa diacritice RO pe o voce pocket-tts (English-only), am ales fallback pe Supertonic M2 (altă voce, română) ca să evit tăcerea. Un singur nume propriu („Constanța") într-un răspuns altfel englezesc a comutat vocea întregului bloc — Marius a perceput asta ca bug mai grav decât problema inițială. Greșeala: Am optimizat pentru „să se audă ceva" fără să întreb ce dimensiune e prioritară pentru user. Vocea clonată E produsul; schimbarea ei e regresia maximă percepută. Regula: Când un input parțial invalid ajunge la un sistem cu identitate configurată de user (voce, persona, stil), remediază conținutul ca să treacă (transliterare, sanitizare), nu comuta pe altă identitate. Fallback pe alt engine/voce doar la eșec TEHNIC (server picat), nu la eșec de conținut. Când se aplică: Orice pipeline TTS/persona cu fallback. Implementare: fold_ro_diacritics în tools/tts.py + push_text în src/voice/tts_stream.py.

Munca mai lungă de 300s nu are ce căuta într-un turn de chat — rulează detașat și raportează pe canal

Data: 2026-08-31 Context: Marius a trimis două linkuri de video Facebook pe Discord. Niciunul n-a produs un răspuns. În jurnal: Claude CLI timed out after 300s, de cinci ori, pe patru încercări diferite. scripts/transcribe_video.sh rula în interiorul turnului, iar yt-dlp + ffmpeg + openai-whisper pe 9 minute de audio depășeau constant DEFAULT_TIMEOUT (src/claude_session.py:37). Procesul era omorât la mijlocul Whisper-ului. Greșeala: Am tratat o sarcină cu durată nemărginită (descărcare rețea + inferență ML) ca pe un apel sincron într-un turn conversațional. Efectele în cascadă au fost mai rele decât timeout-ul în sine:

  • Retry-uri oarbe pe linkul greșit — la reîncercare am reprocesat linkul 1 de două ori (două notițe duplicate) și n-am procesat niciodată linkul 2. Când mai multe linkuri eșuează, verifică care a eșuat înainte să reiei.
  • Documente pe jumătate — scriptul scrie template-ul cu TL;DR gol, iar rezumatul îl completez eu după ce scriptul întoarce. Procesul omorât ⇒ notițe cu TL;DR gol, care arată „gata" în index dar nu sunt.
  • Orfani pe disc — trap ... EXIT nu rulează la SIGKILL; rămâneau 33MB în /tmp/transcribe_$$.
  • Eșec tăcut la nivel de user — Marius n-a primit nici rezultat, nici eroare acționabilă. Regula: Dacă un pas poate depăși ~2-3 minute, nu-l rula în turn. Pornește-l detașat (setsid nohup), întoarce imediat un confirm cu job id + cale de log, și raportează finalizarea pe canal (tools/discord_send_file.py --text). Turnul confirmă pornirea, nu terminarea. Corolare descoperite la fix:
  • Nu forța limba. Rularea cu ro pe audio englezesc a produs transcriere halucinată (română inventată amestecată cu CJK). Implicit e acum auto; forțează doar dacă ești sigur.
  • Alege motorul înainte de a ridica timeout-ul. faster-whisper int8 face 554s de audio în 113s (~5x realtime); openai-whisper depășea 300s pe același fișier. faster-whisper era deja în requirements.txt, openai-whisper nici măcar nu era declarat. Un motor de 5x a fost fix mai bun decât un timeout mai mare.
  • Folosește .venv/bin/python3 explicit în scripturi shell. Scriptul chema python3 bare, iar whisper nu există în python-ul de sistem — mergea doar din noroc, când era chemat cu venv-ul activat.
  • Nu trece JSON mare printr-o variabilă de shell. Descrierile Facebook conțin caractere de control care corup variabila (character not in range); scrie-l pe disc și parsează fișierul o singură dată. || echo Unknown masca eșecul.
  • Titlurile Facebook încep cu statistici, nu cu titlul: 830K views · 15K reactions | Titlul real | Pagina. split('|')[0] producea notițe numite 2026-08-31_807k-views-15k-reactions.md. Sari segmentele de statistici și pe cel egal cu numele paginii. Când se aplică: Orice transcriere video/audio, OCR pe fișiere mari, scraping multi-pagină, build sau inferență ML pornită dintr-un mesaj de chat. Implementare: scripts/transcribe_video.sh (--bg, --notify) + send_text() în tools/discord_send_file.py.

Unde există git add -A, scrie .gitignore cu tipare, nu cu nume exacte

Data: 2026-08-31 Context: bridge/whatsapp/auth.bak-*/ cu creds.json (chei de sesiune WhatsApp) a ajuns pe Gitea prin chore: auto-commit from dashboard. .gitignore avea bridge/whatsapp/auth/ — regula acoperea directorul viu, nu backup-ul lui. Auditul de după a arătat că toate cele 21 de nume plauzibile testate (.env.local, credentials.json, id_rsa, token.json, auth-old/, config.json.bak, dump.sql) treceau nestingherite. Greșeala: .gitignore era o listă de nume exacte observate, nu de clase de fișiere. Într-un repo unde dashboard/handlers/git.py face git add -A + commit + push fără intervenție umană, orice fișier neignorat e publicat automat — nu există pasul în care cineva se uită la ce se stagează. Regula: Dacă ceva din sistem face git add -A neasistat, .gitignore e o barieră de securitate, nu o conveniență. Scrie-l pe tipare (auth*/, .env*, *token*.json, *.bak, *.pem), nu pe nume exacte, și adaugă excepții (!memory/kb/**/*.bak) pentru fals pozitivele legitime. Verifică ambele direcții: (1) git ls-files | git check-ignore --stdin să nu întoarcă nimic — niciun fișier tracked nu devine ignorat; (2) o listă de nume ipotetice de secrete să fie toate prinse. Când se aplică: Orice repo cu auto-commit, dar și înainte de a adăuga un backup/dump lângă un fișier deja ignorat. Întrebarea de reflex: „regula acoperă și variantele de nume ale acestui fișier?"