AUDIT / src /lib /agentTelemetry.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 3)
95eb75a verified
Raw
History Blame Contribute Delete
10.7 kB
/**
* agentTelemetry.ts — Sistema di telemetria unificato per tutti i blocchi verdict-based
*
* Raccoglie contatori per ogni sistema di verifica/controllo qualità del loop agente.
* Permette di calibrare le soglie nel tempo (es: complexity >= 7, routeQualityScore < 0.85).
*
* Sistemi tracciati:
* - crossCritic → PASS / FAIL / UNCERTAIN
* - postResponseVerify → ok / unsafe / claim_vs_reality / [issue type]
* - goalVerify → pass / fail
* - planCritic → validated / rejected / skipped
* - validationGate → accept / repaired / retry / reject
* - finalOutputGate → pass / garbage / empty / no-proof
* - inlineVerify → safe / [issue: hallucination | contradiction | ...]
* - injectionGuard → detected / clean
*
* Design:
* - localStorage + in-memory cache (identico a scenarioFingerprintBlock — BUG-1-FIX applicato)
* - Fail-open totale: qualsiasi errore → noop silenzioso
* - Safari-safe: no Worker, no SharedArrayBuffer
* - Zero dipendenze esterne
* - Additive-only: non modifica mai il comportamento dei blocchi chiamanti
*
* @module agentTelemetry
*/
const LS_KEY = "agent_telemetry_v1";
const MAX_STORE = 200; // max entries totali prima di pruning (evita crescita unbounded)
// ─── Tipi ─────────────────────────────────────────────────────────────────────
export type TelemetrySystem =
| "crossCritic"
| "postResponseVerify"
| "goalVerify"
| "planCritic"
| "validationGate"
| "finalOutputGate"
| "inlineVerify"
| "injectionGuard";
interface VerdictStats {
count: number;
lastSeenMs: number;
}
interface SystemStats {
[verdict: string]: VerdictStats;
}
interface TelemetryStore {
[system: string]: SystemStats;
}
// ─── Cache in-memory (lazy-load, flush on write — BUG-1-FIX pattern) ──────────
let _cache: TelemetryStore | null = null;
function _load(): TelemetryStore {
if (_cache !== null) return _cache;
try {
const raw = typeof localStorage !== "undefined" ? localStorage.getItem(LS_KEY) : null;
_cache = raw ? (JSON.parse(raw) as TelemetryStore) : {};
return _cache;
} catch { _cache = {}; return _cache; }
}
function _save(store: TelemetryStore): void {
_cache = store;
try {
if (typeof localStorage !== "undefined") localStorage.setItem(LS_KEY, JSON.stringify(store));
} catch { /* Safari private mode — non-fatal */ }
}
function _prune(store: TelemetryStore): TelemetryStore {
// Conta entries totali — se sopra MAX_STORE, rimuovi le più vecchie
let total = 0;
for (const sys of Object.values(store)) total += Object.keys(sys).length;
if (total <= MAX_STORE) return store;
// Semplice: rimuovi i sistemi con meno dati (sacrifica i meno usati)
const sorted = Object.entries(store).sort(([, a], [, b]) => {
const sumA = Object.values(a).reduce((s, v) => s + v.count, 0);
const sumB = Object.values(b).reduce((s, v) => s + v.count, 0);
return sumB - sumA;
});
const pruned: TelemetryStore = {};
let kept = 0;
for (const [sys, stats] of sorted) {
if (kept + Object.keys(stats).length > MAX_STORE) break;
pruned[sys] = stats;
kept += Object.keys(stats).length;
}
return pruned;
}
// ─── API pubblica ──────────────────────────────────────────────────────────────
/**
* Registra un evento verdict per un sistema.
* Fire-and-forget, mai lancia eccezioni.
*
* @param system - Nome del sistema (es: "crossCritic")
* @param verdict - Risultato (es: "PASS", "fail", "retry")
*/
export function record(system: TelemetrySystem | string, verdict: string): void {
try {
const store = _load();
if (!store[system]) store[system] = {};
const sys = store[system]!;
const key = verdict.toLowerCase();
const prev = sys[key] ?? { count: 0, lastSeenMs: 0 };
sys[key] = { count: prev.count + 1, lastSeenMs: Date.now() };
_save(_prune(store));
// Gap N4: auto-sync al backend ogni _SYNC_EVERY chiamate (fire-and-forget).
// _syncCounter e _SYNC_EVERY sono definiti più in basso ma inizializzati prima
// di qualsiasi call (modulo caricato prima dell'uso). syncToBackend è hoisted.
_syncCounter++;
if (_syncCounter >= _SYNC_EVERY) {
_syncCounter = 0;
syncToBackend().catch(() => {}); // eslint-disable-line @typescript-eslint/no-floating-promises
}
} catch { /* fail-open */ }
}
export interface TelemetryEntry {
system: string;
verdict: string;
count: number;
lastSeenMs: number;
}
/**
* Restituisce tutti gli eventi registrati ordinati per sistema e count.
* Per diagnostica e calibrazione soglie.
*/
export function getReport(): TelemetryEntry[] {
try {
const store = _load();
const entries: TelemetryEntry[] = [];
for (const [system, stats] of Object.entries(store)) {
for (const [verdict, vs] of Object.entries(stats)) {
entries.push({ system, verdict, count: vs.count, lastSeenMs: vs.lastSeenMs });
}
}
return entries.sort((a, b) =>
a.system.localeCompare(b.system) || b.count - a.count
);
} catch { return []; }
}
/**
* Restituisce il report per un singolo sistema (es: calibrazione soglie).
* Esempio: getSystemReport("crossCritic") → { pass: 12, fail: 3, uncertain: 1 }
*/
export function getSystemReport(system: string): Record<string, number> {
try {
const store = _load();
const stats = store[system] ?? {};
return Object.fromEntries(Object.entries(stats).map(([v, s]) => [v, s.count]));
} catch { return {}; }
}
/**
* Calcola il fail rate di un sistema (fail / (pass + fail)).
* Utile per calibrare se la soglia di trigger è troppo alta o troppo bassa.
*
* @param system - Sistema da analizzare
* @param failVerdict - Chiave che conta come "fail" (default: "fail")
* @param passVerdict - Chiave che conta come "pass" (default: "pass")
* @returns null se non abbastanza dati (< 5 eventi totali)
*/
export function getFailRate(
system: string,
failVerdict = "fail",
passVerdict = "pass",
): number | null {
try {
const rep = getSystemReport(system);
const fail = rep[failVerdict] ?? 0;
const pass = rep[passVerdict] ?? 0;
const total = fail + pass;
if (total < 5) return null;
return Math.round((fail / total) * 1000) / 1000;
} catch { return null; }
}
/** Solo per unit test — non usare in produzione. */
export function _resetTelemetry(): void {
_cache = null;
try {
if (typeof localStorage !== "undefined") localStorage.removeItem(LS_KEY);
} catch { /* noop */ }
}
// ─── Gap N4: Backend sync — verdetti persistenti cross-session/device ─────────
// Obiettivo: agentTelemetry usava solo localStorage → dati di calibrazione persi
// su altri device o dopo clear cache. Questi helper sincronizzano con il backend
// HF Space (/data/agent_telemetry.json) per persistenza vera.
//
// Flusso:
// BOOT → loadFromBackend() → merge server store in localStorage (silent, 2s delay)
// RECORD → ogni 10 record → syncToBackend() fire-and-forget (non blocca mai)
//
// Merge: additive — max(count), max(lastSeenMs) — i contatori non calano mai.
// Fail-open totale: qualsiasi errore di rete → silent noop, localStorage non toccato.
let _syncCounter = 0; // conta i record dall'ultimo sync
const _SYNC_EVERY = 10; // sync al backend ogni N record
/** Merge additive locale: max(count), max(lastSeenMs) per ogni (system, verdict). */
function _mergeLocal(a: TelemetryStore, b: TelemetryStore): TelemetryStore {
const out: TelemetryStore = {};
// Copia a
for (const [sys, verdicts] of Object.entries(a)) {
out[sys] = { ...verdicts };
}
// Merge b sopra a
for (const [sys, verdicts] of Object.entries(b)) {
if (!out[sys]) out[sys] = {};
for (const [verdict, vs] of Object.entries(verdicts)) {
const prev = out[sys]![verdict] ?? { count: 0, lastSeenMs: 0 };
out[sys]![verdict] = {
count: Math.max(prev.count, vs.count),
lastSeenMs: Math.max(prev.lastSeenMs, vs.lastSeenMs),
};
}
}
return out;
}
/**
* Carica dati dal backend e fa merge con il localStorage locale.
* Chiamata una volta al boot (delay 2s, non-blocking).
* GET /api/agent-telemetry/sync → merge → _save().
*/
export async function loadFromBackend(): Promise<void> {
try {
const { ENV } = await import("@/config/env");
const base = ENV.BACKEND_URL;
if (!base) return;
const res = await fetch(base.replace(/\/$/, "") + "/api/agent-telemetry/sync", {
method: "GET",
signal: AbortSignal.timeout(6000),
});
if (!res.ok) return;
const body = await res.json() as { ok?: boolean; data?: unknown };
if (!body.ok || !body.data || typeof body.data !== "object") return;
const local = _load();
const merged = _mergeLocal(local, body.data as TelemetryStore);
_save(merged);
} catch { /* fail-open — nessun impatto sull'agente */ }
}
/**
* Sincronizza il localStorage corrente con il backend (merge bidirezionale).
* Il server ritorna il merged: aggiorniamo localStorage con dati da altri device.
* POST /api/agent-telemetry/sync → merge → _save().
* Fire-and-forget — non restituisce mai un errore.
*/
export async function syncToBackend(): Promise<void> {
try {
const { ENV } = await import("@/config/env");
const base = ENV.BACKEND_URL;
if (!base) return;
const store = _load();
const res = await fetch(base.replace(/\/$/, "") + "/api/agent-telemetry/sync", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ data: store }),
signal: AbortSignal.timeout(8000),
});
if (!res.ok) return;
const body = await res.json() as { ok?: boolean; merged?: unknown };
if (!body.ok || !body.merged || typeof body.merged !== "object") return;
// Aggiorna localStorage con il merged del server (include dati da altri device)
_save(body.merged as TelemetryStore);
} catch { /* fail-open */ }
}
// ── Auto-boot: carica dal backend 2s dopo il primo import ──────────────────
if (typeof window !== "undefined" && typeof fetch !== "undefined") {
setTimeout(() => {
loadFromBackend().catch(() => {});
}, 2000);
}
// Gap N4: auto-sync integrato direttamente in record() — counter sopra.