@cleocode/core 2026.4.37 → 2026.4.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/hooks/handlers/task-hooks.d.ts.map +1 -1
  2. package/dist/hooks/handlers/task-hooks.js +11 -0
  3. package/dist/hooks/handlers/task-hooks.js.map +1 -1
  4. package/dist/index.js +1048 -33
  5. package/dist/index.js.map +4 -4
  6. package/dist/internal.d.ts +3 -1
  7. package/dist/internal.d.ts.map +1 -1
  8. package/dist/internal.js +3 -1
  9. package/dist/internal.js.map +1 -1
  10. package/dist/memory/brain-lifecycle.d.ts +2 -0
  11. package/dist/memory/brain-lifecycle.d.ts.map +1 -1
  12. package/dist/memory/decisions.d.ts.map +1 -1
  13. package/dist/memory/decisions.js +18 -0
  14. package/dist/memory/decisions.js.map +1 -1
  15. package/dist/memory/engine-compat.d.ts +17 -0
  16. package/dist/memory/engine-compat.d.ts.map +1 -1
  17. package/dist/memory/engine-compat.js +36 -0
  18. package/dist/memory/engine-compat.js.map +1 -1
  19. package/dist/memory/graph-memory-bridge.d.ts +158 -0
  20. package/dist/memory/graph-memory-bridge.d.ts.map +1 -0
  21. package/dist/memory/graph-memory-bridge.js +519 -0
  22. package/dist/memory/graph-memory-bridge.js.map +1 -0
  23. package/dist/memory/index.d.ts +1 -0
  24. package/dist/memory/index.d.ts.map +1 -1
  25. package/dist/memory/index.js +2 -0
  26. package/dist/memory/index.js.map +1 -1
  27. package/dist/memory/learnings.d.ts.map +1 -1
  28. package/dist/memory/learnings.js +18 -0
  29. package/dist/memory/learnings.js.map +1 -1
  30. package/dist/memory/llm-extraction.js.map +1 -1
  31. package/dist/memory/patterns.d.ts.map +1 -1
  32. package/dist/memory/patterns.js +18 -0
  33. package/dist/memory/patterns.js.map +1 -1
  34. package/dist/memory/quality-feedback.d.ts +129 -0
  35. package/dist/memory/quality-feedback.d.ts.map +1 -0
  36. package/dist/memory/quality-feedback.js +449 -0
  37. package/dist/memory/quality-feedback.js.map +1 -0
  38. package/dist/memory/sleep-consolidation.d.ts +98 -0
  39. package/dist/memory/sleep-consolidation.d.ts.map +1 -0
  40. package/dist/memory/sleep-consolidation.js +706 -0
  41. package/dist/memory/sleep-consolidation.js.map +1 -0
  42. package/dist/memory/temporal-supersession.d.ts +155 -0
  43. package/dist/memory/temporal-supersession.d.ts.map +1 -0
  44. package/dist/memory/temporal-supersession.js +406 -0
  45. package/dist/memory/temporal-supersession.js.map +1 -0
  46. package/dist/tasks/complete.d.ts.map +1 -1
  47. package/package.json +8 -8
  48. package/src/hooks/handlers/task-hooks.ts +11 -0
  49. package/src/internal.ts +12 -0
  50. package/src/memory/__tests__/graph-memory-bridge.test.ts +357 -0
  51. package/src/memory/__tests__/llm-extraction.test.ts +17 -0
  52. package/src/memory/__tests__/quality-feedback.test.ts +418 -0
  53. package/src/memory/__tests__/sleep-consolidation.test.ts +790 -0
  54. package/src/memory/__tests__/temporal-supersession.test.ts +534 -0
  55. package/src/memory/brain-lifecycle.ts +13 -0
  56. package/src/memory/decisions.ts +24 -0
  57. package/src/memory/engine-compat.ts +37 -0
  58. package/src/memory/graph-memory-bridge.ts +751 -0
  59. package/src/memory/index.ts +2 -0
  60. package/src/memory/learnings.ts +24 -0
  61. package/src/memory/patterns.ts +24 -0
  62. package/src/memory/quality-feedback.ts +640 -0
  63. package/src/memory/sleep-consolidation.ts +932 -0
  64. package/src/memory/temporal-supersession.ts +568 -0
  65. package/src/store/__tests__/performance-safety.test.ts +4 -4
  66. package/src/tasks/complete.ts +20 -0
