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).
This commit is contained in:
@@ -2,6 +2,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { Type } from "typebox";
|
||||
import { gatewayRequest, loadConfig } from "../shared";
|
||||
import { localDbPath, submitOrQueue } from "../local-db.ts";
|
||||
import { breakerInfo } from "../shared.ts";
|
||||
|
||||
export function registerQmemStore(pi: ExtensionAPI) {
|
||||
pi.registerTool({
|
||||
@@ -111,6 +112,13 @@ export function registerQmemStore(pi: ExtensionAPI) {
|
||||
`⚠️ Gateway non raggiungibile (HTTP ${submitted.status}): record ACCODATO in locale (outbox).\n` +
|
||||
`id locale: ${submitted.local_id}\n` +
|
||||
`in coda: ${submitted.queue_size} record\n` +
|
||||
(() => {
|
||||
const br = breakerInfo();
|
||||
return br.open
|
||||
? `circuit breaker aperto: nessun nuovo tentativo verso il gateway per altri ${Math.ceil(br.remainingMs / 1000)}s ` +
|
||||
`(ultimo errore: ${br.lastError ?? "?"}; per forzare: /qmem:local breaker reset)\n`
|
||||
: "";
|
||||
})() +
|
||||
`Il contenuto è già ricercabile offline (indice locale, marcato ⏳) e verrà caricato automaticamente al ritorno della connessione ` +
|
||||
`(/qmem:local flush per forzare, /qmem:local queue per lo stato).`,
|
||||
},
|
||||
@@ -120,6 +128,9 @@ export function registerQmemStore(pi: ExtensionAPI) {
|
||||
local_id: submitted.local_id,
|
||||
queue_size: submitted.queue_size,
|
||||
gateway_status: submitted.status,
|
||||
breaker: submitted.breaker
|
||||
? { open: submitted.breaker.open, remaining_ms: Math.round(submitted.breaker.remainingMs), failures: submitted.breaker.failures }
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user