65 Commits
Author SHA1 Message Date
Matteo Benedetto 12e63d09fa fix(fallback): connectTimeoutMs 2500→15000 e breakerTripAfter 2→3
Il default di 2,5 s per connect+headers è troppo stretto su percorsi VPN/AP:
misurati GET /v1/status 1,23 s e POST /v1/memories:search 1,98 s, talvolta
oltre 2,5 s. In quelle condizioni il breaker si apriva pur con gateway
raggiungibile e tutte le chiamate successive andavano in fast-fail per 2–10
minuti, spingendo di fatto ogni operazione sul solo fallback locale e
lasciando l'outbox non sincronizzata.

- extensions/shared.ts: connectTimeoutMs 2500 → 15_000 (default, fallback nel
  path di richiesta e commento), breakerTripAfter 2 → 3
- README.md: default aggiornati + motivazione nella tabella dei timeout
- skills/qmem/SKILL.md: default aggiornato e sintassi CLI del reset breaker
  (`qmem-sqlite.mjs breaker --reset`)

Verifica sul campo: con connectTimeoutMs=60000 l'outbox (18 record) è stata
sincronizzata completamente e il breaker è rimasto chiuso.
2026-09-16 10:26:52 +02:00
enne2 76eedcd526 fix(store): submitOrQueue restituisce ok — niente più falso "Errore 200" a salvataggio riuscito
extensions/local-db.ts: né il tipo di ritorno né i return di successo/fallthrough
di submitOrQueue prevedevano il campo `ok`. store.ts fa
`const { ok, status, data } = submitted; if (!ok)` → ok=undefined ⇒ !ok ⇒
stampava "Errore 200: {memory_id...}" anche quando il record era stato creato
sul gateway, saltando la conferma "Memoria salvata: <id>" e l'avviso duplicati
(score >= 0.92). Regressione introdotta in 9dd2429.

Ora: ok:true nel return di successo, ok:false in quello accodato e nel
fallthrough, `ok: boolean` nel tipo di ritorno.

Verifica (bun, gateway http://127.0.0.1:8082):
  kind valido   → ok:true,  status 200, remote_id c3733a1a-… → "Memoria salvata: …"
  kind invalido → ok:false, status 422 → "Errore 422: …"
(prima del fix ogni esito non-accodato stampava "Errore 200").
2026-09-15 23:24:10 +02:00
enne2 6607135856 fix(search): query opzionale — queries-only valido, derive base query da queries[0] (evita fallimenti validazione glm/deepseek) 2026-09-15 23:14:34 +02:00
Matteo Benedetto 9dd24298cc perf(fallback): connect timeout breve + circuit breaker persistente (fast-fail)
Il gateway irraggiungibile costava ~30s x4 tentativi (fino a ~2 minuti) per ogni
chiamata: ora si distinguono i due casi e le chiamate successive sono immediate.

- due timeout separati: `connectTimeoutMs` (default 2500, connect+headers) e
  `timeoutMs` (default 30000, budget per il body). Nessuna risposta entro il
  primo = "gateway non raggiungibile"; body lento = "elaborazione lunga"
- circuit breaker persistente in ~/.local/share/pi-qmem/breaker.json
  (env QMEM_BREAKER_FILE): fallimento definitivo (connessione rifiutata/DNS/
  connect timeout) → nessun retry e apertura immediata per `breakerBaseMs`
  (default 120000 = 2 min) con escalation fino a `breakerMaxMs` (10 min);
  5xx/body lento sono ambigui → retry con Retry-After e apertura dopo
  `breakerTripAfter` (default 2). Un successo lo richiude; cambiando `url` lo
  stato riparte chiuso (endpoint-aware)
- con breaker aperto gatewayRequest ritorna in ~0 ms senza rete
  (`gateway_unreachable`, `breaker_open`, `retry_in_ms`): i tool passano subito
  al fallback locale e l'outbox accoda
- fix di due bug scoperti durante i test:
  * `res.json().catch(() => ({}))` trasformava un body non completato in
    "successo con dati vuoti" → l'agente vedeva "nessun risultato" invece del
    fallback locale. Ora è `timeout_body` (fallimento, ambiguo)
  * `submitOrQueue` passava un AbortSignal esterno, che con la nuova semantica
    sarebbe stato letto come annullamento utente (eccezione invece di coda)
- messaggi dei tool con lo stato del breaker e come forzare un tentativo;
  `details.breaker` per l'osservabilità
- comandi: `/qmem:local breaker [reset]` e `qmem-sqlite breaker [--reset]`;
  lo stato compare in `/qmem:local status` e nella CLI
- budget interni per enrich/pull/flush (niente AbortSignal esterni)

Misure: connessione rifiutata → 4-8 ms (prima: 4 x 30 s); front che risponde
503 dopo ~40 s → 3,5 s alla prima chiamata, poi 0 ms di rete a breaker aperto;
server che accetta e non risponde → 708 ms (connect timeout); body lento →
1,2 s senza aprire il breaker; persistenza verificata fra processi distinti.

Test: scripts/test-local.mjs 38/38 (nuova fase dedicata al breaker).
2026-09-13 18:09:50 +02:00
Matteo Benedetto d1985514e1 feat(sync): tombstone, pull dall'export, ritocchi flush e fallback su 404
Prepara il client al gateway 2.12.0 (export + soft delete), mantenendo la
compatibilità con la 2.11.0 in produzione fino al redeploy.

- tombstone: colonna `deleted_at` (+ migrazione), monotona nel merge, esclusa
  dalle ricerche locali di default; `--deleted` in CLI e /qmem:local find;
  conteggio nel report/status; marker 🗑 nei risultati di qmem_get
- `pull` usa `include_deleted=true` e mappa l'intero payload dell'export
  (incluso deleted_at), così il mirror impara le cancellazioni
- rate limit: pace del flush 600 ms (100 req/min < 120/min del gateway) e
  messaggio dedicato su 429 (prima 300-400 ms → possibile 429 con code grandi)
- `qmem_get`: fallback sull'indice locale anche sul 404 (un id in coda non è
  ancora sul gateway) con etichetta "non presente sul gateway"