@@ -0,0 +1,640 @@
1
+ /**
2
+ * Memory Quality Feedback Loop — BRAIN self-improvement system.
3
+ *
4
+ * Closes the retrieval→usage→outcome loop so that memory quality scores
5
+ * reflect real-world utility, not just insert-time heuristics.
6
+ *
7
+ * Three operations:
8
+ *
9
+ * 1. trackMemoryUsage — record whether a retrieved memory was actually used
10
+ * by an agent after task completion.
11
+ *
12
+ * 2. correlateOutcomes — scan the retrieval log, join against task outcomes,
13
+ * and apply quality adjustments:
14
+ * - Memory retrieved before a successful task completion: +0.05
15
+ * - Memory retrieved before a failed task: -0.05
16
+ * - Memory never retrieved in 30 days: flagged for pruning
17
+ *
18
+ * 3. getMemoryQualityReport — dashboard metrics over the entire brain.db.
19
+ *
20
+ * Schema addition: brain_usage_log (entry_id, task_id, used, outcome, created_at).
21
+ * The table is self-healing — created on first access.
22
+ *
23
+ * @task T555
24
+ */
25
+
26
+ import { typedAll } from '../store/typed-query.js';
27
+
28
+ // ============================================================================
29
+ // Types
30
+ // ============================================================================
31
+
32
+ /** Outcome of a task that used a retrieved memory. */
33
+ export type MemoryOutcome = 'success' | 'failure' | 'unknown';
34
+
35
+ /** A single row in brain_usage_log. */
36
+ export interface UsageLogRow {
37
+ id: number;
38
+ entry_id: string;
39
+ task_id: string | null;
40
+ used: number; // SQLite integer boolean: 1 = used, 0 = not used
41
+ outcome: string;
42
+ created_at: string;
43
+ }
44
+
45
+ /** Aggregate memory statistics for the quality report. */
46
+ export interface MemoryQualityReport {
47
+ /** Total rows in brain_retrieval_log. */
48
+ totalRetrievals: number;
49
+ /** Count of distinct entry IDs that have ever been retrieved. */
50
+ uniqueEntriesRetrieved: number;
51
+ /** Ratio of usage_log rows with used=1 over total usage_log rows (0–1). */
52
+ usageRate: number;
53
+ /** Top 10 entries sorted by citation_count descending. */
54
+ topRetrieved: Array<{ id: string; type: string; title: string; citationCount: number }>;
55
+ /** Up to 10 entries with citation_count = 0, candidates for pruning. */
56
+ neverRetrieved: Array<{ id: string; type: string; title: string; qualityScore: number }>;
57
+ /** Distribution of quality scores bucketed into [0.0,0.3), [0.3,0.6), [0.6,1.0]. */
58
+ qualityDistribution: {
59
+ low: number; // < 0.3
60
+ medium: number; // 0.3–0.6
61
+ high: number; // > 0.6
62
+ };
63
+ /** Count of entries per memory tier. */
64
+ tierDistribution: {
65
+ short: number;
66
+ medium: number;
67
+ long: number;
68
+ unknown: number;
69
+ };
70
+ /** Ratio of entries with quality_score < 0.3 to total entries. */
71
+ noiseRatio: number;
72
+ }
73
+
74
+ /** Result of a correlateOutcomes run. */
75
+ export interface CorrelateOutcomesResult {
76
+ /** Number of entries that received a quality boost (+0.05). */
77
+ boosted: number;
78
+ /** Number of entries that received a quality penalty (-0.05). */
79
+ penalized: number;
80
+ /** Number of entries flagged for pruning (prune_candidate = 1). */
81
+ flaggedForPruning: number;
82
+ /** Timestamp of this run (ISO string). */
83
+ ranAt: string;
84
+ }
85
+
86
+ // ============================================================================
87
+ // Internal: schema bootstrap
88
+ // ============================================================================
89
+
90
+ /**
91
+ * Ensure brain_usage_log exists. Safe to call multiple times — uses
92
+ * CREATE TABLE IF NOT EXISTS. Returns silently on error (best-effort).
93
+ */
94
+ async function ensureUsageLogTable(projectRoot: string): Promise<void> {
95
+ const { getBrainDb, getBrainNativeDb } = await import('../store/brain-sqlite.js');
96
+ await getBrainDb(projectRoot);
97
+ const nativeDb = getBrainNativeDb();
98
+ if (!nativeDb) return;
99
+
100
+ try {
101
+ nativeDb
102
+ .prepare(
103
+ `CREATE TABLE IF NOT EXISTS brain_usage_log (
104
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
105
+ entry_id TEXT NOT NULL,
106
+ task_id TEXT,
107
+ used INTEGER NOT NULL DEFAULT 0,
108
+ outcome TEXT NOT NULL DEFAULT 'unknown',
109
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
110
+ )`,
111
+ )
112
+ .run();
113
+
114
+ // Index to speed up correlateOutcomes JOIN on entry_id.
115
+ nativeDb
116
+ .prepare(
117
+ `CREATE INDEX IF NOT EXISTS idx_brain_usage_log_entry_id
118
+ ON brain_usage_log(entry_id)`,
119
+ )
120
+ .run();
121
+
122
+ // Index to speed up pruning scan (entries never retrieved).
123
+ nativeDb
124
+ .prepare(
125
+ `CREATE INDEX IF NOT EXISTS idx_brain_usage_log_task_id
126
+ ON brain_usage_log(task_id)`,
127
+ )
128
+ .run();
129
+ } catch {
130
+ // best-effort: DDL failures are non-fatal
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Ensure prune_candidate column exists on all typed brain tables.
136
+ * Added lazily so existing schemas are not broken.
137
+ */
138
+ async function ensurePruneCandidateColumn(projectRoot: string): Promise<void> {
139
+ const { getBrainDb, getBrainNativeDb } = await import('../store/brain-sqlite.js');
140
+ await getBrainDb(projectRoot);
141
+ const nativeDb = getBrainNativeDb();
142
+ if (!nativeDb) return;
143
+
144
+ const tables = [
145
+ 'brain_decisions',
146
+ 'brain_patterns',
147
+ 'brain_learnings',
148
+ 'brain_observations',
149
+ ] as const;
150
+
151
+ for (const tbl of tables) {
152
+ try {
153
+ nativeDb.prepare(`ALTER TABLE ${tbl} ADD COLUMN prune_candidate INTEGER DEFAULT 0`).run();
154
+ } catch {
155
+ // Column already exists — silently continue
156
+ }
157
+ }
158
+ }
159
+
160
+ // ============================================================================
161
+ // 1. trackMemoryUsage
162
+ // ============================================================================
163
+
164
+ /**
165
+ * Record whether a retrieved memory entry was actually used by an agent.
166
+ *
167
+ * Call this after task completion, once the agent has decided which retrieved
168
+ * entries were referenced. Inserts a row into brain_usage_log; the
169
+ * correlateOutcomes pass will read these rows to adjust quality scores.
170
+ *
171
+ * @param projectRoot - Project root directory
172
+ * @param memoryId - The brain entry ID (e.g. "O-...", "D-...", "P-...")
173
+ * @param used - Whether the agent actually used this entry
174
+ * @param taskId - Optional task ID for outcome correlation
175
+ * @param outcome - Optional task outcome; defaults to 'unknown' until correlated
176
+ */
177
+ export async function trackMemoryUsage(
178
+ projectRoot: string,
179
+ memoryId: string,
180
+ used: boolean,
181
+ taskId?: string,
182
+ outcome: MemoryOutcome = 'unknown',
183
+ ): Promise<void> {
184
+ if (!memoryId?.trim()) return;
185
+
186
+ await ensureUsageLogTable(projectRoot);
187
+
188
+ const { getBrainNativeDb } = await import('../store/brain-sqlite.js');
189
+ const nativeDb = getBrainNativeDb();
190
+ if (!nativeDb) return;
191
+
192
+ const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
193
+
194
+ try {
195
+ nativeDb
196
+ .prepare(
197
+ `INSERT INTO brain_usage_log (entry_id, task_id, used, outcome, created_at)
198
+ VALUES (?, ?, ?, ?, ?)`,
199
+ )
200
+ .run(memoryId, taskId ?? null, used ? 1 : 0, outcome, now);
201
+ } catch {
202
+ // best-effort
203
+ }
204
+ }
205
+
206
+ // ============================================================================
207
+ // 2. correlateOutcomes
208
+ // ============================================================================
209
+
210
+ /**
211
+ * Resolve the table name for a brain entry based on its ID prefix.
212
+ */
213
+ function tableForId(id: string): string | null {
214
+ if (id.startsWith('D-') || /^D\d/.test(id)) return 'brain_decisions';
215
+ if (id.startsWith('P-') || /^P\d/.test(id)) return 'brain_patterns';
216
+ if (id.startsWith('L-') || /^L\d/.test(id)) return 'brain_learnings';
217
+ if (id.startsWith('O-') || id.startsWith('CM-') || /^O/.test(id)) return 'brain_observations';
218
+ return null;
219
+ }
220
+
221
+ /**
222
+ * Apply a quality delta (+0.05 or -0.05) to a brain entry, clamped to [0.0, 1.0].
223
+ */
224
+ function applyQualityDelta(
225
+ nativeDb: ReturnType<typeof import('../store/brain-sqlite.js')['getBrainNativeDb']>,
226
+ table: string,
227
+ id: string,
228
+ delta: number,
229
+ now: string,
230
+ ): void {
231
+ if (!nativeDb) return;
232
+ try {
233
+ nativeDb
234
+ .prepare(
235
+ `UPDATE ${table}
236
+ SET quality_score = MAX(0.0, MIN(1.0, COALESCE(quality_score, 0.5) + ?)),
237
+ updated_at = ?
238
+ WHERE id = ?`,
239
+ )
240
+ .run(delta, now, id);
241
+ } catch {
242
+ // best-effort: column may differ in older schemas
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Analyse the retrieval log and usage log against task outcomes, then
248
+ * adjust quality scores to reflect real-world utility.
249
+ *
250
+ * Algorithm:
251
+ * 1. Read brain_usage_log where outcome != 'unknown' — these are rows
252
+ * already tagged with a definitive outcome.
253
+ * 2. For success rows: boost quality_score by +0.05 for each entry used.
254
+ * 3. For failure rows: penalise quality_score by -0.05 for each entry used.
255
+ * 4. Flag entries whose citation_count = 0 AND last retrieval (if any) is
256
+ * older than 30 days as prune candidates.
257
+ *
258
+ * This is designed to be idempotent — running it twice on the same data
259
+ * applies the delta twice (small, intentional drift toward ground truth).
260
+ * Callers should schedule it once per session end or per task batch.
261
+ *
262
+ * @param projectRoot - Project root directory
263
+ * @returns Summary of changes made
264
+ */
265
+ export async function correlateOutcomes(projectRoot: string): Promise<CorrelateOutcomesResult> {
266
+ await ensureUsageLogTable(projectRoot);
267
+ await ensurePruneCandidateColumn(projectRoot);
268
+
269
+ const { getBrainDb, getBrainNativeDb } = await import('../store/brain-sqlite.js');
270
+ await getBrainDb(projectRoot);
271
+ const nativeDb = getBrainNativeDb();
272
+
273
+ const ranAt = new Date().toISOString();
274
+
275
+ if (!nativeDb) {
276
+ return { boosted: 0, penalized: 0, flaggedForPruning: 0, ranAt };
277
+ }
278
+
279
+ const now = ranAt.replace('T', ' ').slice(0, 19);
280
+ let boosted = 0;
281
+ let penalized = 0;
282
+
283
+ // ---- Step 1: apply quality adjustments from usage_log ----
284
+
285
+ interface UsageAggRow {
286
+ entry_id: string;
287
+ outcome: string;
288
+ used_count: number;
289
+ }
290
+
291
+ // Aggregate: for each (entry_id, outcome) pair sum the 'used' flags so we
292
+ // do a single UPDATE per entry rather than one per log row.
293
+ let usageRows: UsageAggRow[] = [];
294
+ try {
295
+ usageRows = typedAll<UsageAggRow>(
296
+ nativeDb.prepare(
297
+ `SELECT entry_id, outcome, SUM(used) AS used_count
298
+ FROM brain_usage_log
299
+ WHERE outcome IN ('success', 'failure')
300
+ GROUP BY entry_id, outcome`,
301
+ ),
302
+ );
303
+ } catch {
304
+ // brain_usage_log may not exist yet on this DB — treat as empty
305
+ usageRows = [];
306
+ }
307
+
308
+ for (const row of usageRows) {
309
+ const table = tableForId(row.entry_id);
310
+ if (!table) continue;
311
+
312
+ if (row.outcome === 'success' && row.used_count > 0) {
313
+ applyQualityDelta(nativeDb, table, row.entry_id, 0.05, now);
314
+ boosted++;
315
+ } else if (row.outcome === 'failure' && row.used_count > 0) {
316
+ applyQualityDelta(nativeDb, table, row.entry_id, -0.05, now);
317
+ penalized++;
318
+ }
319
+ }
320
+
321
+ // ---- Step 2: flag stale entries for pruning ----
322
+
323
+ const cutoffDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
324
+ .toISOString()
325
+ .replace('T', ' ')
326
+ .slice(0, 19);
327
+
328
+ let flaggedForPruning = 0;
329
+
330
+ const pruneTargetTables = [
331
+ { table: 'brain_decisions', dateCol: 'created_at' },
332
+ { table: 'brain_patterns', dateCol: 'extracted_at' },
333
+ { table: 'brain_learnings', dateCol: 'created_at' },
334
+ { table: 'brain_observations', dateCol: 'created_at' },
335
+ ] as const;
336
+
337
+ for (const { table, dateCol } of pruneTargetTables) {
338
+ try {
339
+ // Entries with zero citation_count whose creation date is older than 30 days.
340
+ const result = nativeDb
341
+ .prepare(
342
+ `UPDATE ${table}
343
+ SET prune_candidate = 1
344
+ WHERE COALESCE(citation_count, 0) = 0
345
+ AND ${dateCol} < ?`,
346
+ )
347
+ .run(cutoffDate);
348
+
349
+ flaggedForPruning += (result as { changes: number }).changes ?? 0;
350
+ } catch {
351
+ // best-effort
352
+ }
353
+ }
354
+
355
+ return { boosted, penalized, flaggedForPruning, ranAt };
356
+ }
357
+
358
+ // ============================================================================
359
+ // 3. getMemoryQualityReport
360
+ // ============================================================================
361
+
362
+ /**
363
+ * Return dashboard-level quality metrics for the BRAIN memory system.
364
+ *
365
+ * Aggregates across all four typed tables (decisions, patterns, learnings,
366
+ * observations) and the retrieval log to produce a single report object.
367
+ *
368
+ * @param projectRoot - Project root directory
369
+ * @returns Quality metrics report
370
+ */
371
+ export async function getMemoryQualityReport(projectRoot: string): Promise<MemoryQualityReport> {
372
+ await ensureUsageLogTable(projectRoot);
373
+
374
+ const { getBrainDb, getBrainNativeDb } = await import('../store/brain-sqlite.js');
375
+ await getBrainDb(projectRoot);
376
+ const nativeDb = getBrainNativeDb();
377
+
378
+ const emptyReport: MemoryQualityReport = {
379
+ totalRetrievals: 0,
380
+ uniqueEntriesRetrieved: 0,
381
+ usageRate: 0,
382
+ topRetrieved: [],
383
+ neverRetrieved: [],
384
+ qualityDistribution: { low: 0, medium: 0, high: 0 },
385
+ tierDistribution: { short: 0, medium: 0, long: 0, unknown: 0 },
386
+ noiseRatio: 0,
387
+ };
388
+
389
+ if (!nativeDb) return emptyReport;
390
+
391
+ // ---- Retrieval log totals ----
392
+
393
+ interface CountRow {
394
+ cnt: number;
395
+ }
396
+
397
+ let totalRetrievals = 0;
398
+ let uniqueEntriesRetrieved = 0;
399
+
400
+ try {
401
+ const logCount = typedAll<CountRow>(
402
+ nativeDb.prepare('SELECT COUNT(*) AS cnt FROM brain_retrieval_log'),
403
+ );
404
+ totalRetrievals = logCount[0]?.cnt ?? 0;
405
+
406
+ const uniqueCount = typedAll<CountRow>(
407
+ nativeDb.prepare(
408
+ `SELECT COUNT(DISTINCT value) AS cnt
409
+ FROM brain_retrieval_log,
410
+ json_each('["' || replace(entry_ids, ',', '","') || '"]')`,
411
+ ),
412
+ );
413
+ uniqueEntriesRetrieved = uniqueCount[0]?.cnt ?? 0;
414
+ } catch {
415
+ // brain_retrieval_log not yet created — harmless
416
+ }
417
+
418
+ // ---- Usage rate ----
419
+
420
+ let usageRate = 0;
421
+ try {
422
+ const totalUsage = typedAll<CountRow>(
423
+ nativeDb.prepare('SELECT COUNT(*) AS cnt FROM brain_usage_log'),
424
+ );
425
+ const usedCount = typedAll<CountRow>(
426
+ nativeDb.prepare('SELECT COUNT(*) AS cnt FROM brain_usage_log WHERE used = 1'),
427
+ );
428
+ const total = totalUsage[0]?.cnt ?? 0;
429
+ const used = usedCount[0]?.cnt ?? 0;
430
+ usageRate = total > 0 ? used / total : 0;
431
+ } catch {
432
+ // brain_usage_log not yet created
433
+ }
434
+
435
+ // ---- Top 10 most-retrieved entries (by citation_count) ----
436
+
437
+ interface CitedRow {
438
+ id: string;
439
+ type: string;
440
+ title: string;
441
+ citation_count: number;
442
+ }
443
+
444
+ const topRetrieved: MemoryQualityReport['topRetrieved'] = [];
445
+
446
+ try {
447
+ const rows = typedAll<CitedRow>(
448
+ nativeDb.prepare(
449
+ `SELECT id,
450
+ 'decision' AS type,
451
+ decision AS title,
452
+ COALESCE(citation_count, 0) AS citation_count
453
+ FROM brain_decisions
454
+ UNION ALL
455
+ SELECT id,
456
+ 'pattern' AS type,
457
+ pattern AS title,
458
+ COALESCE(citation_count, 0) AS citation_count
459
+ FROM brain_patterns
460
+ UNION ALL
461
+ SELECT id,
462
+ 'learning' AS type,
463
+ insight AS title,
464
+ COALESCE(citation_count, 0) AS citation_count
465
+ FROM brain_learnings
466
+ UNION ALL
467
+ SELECT id,
468
+ 'observation' AS type,
469
+ title AS title,
470
+ COALESCE(citation_count, 0) AS citation_count
471
+ FROM brain_observations
472
+ ORDER BY citation_count DESC
473
+ LIMIT 10`,
474
+ ),
475
+ );
476
+
477
+ for (const r of rows) {
478
+ topRetrieved.push({
479
+ id: r.id,
480
+ type: r.type,
481
+ title: String(r.title ?? '').slice(0, 120),
482
+ citationCount: r.citation_count,
483
+ });
484
+ }
485
+ } catch {
486
+ // best-effort
487
+ }
488
+
489
+ // ---- Top 10 never-retrieved entries (candidates for pruning) ----
490
+
491
+ interface NeverRow {
492
+ id: string;
493
+ type: string;
494
+ title: string;
495
+ quality_score: number;
496
+ }
497
+
498
+ const neverRetrieved: MemoryQualityReport['neverRetrieved'] = [];
499
+
500
+ try {
501
+ const rows = typedAll<NeverRow>(
502
+ nativeDb.prepare(
503
+ `SELECT id,
504
+ 'decision' AS type,
505
+ decision AS title,
506
+ COALESCE(quality_score, 0.5) AS quality_score
507
+ FROM brain_decisions
508
+ WHERE COALESCE(citation_count, 0) = 0
509
+ UNION ALL
510
+ SELECT id,
511
+ 'pattern' AS type,
512
+ pattern AS title,
513
+ COALESCE(quality_score, 0.5) AS quality_score
514
+ FROM brain_patterns
515
+ WHERE COALESCE(citation_count, 0) = 0
516
+ UNION ALL
517
+ SELECT id,
518
+ 'learning' AS type,
519
+ insight AS title,
520
+ COALESCE(quality_score, 0.5) AS quality_score
521
+ FROM brain_learnings
522
+ WHERE COALESCE(citation_count, 0) = 0
523
+ UNION ALL
524
+ SELECT id,
525
+ 'observation' AS type,
526
+ title AS title,
527
+ COALESCE(quality_score, 0.5) AS quality_score
528
+ FROM brain_observations
529
+ WHERE COALESCE(citation_count, 0) = 0
530
+ ORDER BY quality_score ASC
531
+ LIMIT 10`,
532
+ ),
533
+ );
534
+
535
+ for (const r of rows) {
536
+ neverRetrieved.push({
537
+ id: r.id,
538
+ type: r.type,
539
+ title: String(r.title ?? '').slice(0, 120),
540
+ qualityScore: r.quality_score,
541
+ });
542
+ }
543
+ } catch {
544
+ // best-effort
545
+ }
546
+
547
+ // ---- Quality score distribution ----
548
+
549
+ interface DistRow {
550
+ low: number;
551
+ medium: number;
552
+ high: number;
553
+ }
554
+
555
+ let qualityDistribution = { low: 0, medium: 0, high: 0 };
556
+
557
+ try {
558
+ const rows = typedAll<DistRow>(
559
+ nativeDb.prepare(
560
+ `SELECT
561
+ SUM(CASE WHEN qs < 0.3 THEN 1 ELSE 0 END) AS low,
562
+ SUM(CASE WHEN qs >= 0.3 AND qs <= 0.6 THEN 1 ELSE 0 END) AS medium,
563
+ SUM(CASE WHEN qs > 0.6 THEN 1 ELSE 0 END) AS high
564
+ FROM (
565
+ SELECT COALESCE(quality_score, 0.5) AS qs FROM brain_decisions
566
+ UNION ALL
567
+ SELECT COALESCE(quality_score, 0.5) AS qs FROM brain_patterns
568
+ UNION ALL
569
+ SELECT COALESCE(quality_score, 0.5) AS qs FROM brain_learnings
570
+ UNION ALL
571
+ SELECT COALESCE(quality_score, 0.5) AS qs FROM brain_observations
572
+ )`,
573
+ ),
574
+ );
575
+ if (rows[0]) {
576
+ qualityDistribution = {
577
+ low: rows[0].low ?? 0,
578
+ medium: rows[0].medium ?? 0,
579
+ high: rows[0].high ?? 0,
580
+ };
581
+ }
582
+ } catch {
583
+ // best-effort
584
+ }
585
+
586
+ // ---- Tier distribution ----
587
+
588
+ interface TierRow {
589
+ tier: string | null;
590
+ cnt: number;
591
+ }
592
+
593
+ const tierDistribution = { short: 0, medium: 0, long: 0, unknown: 0 };
594
+
595
+ try {
596
+ const rows = typedAll<TierRow>(
597
+ nativeDb.prepare(
598
+ `SELECT memory_tier AS tier, COUNT(*) AS cnt
599
+ FROM (
600
+ SELECT memory_tier FROM brain_decisions
601
+ UNION ALL
602
+ SELECT memory_tier FROM brain_patterns
603
+ UNION ALL
604
+ SELECT memory_tier FROM brain_learnings
605
+ UNION ALL
606
+ SELECT memory_tier FROM brain_observations
607
+ )
608
+ GROUP BY memory_tier`,
609
+ ),
610
+ );
611
+
612
+ for (const r of rows) {
613
+ const tier = r.tier?.toLowerCase() ?? 'unknown';
614
+ if (tier === 'short' || tier === 'medium' || tier === 'long') {
615
+ tierDistribution[tier] += r.cnt;
616
+ } else {
617
+ tierDistribution.unknown += r.cnt;
618
+ }
619
+ }
620
+ } catch {
621
+ // memory_tier column may not exist on older schemas
622
+ }
623
+
624
+ // ---- Noise ratio ----
625
+
626
+ const totalEntries =
627
+ qualityDistribution.low + qualityDistribution.medium + qualityDistribution.high;
628
+ const noiseRatio = totalEntries > 0 ? qualityDistribution.low / totalEntries : 0;
629
+
630
+ return {
631
+ totalRetrievals,
632
+ uniqueEntriesRetrieved,
633
+ usageRate,
634
+ topRetrieved,
635
+ neverRetrieved,
636
+ qualityDistribution,
637
+ tierDistribution,
638
+ noiseRatio,
639
+ };
640
+ }