Files
pi-qmem/README.md
T
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

187 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# pi-qmem
Memoria centralizzata e condivisa per agenti AI — estensione per pi.
Salva e cerca record semantici (fatti, decisioni, preferenze, episodi) in un
Memory Gateway (FastAPI + Qdrant + BGE-M3) ospitato su un server remoto
raggiungibile via VPN. Nessun LLM in scrittura: l'agente salva record
deliberati e strutturati; il retrieval è vettoriale + filtri metadata.
## Installazione
```bash
pi install git:git.enne2.net/enne2/pi-qmem
```
Oppure copia `extensions/index.ts` in `~/.pi/agent/extensions/pi-qmem/`.
## Tool
| Tool | Descrizione |
|---|---|
| `qmem_store` | Salva un record di memoria (text, kind, agent_id, **project_id obbligatorio**, scope, source, expires_at, supersedes_id, supersede_reason). **Se il gateway è giù il record viene accodato localmente (outbox)** e inviato automaticamente al ritorno della connessione |
| `qmem_search` | Ricerca semantica su tutta la conoscenza condivisa (query, kind, project_id, scope, top_k, include_superseded, min_score). **Se il gateway non risponde degrada all'indice locale SQLite/FTS5** (testuale, etichettato `fallback: local_sqlite`) |
| `qmem_correct` | Corregge una memoria falsa: crea un nuovo record che **supersede** il vecchio (che resta in archivio marcato superseded) |
| `qmem_meta` | Discovery: panoramica di scope×kind, progetti, agenti e superseduti (per scegliere i filtri di ricerca) |
## Comando
`/qmem:config` — menu interattivo (TUI):
- 🌐 Imposta URL gateway
- 🔑 Cambia API key
- 🔌 Test connessione (verifica URL + validità chiave)
- 📋 Mostra configurazione
- ↩️ Annulla
Modalità CLI rapida: `/qmem:config url <URL> | apikey <KEY> | test`
Config salvata in `~/.config/pi-qmem/config.json` (0600):
```json
{
"url": "https://qmem.enne2.net",
"apiKey": "...",
"localDbPath": "~/.local/share/pi-qmem/qmem.sqlite",
"localFallback": true,
"offlineQueue": true,
"connectTimeoutMs": 15000,
"timeoutMs": 30000,
"breakerBaseMs": 120000,
"breakerMaxMs": 600000,
"breakerTripAfter": 3
}
```
`localFallback: false` disabilita il fallback in lettura; `offlineQueue: false`
disabilita l'accodamento offline in scrittura (lo store torna a fallire come
prima).
## Circuit breaker (fast-fail quando il gateway è irraggiungibile)
Due timeout distinti, per non confondere "gateway giù" con "elaborazione lunga":
| Fase | Chiave | Default | Significato |
|---|---|---|---|
| **connect + headers** | `connectTimeoutMs` | **15000** | nessuna risposta entro questo tempo → **gateway non raggiungibile** (fallimento definitivo). Vale per latenze di rete instabili (VPN/AP): un valore troppo stretto (2,5 s) apre il breaker anche se il gateway è su, con misure tipiche di 1,22,0 s per `status`/`search` |
| **body** (dopo gli header) | `timeoutMs` | 30000 | budget per il rerank/export/ricerca: un superamento è un fallimento **ambiguo** |
Comportamento:
- **Connessione fallita/nessuna risposta** (ECONNREFUSED, DNS, timeout di connect): **nessun retry**, il
**circuit breaker** si apre subito e resta aperto `breakerBaseMs` (**2 min**), con escalation
esponenziale fino a `breakerMaxMs` (10 min).
- **5xx o body lento**: fallimenti **ambigui** → retry con `Retry-After` e breaker solo dopo
`breakerTripAfter` (default 3) fallimenti consecutivi.
- **Breaker aperto**: le chiamate ritornano in **~0 ms senza toccare la rete** (`error:
"gateway_unreachable"`, `breaker_open: true`, `retry_in_ms`), quindi i tool passano subito al
fallback locale e l'outbox accoda senza attese.
- **Stato persistente**: `~/.local/share/pi-qmem/breaker.json` (env `QMEM_BREAKER_FILE`) → vale anche
per nuove sessioni, `/reload` e processi CLI. Cambiando `url` il breaker riparte chiuso.
- **Reset manuale**: `/qmem:local breaker reset` oppure `node scripts/qmem-sqlite.mjs breaker --reset`;
un successo lo richiude da solo. Stato: `/qmem:local breaker` o `qmem-sqlite breaker`.
## Indice locale (fallback offline)
Il gateway remoto non è sempre raggiungibile (VPN giù, nodi offline). L'estensione
mantiene quindi un **indice locale SQLite + FTS5** che permette di cercare
testualmente la conoscenza **senza gateway, senza modelli, senza dipendenze**:
- **DB**: `~/.local/share/pi-qmem/qmem.sqlite` (override: `localDbPath` in
`~/.config/pi-qmem/config.json` oppure env `QMEM_SQLITE`)
- **Ricostruzione**: dalle sessioni pi (`~/.pi/agent/sessions/<cwd>/*.jsonl`),
incrociando `toolCall` ↔ `toolResult`: `qmem_store` e `qmem_correct` forniscono
l'**ID del gateway** e il testo integrale, `qmem_get` il payload completo,
`qmem_search` i record visti (anche creati da altri agenti)
- **Ricerca**: FTS5 `unicode61 remove_diacritics 2` (accenti e prefissi),
ranking BM25, filtro dei superseduti/privati di default; se la query in AND non
trova nulla si ripiega su OR (match parziale, dichiarato)
- **Fallback automatico**: `qmem_search`/`qmem_get` usano l'indice locale quando
il gateway risponde 0/429/5xx, etichettando i risultati come **non neurali**
- **Outbox (store offline)**: `qmem_store` con gateway irraggiungibile accoda il
record in SQLite: è subito ricercabile (marcato ⏳ `pending`) e viene inviato a
`POST /v1/memories` al ritorno della connessione — `Idempotency-Key` = id locale
(retry senza duplicati), poi il record locale **adotta l'ID del gateway**.
Esiti: `synced` · `duplicate` (409, con l'ID del match) · `failed` (4xx di
validazione). Le correzioni che puntano a un record ancora locale vengono
rimappate all'ID remoto al flush. Trigger: `session_start` (background, non
blocca l'avvio), dopo uno store riuscito, o `/qmem:local flush`
- **Arricchimento e pull**: quando il gateway torna online, `enrich` completa
testo/`project_id`/`private`/stato supersede via `GET /v1/memories/{id}`, e
`pull` sincronizza dall'**export paginato** (`GET /v1/memories:export`,
disponibile dal gateway **2.12.0** insieme al **soft delete**): i **tombstone**
(`deleted_at`) arrivano col record e vengono esclusi dall'indice locale
(visibili con `/qmem:local find --deleted`)
Comandi (TUI) e CLI standalone:
```bash
/qmem:local status # record, copertura, lag, duplicati
/qmem:local import # ricostruisce/aggiorna dalle sessioni pi
/qmem:local find "circuit breaker" # ricerca testuale locale
/qmem:local queue # stato della coda (in attesa/sync/dup/fallite)
/qmem:local flush # invia subito la coda al gateway
/qmem:local enrich [--all] # arricchisce dal gateway
/qmem:local pull # pull incrementale dall'export
# equivalente standalone (stesso core, nessuna dipendenza)
node scripts/qmem-sqlite.mjs status|import|find "query"|enrich|pull
node scripts/qmem-sqlite.mjs store --project P --text "..." [--queue-only]
node scripts/qmem-sqlite.mjs queue|flush
node scripts/test-local.mjs # suite di test (37 controlli, HOME temporanea)
```
Nota: un body non completato **non** viene più restituito come "successo con dati vuoti"
(prima `res.json().catch(() => ({}))` mascherava il timeout: l'agente vedeva "nessun risultato"
invece del fallback locale).
Limiti dichiarati: è uno **storico osservato** (più vecchio del gateway), la
ricerca è **lessicale** (nessuno score 0.45/0.60: non applicare le soglie
semantiche) e un record in coda (⏳) **non è ancora nella memoria condivisa**:
sarà visibile agli altri agenti solo dopo il flush. La coda è locale alla
macchina (nessuna sincronizzazione tra macchine diverse).
## Regole comportamentali (autocontenute)
Le regole vincolanti (obbligo `project_id`, punteggi, correzione/supersede, discovery, **identificazione macchina nei record locali**) sono **distribuite con l'estensione**, senza toccare AGENTS.md:
- **`promptGuidelines`** sui 4 tool (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); include la sezione **Identificazione macchina** con `MACCHINA CORRENTE` rilevata dinamicamente dall'estensione (`os.hostname()` + `/etc/os-release`)
- **Skill `qmem`** (`skills/qmem/SKILL.md`, standard agentskills.io) → procedura completa on-demand, caricabile con `/skill:qmem`
- Manifest: `pi.skills` nel package.json
## Gateway (componente server)
Il Memory Gateway FastAPI + Qdrant **non è più duplicato in questo package**:
la fonte unica è il repository dedicato
```
git:git.enne2.net/enne2/qmem-gateway (privato)
```
che contiene il codice (`gateway/`), il deploy (`docker-compose.yml` con Qdrant
1.19 + gateway, `.env`, `.gitignore`), la suite di test e il README operativo.
Su questa macchina è clonato in `~/dev/qmem-gateway`.
Motivo della dedup: la copia qui dentro era byte-identica al repo canonico
(GATEWAY_VERSION 2.11.0 / guardrail similarity-v2) e manteneva due fonti
potenzialmente divergenti. Storia completa del codice rimossa:
`git log -- gateway/` (ultimo commit prima della rimozione).
## Architettura
```
pi (estensione) ──HTTPS/VPN──▶ Memory Gateway (FastAPI) ──▶ Qdrant 1.19
└──▶ Ollama BGE-M3 (embedding locale)
```
- Accesso condiviso: una chiave API con accesso completo in lettura/scrittura
- `agent_id` è solo metadata di provenienza, non isolamento
- Rate limit, audit log, cleanup automatico dei record scaduti
- **Correzioni**: i record sono immutabili; correggere una memoria falsa = nuovo record che supersede il vecchio (`qmem_correct` o `qmem_store` con `supersedes_id`). Il vecchio resta in archivio con `superseded_by`, escluso dalla ricerca di default (`include_superseded=true` per la lineage)
## Licenza
MIT