- test: 27 controlli (nuovi: pull con tombstone, esclusione/visibilità
  tombstone, ricerca del record esportato)

Verificato con la suite locale completa: 27/27.
2026-09-13 17:53:59 +02:00
Matteo Benedetto d509778448 chore(gateway): rimuove la copia duplicata del gateway dal package (dedup)
La cartella gateway/ era un duplicato byte-identico (sha256 su 14 file) del
repository canonico privato enne2/qmem-gateway (GATEWAY_VERSION 2.11.0,
guardrail similarity-v2): due fonti dello stesso codice, rischio di divergenza.

- la fonte unica del gateway + deploy (docker-compose.yml, .env, test, README
  operativo) è git:git.enne2.net/enne2/qmem-gateway, clonata in ~/dev/qmem-gateway
- gli artefatti presenti SOLO qui (README.md, requirements-dev.txt, tests/)
  sono stati spostati in quella repo prima della rimozione (commit locale
  15cc9d3, nessun push): nessuna perdita di contenuto
- backup integrale della cartella rimossa:
  ~/archive/backups/pi-qmem-gateway-copy-20260913-173728.tar.gz
- README aggiornato con il puntatore al repo canonico

Il package pi-qmem ora contiene solo estensione, skill, tool CLI e test
dell'indice locale (il gateway si deploya dal repo dedicato).
2026-09-13 17:37:45 +02:00
Matteo Benedetto 322b4cf446 feat(outbox): store offline con coda locale e sincronizzazione al ritorno della rete
Prima qmem_store falliva se il gateway non era raggiungibile: la conoscenza
andava persa. Ora il record entra in una coda locale persistente e viene
inviato automaticamente quando la connessione torna.

Core (extensions/local-db.ts):
- tabella `pending` (local_id, payload JSON, attempts, last_error, status,
  remote_id) + colonna `records.pending` (migrazione automatica dei DB esistenti)
- queueStore(): accoda e crea subito il placeholder locale ricercabile ()
- flushQueue(): POST /v1/memories con Idempotency-Key = local_id (retry senza
  duplicati), FIFO, pacing sotto il rate limit, timeout 12s per richiesta
