File size: 10,662 Bytes
95eb75a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
/**
 * 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.