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

67 lines
3.6 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.
---
name: qmem
description: "Shared agent memory (Qdrant + BGE-M3). Search, store, correct, census, or explore hierarchical memory records. Use before any task needing prior knowledge, project discovery, or memory maintenance."
---
# qmem — Shared Memory
Gateway qmem.enne2.net → Qdrant + BGE-M3. No LLM writes: records are deliberate and structured. One API key; `agent_id` is provenance only.
## Tools
| Tool | Use |
|---|---|
| `qmem_search` | Semantic search with filters (kind, project_id, scope, top_k, min_score, parent_id, level, topic, hybrid) |
| `qmem_store` | Save a record (kind, project_id REQUIRED, scope, source, expires_at, supersedes_id, parent_id, level, topic, links) |
| `qmem_correct` | Fix a false record: a new version supersedes the old |
| `qmem_meta` | Discovery: projects, scopes×kinds, agents, superseded (choose filters) |
| `qmem_get` | Fetch one record exactly by UUID (O(1), includes superseded) |
| `qmem_tree` | Show full hierarchy (L1 root + L2 children) of a topic |
## Hierarchy (L1 root + L2 subtopics)
1. Save detail leaves as `L2_SUBTOPIC`, topic `MACRO/SUB`, with `parent_id` when known.
2. Save a macro index as `L1_ROOT`, topic `MACRO/ROOT`, `links=[{target_id: <l2-uuid>, predicate: "parent_of"}]`.
3. Filter search by `parent_id`, `level`, or `topic`.
4. From an L2 node, use its `parent_id` to `qmem_get` the root.
## Search scores
- **>=0.60** solid — use it
- **0.450.60** weak — verify the evidence first
- **<0.45** noise — filtered by default (`min_score` 0.45)
Empty results: reformulate, narrow kind/scope/project_id, or lower `min_score`. Score is not truth: check the cited source.
## Indice locale (fallback quando il gateway è giù)
Se il gateway non risponde, `qmem_search` usa l'**indice locale SQLite/FTS5**
(`~/.local/share/pi-qmem/qmem.sqlite`) e lo dichiara: `fallback: local_sqlite`.
In quel caso:
- la ricerca è **testuale (BM25)**, non neurale: nessuno score semantico e nessuna
soglia 0.45/0.60 da applicare;
- i risultati sono **osservazioni più vecchie** del gateway (storico ricostruito
dalle sessioni pi + ultimo enrich): verifica prima dell'uso;
- comandi: `/qmem:local status | import | find <query> | queue | flush | breaker [reset] | enrich | pull`
(`import` dalle sessioni, `enrich`/`pull` dal gateway quando torna online);
- **circuit breaker**: quando il gateway non risponde entro `connectTimeoutMs` (default 2,5 s) le
chiamate successive falliscono in ~0 ms **senza toccare la rete** per ~2 minuti (stato in
`~/.local/share/pi-qmem/breaker.json`): il fallback locale è immediato. Un 5xx o un body lento
sono invece "ambigui" (retry con `Retry-After`, apertura dopo 2 fallimenti). Reset:
`/qmem:local breaker reset`.
`qmem_store` accoda in locale: con il gateway giù il record entra nell'**outbox**
locale (SQLite), è subito ricercabile (marcato ⏳) e viene inviato al gateway al
ritorno della connessione (flush automatico su `session_start`, oppure
`/qmem:local flush`). Finché non è sincronizzato **non** è nella memoria
condivisa: i risultati locali marcati ⏳ non sono visibili agli altri agenti.
Stato e gestione della coda:
- `/qmem:local queue` — voci in attesa, tentativi, ultimo errore
- `/qmem:local flush` — invio immediato (idempotente: `Idempotency-Key` = id locale)
- esiti: `synced` (sul gateway, il record locale adotta l'ID remoto), `duplicate`
(409: era già presente, viene registrato l'ID del match), `failed` (4xx di
validazione: non ritentato in automatico)
- supersede offline: una correzione che punta a un record ancora locale viene
rimappata all'ID remoto al momento del flush