- esiti: synced (il record locale adotta l'ID remoto, niente duplicati) ·
  duplicate (409: registra l'ID del match e NON sovrascrive il testo locale
  autorevole) · failed (4xx di validazione, non ritentato) · 0/429/5xx: resta in
  coda e il flush si ferma
- supersede offline: supersedes_id che punta a un local_id viene rimappato al
  remote_id al flush (se il genitore non è sincronizzato → failed esplicito)
- submitOrQueue(): online → gateway + indicizzazione locale; offline → coda
- maybeBackgroundFlush() (single-flight) e flushQueueIfPending() per session_start
- stato/report: queued/synced/duplicate/failed, più vecchio, ultimo errore,
  last_flush, record pendenti in indice

Estensione:
- qmem_store: gateway giù → accoda e risponde con id locale, dimensione coda e
  spiegazione (details.queued/local_id/queue_size)
- fallback offline di session_start: flush in background (non blocca l'avvio)
- /qmem:local queue|flush; status con la coda; marker " in coda" nei risultati
  locali di qmem_search/qmem_get
- regole e skill: un record in coda NON è ancora nella memoria condivisa

CLI: store [--queue-only], queue, flush (+ status con la coda).
Test: scripts/test-local.mjs ora copre anche outbox → 24 controlli (flush con
2 sync + 1 duplicato 409 + 1 fallito 422, Idempotency-Key, rimappatura del
supersede, ricerca del record con l'ID remoto dopo il sync).

Verifiche: 24/24 test superati; demo reale su DB temporaneo: store accodato,
queue con local_id, flush con gateway giù → "fermato: HTTP 0" e voce che resta
in coda con l'errore registrato.
2026-09-13 17:29:09 +02:00
Matteo Benedetto 1832562a7f feat: indice locale SQLite/FTS5 + fallback testuale offline per qmem
Il gateway remoto non è sempre raggiungibile (VPN/nodi giù): finora le ricerche
fallivano e la conoscenza non era consultabile. Ora l'estensione mantiene un
indice locale testuale e vi degrada automaticamente.

Core (extensions/local-db.ts):
- schema SQLite con FTS5 (unicode61 remove_diacritics 2), trigger di sync,
  tabella meta per cursori/stato; usa node:sqlite (Node >= 22.5, nessuna
  dipendenza esterna), con soppressione del warning "experimental"
- import idempotente dalle sessioni pi (tutte le directory di progetto):
  qmem_store/qmem_correct (ID + testo integrale), qmem_get (payload completo),
  qmem_search (record osservati, anche creati da altri agenti)
- merge senza regressioni: le osservazioni povere (es. search senza
  project_id) non azzerano i campi già noti; superseded_by monotono
- ricerca FTS5 con filtri (kind/project/scope/level/topic), esclusione di
  superseduti e privati, ranking bm25, snippet, ripiego AND -> OR dichiarato
- enrich dal gateway (GET /v1/memories/{id}, pacing < rate limit, timeout 8s
  per richiesta, stop al primo guasto) e pull da /v1/memories:export (endpoint
  lato gateway previsto: se assente lo segnala senza errore)
- localGet per il recupero puntuale offline

Estensione:
- qmem_search: su 0/429/5xx degrada all'indice locale, risultati etichettati
  "INDICE LOCALE, ricerca testuale non neurale" + details.fallback=local_sqlite
- qmem_get: fallback locale per UUID
- qmem_store: avviso esplicito che il record NON è salvato (nessuna coda)
- rendering arricchito con project_id e flag privato (anche per il gateway)
- comando /qmem:local status|import|find|enrich|pull
- regole e skill aggiornate: quando si usa l'indice locale non applicare le
  soglie 0.45/0.60 (sono semantiche)

CLI standalone (stesso core): scripts/qmem-sqlite.mjs status|import|find|
enrich|pull (+ --json). Test: scripts/test-local.mjs (14 controlli, HOME
temporanea, sessioni sintetiche, gateway black-hole e stub HTTP).

Verifiche: 14/14 test superati; import reale 185 sessioni -> 1046 record unici
(1032 con testo, 986 attivi, 60 superseduti, 672 con project_id, 20 gruppi di
duplicati) in 2,8 MB; enrich con gateway giù si ferma in ~16s con messaggio
chiaro invece di restare appeso.
2026-09-13 15:53:56 +02:00
enne2 1b293efb2c test: aggiorna fake cross per soglia SUGGEST 0.85 2026-09-08 13:04:57 +02:00
enne2 681834d2a3 fix(gateway): calibrazione soglie cross-gate (BLOCK 0.90, SUGGEST 0.85 su distribuzione reale: dup 0.9989 / correlato 0.8766 / irrilevante 0.0), find_similar top-5, GET id invalido → 404 2026-09-08 13:04:45 +02:00
enne2 128059dd4b feat(gateway): strategie rerank oltre la search (A-F)
- A: gate store con cross-encoder — guardrail.decide async, conferma/scarta
  quasi-duplicati (CROSS_DUP_CONFIRMED/WEAK/LOW_COSINE), suggerimento
  supersedes in WARN, degrada a cosine-only se il reranker è giù
- B: verifica supersede — cross-score (nuovo,vecchio) sotto soglia →
  supersede_warning non bloccante + audit
- C: score composito in search — rerank + importance (nuovo campo payload)
  + recency decay (180gg) + authority, pesi SCORE_W_* da env
- E: multi-query — SearchIn.queries (max 3), pool unito con dedup, rerank
  unico; endpoint POST /v1/score come primitiva cross-encoder (F-lite)
- extension search.ts: param queries + rerank_score/composite in output
- D: scripts/consolidate.py — dedup periodico a coppie via cross-encoder
  con report ntfy e --apply via gateway
- test: 69 pass (+11 strategie); guardrail_version similarity-v2
2026-09-08 12:57:33 +02:00
enne2 fcd6b1670e feat(gateway): catena di fallback per gli embedding + retry transiente su Qdrant
- embed.py: EMBED_CHAIN (JSON per-nodo {name,url,api,key,timeout_ms}, api
  llamacpp|ollama), cooldown 60s sui nodi falliti, validazione dimensione
  EMBED_DIM, compatibilità legacy quando la catena è vuota
- state.py: ResilientQdrant — proxy che ritenta i metodi del client Qdrant
  su httpx.TransportError (store/search/transienti), errori applicativi
  esenti; contatore qdrant_retries in metriche
- metrics: qmem_embed_calls_total + durata per backend
- /v1/version espone embed_nodes; versione 2.10.0
- test: 11 nuovi (chain, cooldown, dim mismatch, legacy, retry transiente) — 58 pass
2026-09-08 12:19:16 +02:00
enne2 78a93ee771 fix(gateway): troncamento documenti rerank (RERANK_MAX_DOC_CHARS=800) — evita 500 batch-size e costi oltre il ctx 2026-09-08 12:12:38 +02:00
enne2 20766de540 feat(gateway): stadio rerank con catena di fallback resiliente (frigate→brain)
- gateway/rerank.py: catena da RERANK_CHAIN (JSON, per-nodo key+timeout),
  cooldown 60s sui nodi falliti, score sigmoide [0,1], degrada con grazia
  all'ordine di fusione se tutti i nodi sono giù
- routes: /v1/memories:search applica il rerank post-fusione (fetch esteso a
  RERANK_CANDIDATES), risposta con rerank{used,backend,took_ms}, flag
  per-query rerank=false; /v1/version espone lo stato rerank
- store: search() accetta limit esteso; models: SearchIn.rerank
- metrics: qmem_rerank_calls_total + durata per backend
- test: 10 nuovi (fallback, cooldown, degradazione, integrazione) — 46 pass
2026-09-08 12:10:27 +02:00
enne2 c21ef5e92a fix(qmem): ripristina GATE dentro QMEM_RULES (portabile con estensione), rimuovilo da AGENTS.md (globale+qwen-light) per evitare duplicazione — QMEM_RULES 526 token, AGENTS globale senza GATE 2026-08-28 16:55:22 +02:00
enne2 21c610835f perf(qmem): rimuovi GATE da QMEM_RULES (era duplicato in AGENTS.md) — regole qmem-only in inglese compatto 591→229 token; GATE vive ora in AGENTS.md (globale + qwen-light) 2026-08-28 16:52:20 +02:00
enne2 7097c4002b perf(qmem): skill qmem in inglese conciso — description sempre-on 70→42 token, corpo on-demand 1817→1028 token (totale -43%), procedure preservate 2026-08-28 16:38:33 +02:00
enne2 ca89679b3c perf(qmem): A2 schemi tool in inglese compatto — testo model-visible 2242→462 token (-79%) col tokenizer Qwen3.8; nomi/parametri/enum/default invariati, promptGuidelines duplicate rimosse (già in QMEM_RULES) 2026-08-28 16:27:49 +02:00
enne2 6c1c4f526f fix(qmem): frontmatter skill quotato (YAML ': ') + A1 regole iniettate compatte (procedure → skill qmem) 2026-08-28 16:02:35 +02:00
enne2 86ac1996fb feat(private): memorie riservate escluse dalle ricerche standard
- gateway: campo private (bool) in MemoryIn; filtro must_not private=true
  di default in search_filter; include_private in SearchIn per ricerche
  esplicite; campo private esposto in format_results
- estensione: parametro private in qmem_store, include_private in qmem_search
  (con descrizioni e avvertenze sui prompt dei modelli)
- testato su brain: record private invisibile alla ricerca standard,
  visibile solo con include_private=true; topic esplicito da solo non sblocca
- deploy: /opt/memory/gateway ricostruito (container memory-gateway)
2026-08-25 20:14:28 +02:00
enne2 5a5081e5d9 feat(prompt): regola vincolante identificazione macchina nei record qmem (hostname auto-rilevato, layout modulare) + bump v1.8.0 2026-08-24 22:55:51 +02:00
Matteo Benedetto 4776b4b496 refactor: split gateway and pi-qmem extension into modules
- gateway: separate config, models, state, audit, guardrail, embeddings,
  store, metrics, cleanup and routes; keep main.py as FastAPI bootstrap
- extension: split client/config, six tools, config command and rules;
  preserve jiti entrypoint and registrations
- Dockerfile copies the complete gateway module set
- tests: update monkeypatch boundaries for modular config/state
2026-08-24 16:19:19 +02:00
Matteo Benedetto e68fca3388 feat(reparenting): supersede di un record ri-parenta automaticamente i figli attivi
- gateway: nel blocco supersede, i figli attivi (parent_id == vecchio UUID,
  superseded_by vuoto) vengono ri-parentati al nuovo UUID; audit action
  'reparent'; risposta con campo 'reparented'. Un solo livello: i nipoti
  puntano agli UUID dei figli, invariati. I figli superseduti restano
  storici ancorati alla vecchia lineage.
- estensione: qmem_tree con fallback lineage (cerca figli anche per
  supersedes_id del root) per gli orfani pre-fix; qmem_correct riporta
  il numero di figli ri-parentati.
- test: 2 nuovi (ri-parenta figli attivi; ri-parenta solo attivi con
  figlio già superseduto). 34 pass.
- deploy su brain (10.8.0.3): main.py aggiornato, container ricreato,
  smoke test end-to-end OK (reparented=1).
2026-08-24 15:43:33 +02:00
Matteo Benedetto 0283d3964e feat(hierarchy): aggiunti tool qmem_tree e regole operative per gestione gerarchica L1/L2 2026-08-23 13:16:13 +02:00
Matteo Benedetto 369a2c2d9e feat(hierarchy): supporto metadati strutturati e gerarchici su Qdrant (parent_id, level, topic, links) 2026-08-23 13:07:16 +02:00
Matteo Benedetto fc11f878f6 feat(prompt): blocco Riflessione in QMEM_RULES (loop Reflexion-style) + bump v1.7.1
Regola auto-miglioramento: lezioni strutturate TRIGGER→CAUSA→AZIONE→VERIFICA
dopo fallimenti/successi sorprendenti; promozione a fact procedurale per
operazioni ricorrenti; consolidamento periodico. Vietate lezioni vaghe e
promozioni senza evidenza (anti-drift). Solo prompt: nessun cambiamento
server/API.
2026-08-22 16:10:17 +02:00
Matteo Benedetto a448241ba2 fix(monitoring): migliora leggibilità dashboard qmem 2026-08-20 15:13:08 +02:00
Matteo Benedetto 94e9bb44b1 docs: sez. 11.11 istanza frigate (embedding llama.cpp) + bump v2.7.0 2026-08-18 22:48:50 +02:00
Matteo Benedetto 2626649e5a feat: backend embedding configurabile — EMBED_API=ollama|llamacpp
- llama.cpp: /v1/embeddings OpenAI-compatible con Bearer key (EMBED_API_KEY)
- ollama (default): /api/embed invariato
- EMBED_URL sostituisce OLLAMA_URL (alias retrocompatibile)
2026-08-18 22:46:26 +02:00
Matteo Benedetto b7809fc92d feat(version): endpoint /v1/version + git_commit nel build Docker
- GET /v1/version (pubblico): espone version, git_commit, guardrail_version,
  guardrail_enabled, soglie, embedding_model
- /v1/status include version, git_commit, guardrail_version
- Dockerfile: ARG GIT_COMMIT / ENV GIT_COMMIT (default unknown)
- GATEWAY_VERSION 2.7.0, GIT_COMMIT da env
- Test: 2 nuovi (version endpoint, status git_commit) — 30/30 passano
2026-08-18 16:19:36 +02:00
Matteo Benedetto eccb2cb870 feat(guardrail): similarità pre-scrittura su POST /v1/memories
Guardrail deterministico FUORI dall'LLM (stessa architettura di egeos-copilot):
- Strato 1: text_hash SHA-256 normalizzato -> BLOCK 409 (EXACT_DUPLICATE)
- Strato 2: similarità semantica top-3 BGE-M3 cosine -> BLOCK/WARN/ALLOW
  (soglie configurabili: GUARDRAIL_BLOCK_THRESHOLD 0.85, WARN 0.70)
- Supersede esplicito bypassa il guardrail (correzione intenzionale)
- text_hash e flag guardrail nel payload; audit create_blocked
- Indice payload su text_hash
- Test: 6 nuovi (duplicato esatto, similarità alta/moderata, nessun candidato,
  supersede bypass, disabilitato, text_hash) — 28/28 passano
2026-08-18 12:58:28 +02:00
Matteo Benedetto 91ec4c82d7 feat: tool qmem_get per recupero deterministico record per UUID
Aggiunge qmem_get (GET /v1/memories/{id}) per recuperare un record
esatto per memory_id: usato quando un puntatore/playbook cita un ID.
La ricerca semantica (qmem_search) non garantisce di trovare record
lunghi con query generiche; qmem_get lo risolve in modo deterministico.
Restituisce anche record superseduti (lineage/audit).
2026-08-17 18:07:42 +02:00
enne2 24634f62fe chore: url di default del gateway = https://qmem.enne2.net (era http://10.8.0.3:8082) 2026-08-17 01:51:21 +02:00
Matteo Benedetto 6b4924d0db docs: sez. 11.10 incidente backfill (upsert→update_vectors) + lezioni apprese 2026-08-16 20:41:01 +02:00
Matteo Benedetto c4bfa1d5bf fix CRITICO: backfill usa update_vectors (non upsert) — preserva payload e vettore denso
INCIDENTE 2026-08-16: l'upsert parziale in Qdrant sostituisce l'intero punto,
cancellando payload e vettori densi di tutti i record. update_vectors aggiorna
solo il vettore specificato. Recovery: 278/330 record ricostruiti dalle sessioni
pi (qmem_store/qmem_correct con memory_id), 52 persi (agenti su altre macchine).
2026-08-16 20:39:12 +02:00
Matteo Benedetto 35be152be5 docs: permanenza esplicita — expires_at omesso = memoria permanente, mai cancellata dal cleanup
- descrizione parametro expires_at in qmem_store: chiarisce che il default è permanente
- skill qmem: sezione Igiene dei record con la garanzia di permanenza
- comportamento verificato sul server: record senza expires_at non selezionato dal filtro cleanup
2026-08-16 19:58:54 +02:00
Matteo Benedetto aed962cfcd chore: bump versioni (gateway 2.6.0, estensione 1.7.0) 2026-08-16 19:56:09 +02:00
Matteo Benedetto 027ba15817 docs: sez. 11.8 reranker rimandato (criteri futuri) + 11.9 hybrid retrieval 2026-08-16 19:56:00 +02:00
Matteo Benedetto 70f3699396 test: suite pytest per il gateway (21 test) + script di verifica pre-push
- conftest: FakeQdrant in-memory + mock embed (nessuna dipendenza da Qdrant/Ollama)
- test: validazione (project_id, kind, expires_at, confidence, lunghezza), auth (422/401/429), idempotency (replay/409/key diverse), supersede (404/409/lineage), filtri search, meta, status, metrics
- scripts/check.sh: esbuild (estensione) + pytest (gateway)
- requirements-dev.txt: pytest
2026-08-16 19:53:45 +02:00
Matteo Benedetto faa4e84611 feat: metriche gateway con push a VictoriaMetrics + dashboard Grafana
- raccolta in-memory: richieste per endpoint, latenza (sum/count), errori per status, search queries/hits, punti
- push periodico (30s) a VM via /api/v1/import/prometheus (pattern energy engine, timestamp ms)
- VM_PUSH_URL/VM_PUSH_INTERVAL/METRICS_ENABLED configurabili; default host.docker.internal:8428
- endpoint /v1/metrics (auth) per verifica manuale
- dashboard Grafana versionata in monitoring/qmem-dashboard.json (provisioning: /home/enne2/domotics/grafana/dashboards/)
- verificato sul server: /v1/metrics OK, 6 serie qmem_* in VM
2026-08-16 19:51:08 +02:00
Matteo Benedetto 6c2417eab7 feat: avviso duplicati in qmem_store (search pre-salvataggio, soglia 0.92, non blocca)
- prima di salvare: ricerca top-3 con min_score 0.92 e stesso project_id
- se trovati: avviso nella risposta con id/score/testo dei candidati e suggerimento qmem_correct
- il salvataggio non viene bloccato
- verificato sul server: record quasi identici trovati con score 1.0/0.9908
2026-08-16 19:45:18 +02:00
Matteo Benedetto eba9c19179 feat: campo confidence (high/medium/low, default medium) nei record
- gateway: MemoryIn + payload + risultati search; 422 su valore invalido
- estensione: confidence in qmem_store e qmem_correct (eredita dal superseduto), mostrato nei risultati
- backward compatible: record esistenti → confidence null
- verificato sul server: high/medium/default, 422 su invalida (istanza di test)
2026-08-16 19:43:30 +02:00
Matteo Benedetto 7abced1566 feat: hybrid retrieval (BM25 + vettoriale, RRF) opt-in via hybrid=true
- sparse vector bm25 (modifier IDF) + indice TEXT su text
- migrazione automatica: create_vector_name su collection esistente + backfill dei punti privi di sparse
- write: sparse vector a ogni upsert (fastembed Qdrant/bm25)
- search: hybrid=true → prefetch denso (min_score anti-rumore) + sparso, fusione RRF; default invariato (punteggi cosine)
- qdrant-client 1.13.0 → 1.19.0 (create_vector_name, query_points; search rimosso in 1.19)
- fastembed 0.5.1 + pre-download modello BM25 nel Dockerfile (appuser)
- estensione: parametro hybrid in qmem_search
- verificato sul server: migrazione + backfill OK, termine esatto trovato con RRF 1.0 (istanza di test)
2026-08-16 19:41:42 +02:00
Matteo Benedetto 2d354bfde6 feat: X-Request-ID per richiesta (header risposta + audit log)
- middleware: genera o accetta X-Request-ID, lo restituisce in header
- contextvar letto da _audit: correlazione richieste nei log
- verificato sul server: header + audit correlati (istanza di test)
2026-08-16 19:32:29 +02:00
Matteo Benedetto a2bb659e31 docs: sez. 11.7 accesso condiviso — rischio e criteri per chiavi per-scope future 2026-08-16 19:29:22 +02:00
Matteo Benedetto 3f7c3cee51 security: hash SHA-256 della query nell'audit log (niente contenuto in chiaro)
- query_hash (16 char) al posto di query[:80]
- verificato sul server: audit con query_hash, nessuna query in chiaro
2026-08-16 19:28:01 +02:00
Matteo Benedetto f30966a77c feat: rate limit leggero (30/min per IP) su /v1/status pubblico
- endpoint pubblico per healthcheck, protetto da abusi
- verificato sul server: 36 richieste → 30x200 + 6x429 (istanza di test)
2026-08-16 19:22:18 +02:00
Matteo Benedetto 87d59ccb5c feat: validazione expires_at ISO 8601 (422 con messaggio chiaro, niente fallback silenzioso)
- field_validator Pydantic su MemoryIn.expires_at
- verificato sul server: valida OK, invalida → 422 (istanza di test)
2026-08-16 19:21:01 +02:00
Matteo Benedetto 748356d769 perf: client httpx riusato per gli embedding (lazy, chiuso a shutdown)
- _get_http(): creazione lazy, keep-alive riusato tra le chiamate
- lifespan shutdown: aclose del client
- verificato sul server: status + search OK (istanza di test)
2026-08-16 19:18:41 +02:00
Matteo Benedetto ec0fdb972a refactor: lifespan al posto di @app.on_event (deprecato) + bump v2.5.0
- startup (collection+indici) e cleanup task nel context manager lifespan
- shutdown: cancel del cleanup task
- verificato sul server: startup OK, scrittura OK (istanza di test)
2026-08-16 19:17:19 +02:00
Matteo Benedetto 25ccb13f5b security: utente non-root (appuser, uid 10001) nel Dockerfile del gateway
- verificato sul server: whoami=appuser, status OK, scrittura OK (istanza di test)
2026-08-16 19:14:54 +02:00
Matteo Benedetto 625e569389 feat: soglia di sicurezza in qmem_correct con query (correctMinScore default 0.60)
- rifiuta il supersede se lo score del top-1 è sotto soglia (evidenza debole/rumorosa)
- messaggio guida: verifica con qmem_search e riprova con memory_id esplicito
- soglia configurabile in ~/.config/pi-qmem/config.json
2026-08-16 19:13:38 +02:00
Matteo Benedetto bb182dc370 feat: retry con backoff su errori transitori + timeoutMs applicato
- gatewayRequest: max 3 retry su 429/5xx/timeout/errore rete, backoff esponenziale + jitter, rispetta Retry-After (cap 10s)
- AbortError (annullamento utente) propagato, mai ritentato
- timeoutMs dalla config usato come fallback quando pi non fornisce signal
- testato: 503→200, sempre-503 (4 tentativi), 429+Retry-After, rete giù, abort
2026-08-16 19:12:12 +02:00
Matteo Benedetto 66e898f8b5 docs: sez. 11.6 idempotency nel playbook 2026-08-16 19:10:51 +02:00
Matteo Benedetto a7d4bbb4a8 feat: idempotency su POST /v1/memories (Idempotency-Key, replay → stessa risposta, payload diverso → 409)
- gateway: tabella in-memory con TTL 24h, hash canonico del payload, scoped per API key
- estensione: crypto.randomUUID() per operazione (store e correct), riusata su retry
- from __future__ import annotations (forward-reference _payload_hash)
2026-08-16 19:10:32 +02:00
Matteo Benedetto 9cb3008f4d docs: aggiungi gateway/README.md con istruzioni di deploy (referenziato dal README principale) 2026-08-16 19:01:49 +02:00
Matteo Benedetto 13d12b59b9 feat: regole QMEM_RULES imperative con trigger (MUST/MUST NOT + sequenza errore) 2026-08-13 18:58:44 +02:00
enne2 9925c37ea0 fix: virgole orfane dopo promptGuidelines (parse error) 2026-08-13 13:37:00 +02:00
enne2 c7088d4338 feat: regole comportamentali autocontenute nell'estensione (v1.5.0)
- promptGuidelines sui 4 tool (qmem_store/search/correct/meta): bullets nel Guidelines del system prompt, solo quando i tool sono attivi
- before_agent_start: blocco 'Regole pi-qmem' iniettato nel system prompt a ogni turno (solo se i tool qmem sono attivi) — regole vincolanti versionate con l'estensione
- skills/qmem/SKILL.md: procedura completa (punteggi, project_id, supersede, discovery) standard agentskills.io, distribuita via pi.skills nel manifest
- AGENTS.md ridotto a puntatore (riepilogo essenziale + rinvio a skill/tool)
- README aggiornato
2026-08-13 13:35:53 +02:00
enne2 72ebb949c8 feat: project_id obbligatorio (gateway v2.4.0 + estensione v1.4.0)
- gateway: MemoryIn.project_id required (min_length=1) → POST senza project_id = 422
- estensione: qmem_store project_id Type.String (non più Optional), descrizione con obbligo e rinvio a qmem_meta; qmem_correct resta opzionale (eredita dal superseduto)
- README/playbook sez. 11.5 aggiornati
2026-08-13 13:24:22 +02:00
enne2 334f9c6b44 fix(gateway): with_vectors (plurale) in qdrant.scroll per /v1/meta/overview 2026-08-13 13:09:10 +02:00
enne2 4b1f13c118 feat: discovery endpoint GET /v1/meta/overview + tool qmem_meta
- gateway v2.3.0: /v1/meta/overview (auth) con scope×kind, progetti, agenti, superseduti; scroll aggregato su soli campi metadata + cache TTL 60s invalidata su POST/DELETE/cleanup; audit action 'meta'
- estensione v1.3.0: tool qmem_meta (nessun parametro) che guida la ricerca settorializzata
- README/playbook (sez. 11.4)/AGENTS.md aggiornati
2026-08-13 13:06:10 +02:00
enne2 f6d9c5872d feat: min_score anti-rumore nella ricerca + igiene metadata
- gateway v2.2.0: SearchIn.min_score → score_threshold a livello Qdrant (filtra sotto soglia), audit e risposta con min_score
- estensione v1.2.0: qmem_search con min_score default 0.45 (guida punteggi in descrizione), marker ⚠️ per score<0.60, messaggio dedicato quando 0 risultati rilevanti
- dati: eliminato record rumore ('valuta preferita EUR'), assegnati project_id mancanti via supersede (domotics, pi-qmem x2, agy-pi)
- AGENTS.md: sezione interpretazione punteggi
2026-08-13 13:02:37 +02:00
enne2 1891f22a5e fix(qmem_correct): eredita kind/scope/project_id anche quando memory_id è passato direttamente (GET del record prima del supersede) 2026-08-13 12:42:58 +02:00
enne2 1713e68b82 feat: supersede di memorie false (qmem_correct, include_superseded, playbook sez. 11)
- gateway: supersedes_id validato (404/409), backlink superseded_by+superseded_at+supersede_reason sul vecchio record, indici keyword, search esclude i superseduti di default (include_superseded per lineage), risposta con lineage
- estensione: tool qmem_correct (memory_id o query), qmem_store con supersedes_id/supersede_reason, qmem_search con include_superseded
- README/playbook aggiornati, versione 1.1.0
2026-08-13 12:41:14 +02:00