@cleocode/core 2026.4.37 → 2026.4.38
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.
- package/dist/hooks/handlers/task-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/task-hooks.js +11 -0
- package/dist/hooks/handlers/task-hooks.js.map +1 -1
- package/dist/index.js +644 -33
- package/dist/index.js.map +4 -4
- package/dist/internal.d.ts +3 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +3 -1
- package/dist/internal.js.map +1 -1
- package/dist/memory/decisions.d.ts.map +1 -1
- package/dist/memory/decisions.js +18 -0
- package/dist/memory/decisions.js.map +1 -1
- package/dist/memory/engine-compat.d.ts +17 -0
- package/dist/memory/engine-compat.d.ts.map +1 -1
- package/dist/memory/engine-compat.js +36 -0
- package/dist/memory/engine-compat.js.map +1 -1
- package/dist/memory/graph-memory-bridge.d.ts +158 -0
- package/dist/memory/graph-memory-bridge.d.ts.map +1 -0
- package/dist/memory/graph-memory-bridge.js +519 -0
- package/dist/memory/graph-memory-bridge.js.map +1 -0
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +2 -0
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/learnings.d.ts.map +1 -1
- package/dist/memory/learnings.js +18 -0
- package/dist/memory/learnings.js.map +1 -1
- package/dist/memory/llm-extraction.js.map +1 -1
- package/dist/memory/patterns.d.ts.map +1 -1
- package/dist/memory/patterns.js +18 -0
- package/dist/memory/patterns.js.map +1 -1
- package/dist/memory/quality-feedback.d.ts +129 -0
- package/dist/memory/quality-feedback.d.ts.map +1 -0
- package/dist/memory/quality-feedback.js +449 -0
- package/dist/memory/quality-feedback.js.map +1 -0
- package/dist/memory/sleep-consolidation.d.ts +98 -0
- package/dist/memory/sleep-consolidation.d.ts.map +1 -0
- package/dist/memory/sleep-consolidation.js +706 -0
- package/dist/memory/sleep-consolidation.js.map +1 -0
- package/dist/memory/temporal-supersession.d.ts +155 -0
- package/dist/memory/temporal-supersession.d.ts.map +1 -0
- package/dist/memory/temporal-supersession.js +406 -0
- package/dist/memory/temporal-supersession.js.map +1 -0
- package/package.json +6 -6
- package/src/hooks/handlers/task-hooks.ts +11 -0
- package/src/internal.ts +12 -0
- package/src/memory/__tests__/graph-memory-bridge.test.ts +357 -0
- package/src/memory/__tests__/llm-extraction.test.ts +17 -0
- package/src/memory/__tests__/quality-feedback.test.ts +418 -0
- package/src/memory/__tests__/sleep-consolidation.test.ts +790 -0
- package/src/memory/__tests__/temporal-supersession.test.ts +534 -0
- package/src/memory/decisions.ts +24 -0
- package/src/memory/engine-compat.ts +37 -0
- package/src/memory/graph-memory-bridge.ts +751 -0
- package/src/memory/index.ts +2 -0
- package/src/memory/learnings.ts +24 -0
- package/src/memory/patterns.ts +24 -0
- package/src/memory/quality-feedback.ts +640 -0
- package/src/memory/sleep-consolidation.ts +932 -0
- package/src/memory/temporal-supersession.ts +568 -0
- package/src/store/__tests__/performance-safety.test.ts +4 -4
|
@@ -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
|
+
}
|