claude-mem-lite 2.34.6 → 2.35.0
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/hook-llm.mjs +3 -2
- package/install.mjs +29 -6
- package/lib/doctor-drift.mjs +46 -0
- package/lib/low-signal-patterns.mjs +60 -0
- package/lib/stats-quality.mjs +130 -0
- package/mem-cli.mjs +12 -126
- package/package.json +4 -1
- package/scoring-sql.mjs +7 -20
- package/scripts/pre-skill-bridge.js +2 -1
- package/scripts/pre-tool-recall.js +12 -2
- package/server.mjs +31 -2
- package/source-files.mjs +3 -0
- package/tool-schemas.mjs +2 -0
- package/utils.mjs +7 -2
package/hook-llm.mjs
CHANGED
|
@@ -371,8 +371,9 @@ export function buildDegradedTitle(episode) {
|
|
|
371
371
|
}
|
|
372
372
|
|
|
373
373
|
if (files.length > 0) {
|
|
374
|
-
const
|
|
375
|
-
const
|
|
374
|
+
const uniqueNames = [...new Set(files.map(f => basename(f)))];
|
|
375
|
+
const names = uniqueNames.slice(0, 3).join(', ');
|
|
376
|
+
const suffix = uniqueNames.length > 3 ? ` +${uniqueNames.length - 3} more` : '';
|
|
376
377
|
if (hasError) {
|
|
377
378
|
// Include the triggering command for richer context: "Error: dispatch.mjs — npm test failed"
|
|
378
379
|
const errEntry = episode.entries.find(e => e.isError);
|
package/install.mjs
CHANGED
|
@@ -1065,11 +1065,13 @@ async function doctor() {
|
|
|
1065
1065
|
}
|
|
1066
1066
|
|
|
1067
1067
|
// Dependencies
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1068
|
+
try {
|
|
1069
|
+
const Database = (await import('better-sqlite3')).default;
|
|
1070
|
+
const probe = new Database(':memory:');
|
|
1071
|
+
probe.close();
|
|
1072
|
+
ok('better-sqlite3: verified (import + open OK)');
|
|
1073
|
+
} catch (e) {
|
|
1074
|
+
fail(`better-sqlite3: import/init failed (${e.message})`);
|
|
1073
1075
|
issues++;
|
|
1074
1076
|
}
|
|
1075
1077
|
|
|
@@ -1193,6 +1195,26 @@ async function doctor() {
|
|
|
1193
1195
|
warn('Update state: failed to read');
|
|
1194
1196
|
}
|
|
1195
1197
|
|
|
1198
|
+
// Dev drift: in dev-mode installs, all SOURCE_FILES entries should be
|
|
1199
|
+
// symlinks. A plain file means an earlier install (or manual cp) copied it,
|
|
1200
|
+
// so edits in the repo won't propagate to INSTALL_DIR — hook runtime and
|
|
1201
|
+
// test runtime silently diverge.
|
|
1202
|
+
try {
|
|
1203
|
+
const { checkDevDrift } = await import('./lib/doctor-drift.mjs');
|
|
1204
|
+
const r = checkDevDrift(INSTALL_DIR, SOURCE_FILES);
|
|
1205
|
+
if (r.drift) {
|
|
1206
|
+
const names = r.details.join(', ');
|
|
1207
|
+
const suffix = r.plainCount > r.details.length ? ` +${r.plainCount - r.details.length} more` : '';
|
|
1208
|
+
warn(`Dev drift: ${r.plainCount} non-symlink file(s) in dev install: ${names}${suffix} (re-run: node install.mjs install --dev)`);
|
|
1209
|
+
issues++;
|
|
1210
|
+
} else if (r.devMode) {
|
|
1211
|
+
ok(`Dev drift: clean (${r.symlinkCount} symlinks, 0 plain)`);
|
|
1212
|
+
}
|
|
1213
|
+
// Prod (all plain) install: no message — dev-drift is a dev-only concern.
|
|
1214
|
+
} catch (e) {
|
|
1215
|
+
warn('Dev drift: check failed — ' + e.message);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1196
1218
|
// Stale temp files
|
|
1197
1219
|
try {
|
|
1198
1220
|
const runtimeDir = join(INSTALL_DIR, 'runtime');
|
|
@@ -1225,7 +1247,8 @@ async function doctor() {
|
|
|
1225
1247
|
const Database = (await import('better-sqlite3')).default;
|
|
1226
1248
|
const db = new Database(DB_PATH, { readonly: true });
|
|
1227
1249
|
const obsCount = db.prepare('SELECT COUNT(*) as cnt FROM observations').get()?.cnt || 0;
|
|
1228
|
-
|
|
1250
|
+
// Align with stats / MCP mem_stats: session_summaries, not sdk_sessions
|
|
1251
|
+
const sessCount = db.prepare('SELECT COUNT(*) as cnt FROM session_summaries').get()?.cnt || 0;
|
|
1229
1252
|
db.close();
|
|
1230
1253
|
ok(`DB stats: ${sizeMB}MB, ${obsCount} observations, ${sessCount} sessions`);
|
|
1231
1254
|
} catch (e) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Dev-drift check: in dev-mode installs (symlinked to project repo), every
|
|
2
|
+
// managed source file in INSTALL_DIR should be a symlink. A regular file
|
|
3
|
+
// means an earlier install copied it (e.g. install.mjs before it was added
|
|
4
|
+
// to SOURCE_FILES) or someone ran `cp` manually — edits won't propagate
|
|
5
|
+
// from the repo, testing vs runtime will silently diverge.
|
|
6
|
+
//
|
|
7
|
+
// Returns: { devMode, drift, details } — devMode=false when no symlinks
|
|
8
|
+
// detected (prod copy install), drift=true when in dev-mode AND at least
|
|
9
|
+
// one SOURCE_FILES entry is a plain file.
|
|
10
|
+
|
|
11
|
+
import { existsSync, lstatSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
|
|
14
|
+
export function checkDevDrift(installDir, sourceFiles) {
|
|
15
|
+
if (!existsSync(installDir)) {
|
|
16
|
+
return { devMode: false, drift: false, symlinkCount: 0, plainCount: 0, plainFiles: [], missingCount: 0, details: [] };
|
|
17
|
+
}
|
|
18
|
+
const symlinkFiles = [];
|
|
19
|
+
const plainFiles = [];
|
|
20
|
+
const missing = [];
|
|
21
|
+
for (const rel of sourceFiles) {
|
|
22
|
+
const p = join(installDir, rel);
|
|
23
|
+
if (!existsSync(p)) { missing.push(rel); continue; }
|
|
24
|
+
try {
|
|
25
|
+
const st = lstatSync(p);
|
|
26
|
+
if (st.isSymbolicLink()) symlinkFiles.push(rel);
|
|
27
|
+
else plainFiles.push(rel);
|
|
28
|
+
} catch {
|
|
29
|
+
missing.push(rel);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// devMode detection: if ≥1 symlink exists among source files, consider
|
|
33
|
+
// this a dev install. (Prod install is all plain files → drift=false
|
|
34
|
+
// because there's nothing to drift from.)
|
|
35
|
+
const devMode = symlinkFiles.length > 0;
|
|
36
|
+
const drift = devMode && plainFiles.length > 0;
|
|
37
|
+
return {
|
|
38
|
+
devMode,
|
|
39
|
+
drift,
|
|
40
|
+
symlinkCount: symlinkFiles.length,
|
|
41
|
+
plainCount: plainFiles.length,
|
|
42
|
+
plainFiles,
|
|
43
|
+
missingCount: missing.length,
|
|
44
|
+
details: plainFiles.slice(0, 5),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Single source of truth for LOW_SIGNAL title patterns.
|
|
2
|
+
//
|
|
3
|
+
// "LOW_SIGNAL" = hook-llm fallback titles written when Haiku summarization
|
|
4
|
+
// is unavailable or skipped ("Modified X", "Worked on X", "Reviewed N files:",
|
|
5
|
+
// raw "Error: ..." logs, bare "node/npm/npx <cmd>" etc.). Empirical data:
|
|
6
|
+
// ~544 such entries in production, 18 ever accessed (3.3% retrieval rate).
|
|
7
|
+
//
|
|
8
|
+
// Three consumers must stay in sync (pre-β: by hand-mirrored comments);
|
|
9
|
+
// post-β: all three derive from this module.
|
|
10
|
+
// 1. utils.mjs::LOW_SIGNAL_TITLE — regex for write-side importance cap
|
|
11
|
+
// 2. scoring-sql.mjs::notLowSignalTitleClause — SQL NOT LIKE chain for read-side filter
|
|
12
|
+
// 3. scripts/pre-tool-recall.js — inline SQL (standalone cold-start script)
|
|
13
|
+
//
|
|
14
|
+
// This module is intentionally dependency-free so scripts/pre-tool-recall.js can
|
|
15
|
+
// import it without inflating the ~30ms cold-start budget.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Each entry has:
|
|
19
|
+
* - like: SQLite LIKE pattern (anchored; % = any chars)
|
|
20
|
+
* - regex: JS regex source fragment (MUST match the same title set as `like`)
|
|
21
|
+
*
|
|
22
|
+
* Adding/removing entries requires updating the sync test (tests/low-signal-sync.test.mjs).
|
|
23
|
+
*/
|
|
24
|
+
export const LOW_SIGNAL_PATTERNS = [
|
|
25
|
+
{ like: 'Modified %', regex: '^Modified ' },
|
|
26
|
+
{ like: 'Worked on %', regex: '^Worked on ' },
|
|
27
|
+
{ like: 'Reviewed % files:%', regex: '^Reviewed \\d+ files:' },
|
|
28
|
+
{ like: 'Codebase exploration%', regex: '^Codebase exploration' },
|
|
29
|
+
{ like: 'Error while working%', regex: '^Error while working' },
|
|
30
|
+
{ like: 'Error in %', regex: '^Error in ' },
|
|
31
|
+
{ like: 'Error: %', regex: '^Error: ' },
|
|
32
|
+
{ like: '# %', regex: '^# ' },
|
|
33
|
+
{ like: 'node %', regex: '^node ' },
|
|
34
|
+
{ like: 'npm %', regex: '^npm ' },
|
|
35
|
+
{ like: 'npx %', regex: '^npx ' },
|
|
36
|
+
{ like: '(no description)%', regex: '^\\(no description\\)' },
|
|
37
|
+
{ like: '%(error)', regex: '\\(error\\)$' },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build the combined regex that matches ANY LOW_SIGNAL pattern.
|
|
42
|
+
* Equivalent to the hand-written `LOW_SIGNAL_TITLE` before β refactor.
|
|
43
|
+
*/
|
|
44
|
+
export function buildLowSignalRegex() {
|
|
45
|
+
const src = LOW_SIGNAL_PATTERNS.map(p => `(?:${p.regex})`).join('|');
|
|
46
|
+
return new RegExp(src);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build the SQL NOT LIKE clause chain, optionally prefixed with a table alias.
|
|
51
|
+
* Output is a single parenthesized AND-chain — safe to combine with other AND/OR.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} [alias=''] Table alias (e.g. 'o') — empty for unqualified.
|
|
54
|
+
* @returns {string} SQL boolean expression
|
|
55
|
+
*/
|
|
56
|
+
export function buildNotLowSignalSql(alias = '') {
|
|
57
|
+
const p = alias ? `${alias}.` : '';
|
|
58
|
+
const clauses = LOW_SIGNAL_PATTERNS.map(({ like }) => `${p}title NOT LIKE '${like}'`);
|
|
59
|
+
return '(\n ' + clauses.join('\n AND ') + '\n )';
|
|
60
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Shared quality-dashboard computation — used by both mem-cli.mjs (CLI
|
|
2
|
+
// `stats --quality`) and server.mjs (MCP `mem_stats({quality: true})`).
|
|
3
|
+
// Splits pure data aggregation from text rendering so MCP handlers don't
|
|
4
|
+
// collide with CLI's `out()` stdout-write pattern.
|
|
5
|
+
|
|
6
|
+
import { notLowSignalTitleClause } from '../scoring-sql.mjs';
|
|
7
|
+
import { truncate } from '../format-utils.mjs';
|
|
8
|
+
|
|
9
|
+
export function computeQualityStats(db, { project, days }) {
|
|
10
|
+
const projectFilter = project ? 'AND project = ?' : '';
|
|
11
|
+
const baseParams = project ? [project] : [];
|
|
12
|
+
const cutoff = Date.now() - days * 86400000;
|
|
13
|
+
|
|
14
|
+
// LOW_SIGNAL match = NOT notLowSignal. Shared helper keeps SQL in sync
|
|
15
|
+
// with scoring-sql.mjs and pre-tool-recall.js Edit-fallback filter.
|
|
16
|
+
const lowSignalIsMatchExpr = `NOT ${notLowSignalTitleClause('')}`;
|
|
17
|
+
|
|
18
|
+
// Narrative-text proxy for bugfix investigations that never landed a fix.
|
|
19
|
+
const unresolvedNarrativeExpr = `(
|
|
20
|
+
LOWER(COALESCE(narrative,'')) LIKE '%not yet identified%'
|
|
21
|
+
OR LOWER(COALESCE(narrative,'')) LIKE '%not yet resolved%'
|
|
22
|
+
OR LOWER(COALESCE(narrative,'')) LIKE '%not yet fixed%'
|
|
23
|
+
OR LOWER(COALESCE(narrative,'')) LIKE '%root cause not%'
|
|
24
|
+
OR LOWER(COALESCE(narrative,'')) LIKE '%still fail%'
|
|
25
|
+
OR LOWER(COALESCE(narrative,'')) LIKE '%errors persisted%'
|
|
26
|
+
OR LOWER(COALESCE(narrative,'')) LIKE '%persisted on retry%'
|
|
27
|
+
)`;
|
|
28
|
+
|
|
29
|
+
const windowRow = db.prepare(`
|
|
30
|
+
SELECT
|
|
31
|
+
COUNT(*) as total,
|
|
32
|
+
SUM(CASE WHEN lesson_learned IS NOT NULL AND lesson_learned != '' THEN 1 ELSE 0 END) as with_lesson,
|
|
33
|
+
SUM(CASE WHEN ${lowSignalIsMatchExpr} THEN 1 ELSE 0 END) as low_signal,
|
|
34
|
+
SUM(CASE WHEN type = 'bugfix' THEN 1 ELSE 0 END) as bugfix_total,
|
|
35
|
+
SUM(CASE WHEN type = 'bugfix' AND ${unresolvedNarrativeExpr} THEN 1 ELSE 0 END) as bugfix_unresolved
|
|
36
|
+
FROM observations
|
|
37
|
+
WHERE created_at_epoch >= ? ${projectFilter}
|
|
38
|
+
`).get(cutoff, ...baseParams);
|
|
39
|
+
|
|
40
|
+
const allTimeRow = db.prepare(`
|
|
41
|
+
SELECT
|
|
42
|
+
COUNT(*) as total,
|
|
43
|
+
SUM(CASE WHEN lesson_learned IS NOT NULL AND lesson_learned != '' THEN 1 ELSE 0 END) as with_lesson,
|
|
44
|
+
SUM(CASE WHEN ${lowSignalIsMatchExpr} THEN 1 ELSE 0 END) as low_signal
|
|
45
|
+
FROM observations
|
|
46
|
+
WHERE 1=1 ${projectFilter}
|
|
47
|
+
`).get(...baseParams);
|
|
48
|
+
|
|
49
|
+
const typeRows = db.prepare(`
|
|
50
|
+
SELECT
|
|
51
|
+
type,
|
|
52
|
+
COUNT(*) as total,
|
|
53
|
+
SUM(CASE WHEN COALESCE(access_count, 0) > 0 THEN 1 ELSE 0 END) as accessed,
|
|
54
|
+
SUM(CASE WHEN lesson_learned IS NOT NULL AND lesson_learned != '' THEN 1 ELSE 0 END) as with_lesson
|
|
55
|
+
FROM observations
|
|
56
|
+
WHERE created_at_epoch >= ? ${projectFilter}
|
|
57
|
+
GROUP BY type
|
|
58
|
+
ORDER BY total DESC
|
|
59
|
+
`).all(cutoff, ...baseParams);
|
|
60
|
+
|
|
61
|
+
const topLessons = db.prepare(`
|
|
62
|
+
SELECT id, type, title, lesson_learned, COALESCE(access_count, 0) as ac
|
|
63
|
+
FROM observations
|
|
64
|
+
WHERE lesson_learned IS NOT NULL AND lesson_learned != ''
|
|
65
|
+
AND COALESCE(access_count, 0) > 0
|
|
66
|
+
AND COALESCE(compressed_into, 0) = 0
|
|
67
|
+
${projectFilter}
|
|
68
|
+
ORDER BY ac DESC
|
|
69
|
+
LIMIT 5
|
|
70
|
+
`).all(...baseParams);
|
|
71
|
+
|
|
72
|
+
return { windowRow, allTimeRow, typeRows, topLessons, project, days };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function formatQualityReport(data) {
|
|
76
|
+
const { windowRow, allTimeRow, typeRows, topLessons, project, days } = data;
|
|
77
|
+
const pct = (n, d) => d > 0 ? (100 * n / d).toFixed(1) : '0.0';
|
|
78
|
+
const scope = project ? ` — ${project}` : '';
|
|
79
|
+
const lines = [];
|
|
80
|
+
lines.push(`[mem] Quality snapshot${scope} — window: ${days}d`);
|
|
81
|
+
lines.push('────────────────────────────────────────────────────');
|
|
82
|
+
lines.push(` Writes (${days}d): ${windowRow.total} observations`);
|
|
83
|
+
|
|
84
|
+
const lessonPct = pct(windowRow.with_lesson, windowRow.total);
|
|
85
|
+
const allLessonPct = pct(allTimeRow.with_lesson, allTimeRow.total);
|
|
86
|
+
lines.push(` Lesson rate: ${windowRow.with_lesson} / ${windowRow.total} (${lessonPct}%) [all-time: ${allTimeRow.with_lesson} / ${allTimeRow.total} = ${allLessonPct}%]`);
|
|
87
|
+
|
|
88
|
+
const noisePct = pct(windowRow.low_signal, windowRow.total);
|
|
89
|
+
const allNoisePct = pct(allTimeRow.low_signal, allTimeRow.total);
|
|
90
|
+
lines.push(` LOW_SIGNAL: ${windowRow.low_signal} / ${windowRow.total} (${noisePct}%) [all-time: ${allTimeRow.low_signal} / ${allTimeRow.total} = ${allNoisePct}%]`);
|
|
91
|
+
|
|
92
|
+
if (windowRow.bugfix_total > 0) {
|
|
93
|
+
const unresolvedPct = pct(windowRow.bugfix_unresolved, windowRow.bugfix_total);
|
|
94
|
+
lines.push(` Unresolved bugfix: ${windowRow.bugfix_unresolved} / ${windowRow.bugfix_total} (${unresolvedPct}%) [investigation-only narratives — should trend ↓ with R-6 manual-save contract]`);
|
|
95
|
+
}
|
|
96
|
+
lines.push('');
|
|
97
|
+
|
|
98
|
+
if (typeRows.length > 0) {
|
|
99
|
+
lines.push(` Type breakdown (${days}d):`);
|
|
100
|
+
for (const r of typeRows) {
|
|
101
|
+
const hit = pct(r.accessed, r.total);
|
|
102
|
+
const lp = pct(r.with_lesson, r.total);
|
|
103
|
+
const typeLabel = r.type.padEnd(10);
|
|
104
|
+
lines.push(` ${typeLabel}${String(r.total).padStart(5)} hit ${hit.padStart(5)}% lesson ${lp.padStart(5)}%`);
|
|
105
|
+
}
|
|
106
|
+
lines.push('');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (topLessons.length > 0) {
|
|
110
|
+
lines.push(' Top accessed lessons (all-time):');
|
|
111
|
+
for (const l of topLessons) {
|
|
112
|
+
const t = truncate(l.lesson_learned, 80);
|
|
113
|
+
lines.push(` #${l.id} [${l.type}] (${l.ac}x) ${t}`);
|
|
114
|
+
}
|
|
115
|
+
lines.push('');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// R-2 watchdog — format matches historical cmdStats for test stability
|
|
119
|
+
const lessonNum = parseFloat(lessonPct);
|
|
120
|
+
const noiseNum = parseFloat(noisePct);
|
|
121
|
+
const lessonGap = (lessonNum - 15).toFixed(1);
|
|
122
|
+
const noiseGap = (noiseNum - 30).toFixed(1);
|
|
123
|
+
const lessonStatus = lessonNum >= 15 ? '✅' : '🔴';
|
|
124
|
+
const noiseStatus = noiseNum <= 30 ? '✅' : '🔴';
|
|
125
|
+
lines.push(' Targets (R-2 watchdog):');
|
|
126
|
+
lines.push(` ${lessonStatus} Lesson rate ≥ 15% → currently ${lessonPct}% (gap ${lessonGap >= 0 ? '+' : ''}${lessonGap}pp)`);
|
|
127
|
+
lines.push(` ${noiseStatus} LOW_SIGNAL ≤ 30% → currently ${noisePct}% (gap ${noiseGap >= 0 ? '+' : ''}${noiseGap}pp)`);
|
|
128
|
+
|
|
129
|
+
return lines.join('\n');
|
|
130
|
+
}
|
package/mem-cli.mjs
CHANGED
|
@@ -902,132 +902,17 @@ function cmdSave(db, args) {
|
|
|
902
902
|
// Targets (aspirational, not enforced):
|
|
903
903
|
// - Lesson rate ≥ 15% (current baseline ~4.4%)
|
|
904
904
|
// - LOW_SIGNAL rate ≤ 30% (current baseline ~49.4%)
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
// that flips the sign so we count titles that DO match the LOW_SIGNAL regex.
|
|
913
|
-
const lowSignalIsMatchExpr = `NOT ${notLowSignalTitleClause('')}`;
|
|
914
|
-
|
|
915
|
-
// Unresolved-bugfix detection: narrative-text proxies for "investigation in progress,
|
|
916
|
-
// never reached a fix". Heuristic — false positives possible (e.g. a real lesson noting
|
|
917
|
-
// "the bug persists in legacy clients"), but the directional signal is what we care about.
|
|
918
|
-
// R-7 micro-experiment surfaced this pollution: ~3/5 of randomly-sampled bugfix narratives
|
|
919
|
-
// explicitly ended with "root cause not yet identified".
|
|
920
|
-
const unresolvedNarrativeExpr = `(
|
|
921
|
-
LOWER(COALESCE(narrative,'')) LIKE '%not yet identified%'
|
|
922
|
-
OR LOWER(COALESCE(narrative,'')) LIKE '%not yet resolved%'
|
|
923
|
-
OR LOWER(COALESCE(narrative,'')) LIKE '%not yet fixed%'
|
|
924
|
-
OR LOWER(COALESCE(narrative,'')) LIKE '%root cause not%'
|
|
925
|
-
OR LOWER(COALESCE(narrative,'')) LIKE '%still fail%'
|
|
926
|
-
OR LOWER(COALESCE(narrative,'')) LIKE '%errors persisted%'
|
|
927
|
-
OR LOWER(COALESCE(narrative,'')) LIKE '%persisted on retry%'
|
|
928
|
-
)`;
|
|
929
|
-
|
|
930
|
-
// In-window aggregates
|
|
931
|
-
const windowRow = db.prepare(`
|
|
932
|
-
SELECT
|
|
933
|
-
COUNT(*) as total,
|
|
934
|
-
SUM(CASE WHEN lesson_learned IS NOT NULL AND lesson_learned != '' THEN 1 ELSE 0 END) as with_lesson,
|
|
935
|
-
SUM(CASE WHEN ${lowSignalIsMatchExpr} THEN 1 ELSE 0 END) as low_signal,
|
|
936
|
-
SUM(CASE WHEN type = 'bugfix' THEN 1 ELSE 0 END) as bugfix_total,
|
|
937
|
-
SUM(CASE WHEN type = 'bugfix' AND ${unresolvedNarrativeExpr} THEN 1 ELSE 0 END) as bugfix_unresolved
|
|
938
|
-
FROM observations
|
|
939
|
-
WHERE created_at_epoch >= ? ${projectFilter}
|
|
940
|
-
`).get(cutoff, ...baseParams);
|
|
941
|
-
|
|
942
|
-
// All-time aggregates (context for recent numbers)
|
|
943
|
-
const allTimeRow = db.prepare(`
|
|
944
|
-
SELECT
|
|
945
|
-
COUNT(*) as total,
|
|
946
|
-
SUM(CASE WHEN lesson_learned IS NOT NULL AND lesson_learned != '' THEN 1 ELSE 0 END) as with_lesson,
|
|
947
|
-
SUM(CASE WHEN ${lowSignalIsMatchExpr} THEN 1 ELSE 0 END) as low_signal
|
|
948
|
-
FROM observations
|
|
949
|
-
WHERE 1=1 ${projectFilter}
|
|
950
|
-
`).get(...baseParams);
|
|
951
|
-
|
|
952
|
-
// Per-type: count, hit rate (access_count > 0), lesson rate
|
|
953
|
-
const typeRows = db.prepare(`
|
|
954
|
-
SELECT
|
|
955
|
-
type,
|
|
956
|
-
COUNT(*) as total,
|
|
957
|
-
SUM(CASE WHEN COALESCE(access_count, 0) > 0 THEN 1 ELSE 0 END) as accessed,
|
|
958
|
-
SUM(CASE WHEN lesson_learned IS NOT NULL AND lesson_learned != '' THEN 1 ELSE 0 END) as with_lesson
|
|
959
|
-
FROM observations
|
|
960
|
-
WHERE created_at_epoch >= ? ${projectFilter}
|
|
961
|
-
GROUP BY type
|
|
962
|
-
ORDER BY total DESC
|
|
963
|
-
`).all(cutoff, ...baseParams);
|
|
964
|
-
|
|
965
|
-
// Top-5 most-accessed lessons (all-time, this project scope)
|
|
966
|
-
const topLessons = db.prepare(`
|
|
967
|
-
SELECT id, type, title, lesson_learned, COALESCE(access_count, 0) as ac
|
|
968
|
-
FROM observations
|
|
969
|
-
WHERE lesson_learned IS NOT NULL AND lesson_learned != ''
|
|
970
|
-
AND COALESCE(access_count, 0) > 0
|
|
971
|
-
AND COALESCE(compressed_into, 0) = 0
|
|
972
|
-
${projectFilter}
|
|
973
|
-
ORDER BY ac DESC
|
|
974
|
-
LIMIT 5
|
|
975
|
-
`).all(...baseParams);
|
|
976
|
-
|
|
977
|
-
const pct = (n, d) => d > 0 ? (100 * n / d).toFixed(1) : '0.0';
|
|
978
|
-
const scope = project ? ` — ${project}` : '';
|
|
979
|
-
out(`[mem] Quality snapshot${scope} — window: ${days}d`);
|
|
980
|
-
out('────────────────────────────────────────────────────');
|
|
981
|
-
out(` Writes (${days}d): ${windowRow.total} observations`);
|
|
982
|
-
|
|
983
|
-
const lessonPct = pct(windowRow.with_lesson, windowRow.total);
|
|
984
|
-
const allLessonPct = pct(allTimeRow.with_lesson, allTimeRow.total);
|
|
985
|
-
out(` Lesson rate: ${windowRow.with_lesson} / ${windowRow.total} (${lessonPct}%) [all-time: ${allTimeRow.with_lesson} / ${allTimeRow.total} = ${allLessonPct}%]`);
|
|
986
|
-
|
|
987
|
-
const noisePct = pct(windowRow.low_signal, windowRow.total);
|
|
988
|
-
const allNoisePct = pct(allTimeRow.low_signal, allTimeRow.total);
|
|
989
|
-
out(` LOW_SIGNAL: ${windowRow.low_signal} / ${windowRow.total} (${noisePct}%) [all-time: ${allTimeRow.low_signal} / ${allTimeRow.total} = ${allNoisePct}%]`);
|
|
990
|
-
|
|
991
|
-
if (windowRow.bugfix_total > 0) {
|
|
992
|
-
const unresolvedPct = pct(windowRow.bugfix_unresolved, windowRow.bugfix_total);
|
|
993
|
-
out(` Unresolved bugfix: ${windowRow.bugfix_unresolved} / ${windowRow.bugfix_total} (${unresolvedPct}%) [investigation-only narratives — should trend ↓ with R-6 manual-save contract]`);
|
|
994
|
-
}
|
|
995
|
-
out('');
|
|
996
|
-
|
|
997
|
-
if (typeRows.length > 0) {
|
|
998
|
-
out(` Type breakdown (${days}d):`);
|
|
999
|
-
for (const r of typeRows) {
|
|
1000
|
-
const hit = pct(r.accessed, r.total);
|
|
1001
|
-
const lp = pct(r.with_lesson, r.total);
|
|
1002
|
-
const typeLabel = r.type.padEnd(10);
|
|
1003
|
-
// padStart(5) on count so rows align up to 5-digit totals (99999).
|
|
1004
|
-
out(` ${typeLabel}${String(r.total).padStart(5)} hit ${hit.padStart(5)}% lesson ${lp.padStart(5)}%`);
|
|
1005
|
-
}
|
|
1006
|
-
out('');
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
if (topLessons.length > 0) {
|
|
1010
|
-
out(' Top accessed lessons (all-time):');
|
|
1011
|
-
for (const l of topLessons) {
|
|
1012
|
-
const t = truncate(l.lesson_learned, 80);
|
|
1013
|
-
out(` #${l.id} [${l.type}] (${l.ac}x) ${t}`);
|
|
1014
|
-
}
|
|
1015
|
-
out('');
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
// R-2 watchdog — explicit targets make progress legible.
|
|
1019
|
-
const lessonNum = parseFloat(lessonPct);
|
|
1020
|
-
const noiseNum = parseFloat(noisePct);
|
|
1021
|
-
const lessonGap = (lessonNum - 15).toFixed(1);
|
|
1022
|
-
const noiseGap = (noiseNum - 30).toFixed(1);
|
|
1023
|
-
const lessonStatus = lessonNum >= 15 ? '✅' : '🔴';
|
|
1024
|
-
const noiseStatus = noiseNum <= 30 ? '✅' : '🔴';
|
|
1025
|
-
out(' Targets (R-2 watchdog):');
|
|
1026
|
-
out(` ${lessonStatus} Lesson rate ≥ 15% → currently ${lessonPct}% (gap ${lessonGap >= 0 ? '+' : ''}${lessonGap}pp)`);
|
|
1027
|
-
out(` ${noiseStatus} LOW_SIGNAL ≤ 30% → currently ${noisePct}% (gap ${noiseGap >= 0 ? '+' : ''}${noiseGap}pp)`);
|
|
905
|
+
// Batch A CLI↔MCP alignment: CLI `stats --quality` and MCP `mem_stats({quality:true})`
|
|
906
|
+
// share the same computation + formatting via lib/stats-quality.mjs. This wrapper
|
|
907
|
+
// keeps the cmdStats call-site unchanged (stays sync-compatible) by delegating
|
|
908
|
+
// to a dynamic import + sync function chain inside an async caller.
|
|
909
|
+
async function renderQualityReport(db, { project, days }) {
|
|
910
|
+
const { computeQualityStats, formatQualityReport } = await import('./lib/stats-quality.mjs');
|
|
911
|
+
out(formatQualityReport(computeQualityStats(db, { project, days })));
|
|
1028
912
|
}
|
|
1029
913
|
|
|
1030
|
-
|
|
914
|
+
|
|
915
|
+
async function cmdStats(db, args) {
|
|
1031
916
|
const { flags } = parseArgs(args);
|
|
1032
917
|
const project = flags.project ? resolveProject(db, flags.project) : null;
|
|
1033
918
|
const days = parseInt(flags.days, 10) || 30;
|
|
@@ -1036,7 +921,7 @@ function cmdStats(db, args) {
|
|
|
1036
921
|
// the baseline metric dashboard for the future Haiku prompt A/B test.
|
|
1037
922
|
const quality = flags.quality === true || flags.quality === 'true';
|
|
1038
923
|
if (quality) {
|
|
1039
|
-
renderQualityReport(db, { project, days });
|
|
924
|
+
await renderQualityReport(db, { project, days });
|
|
1040
925
|
return;
|
|
1041
926
|
}
|
|
1042
927
|
|
|
@@ -2025,6 +1910,7 @@ Commands:
|
|
|
2025
1910
|
--tier T Filter by tier (working|active|archive, observations only)
|
|
2026
1911
|
--sort S Sort: relevance (default), time, importance
|
|
2027
1912
|
--or Use OR instead of AND between search terms
|
|
1913
|
+
--include-noise Include hook-llm fallback titles ("Modified X", raw error logs)
|
|
2028
1914
|
|
|
2029
1915
|
recent [N] Show N most recent observations (default 10)
|
|
2030
1916
|
--project P Filter by project
|
|
@@ -2467,7 +2353,7 @@ export async function run(argv) {
|
|
|
2467
2353
|
case 'maintain': cmdMaintain(db, cmdArgs); break;
|
|
2468
2354
|
case 'optimize': await cmdOptimize(db, cmdArgs); break;
|
|
2469
2355
|
case 'fts-check': cmdFtsCheck(db, cmdArgs); break;
|
|
2470
|
-
case 'stats': cmdStats(db, cmdArgs); break;
|
|
2356
|
+
case 'stats': await cmdStats(db, cmdArgs); break;
|
|
2471
2357
|
case 'context': cmdContext(db, cmdArgs); break;
|
|
2472
2358
|
case 'browse': cmdBrowse(db, cmdArgs); break;
|
|
2473
2359
|
case 'registry': cmdRegistry(db, cmdArgs); break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.35.0",
|
|
4
4
|
"description": "Lightweight persistent memory system for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -47,6 +47,9 @@
|
|
|
47
47
|
"lib/plan-reader.mjs",
|
|
48
48
|
"lib/startup-dashboard.mjs",
|
|
49
49
|
"lib/doctor-benchmark.mjs",
|
|
50
|
+
"lib/doctor-drift.mjs",
|
|
51
|
+
"lib/stats-quality.mjs",
|
|
52
|
+
"lib/low-signal-patterns.mjs",
|
|
50
53
|
"registry.mjs",
|
|
51
54
|
"registry-retriever.mjs",
|
|
52
55
|
"registry-indexer.mjs",
|
package/scoring-sql.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// scoring-sql.mjs — SQL constants for BM25 scoring and temporal decay.
|
|
2
2
|
// Extracted from utils.mjs for focused module boundaries.
|
|
3
3
|
|
|
4
|
+
import { buildNotLowSignalSql } from './lib/low-signal-patterns.mjs';
|
|
5
|
+
|
|
4
6
|
// ─── Type-Differentiated Recency Decay ──────────────────────────────────────
|
|
5
7
|
|
|
6
8
|
/** Recency half-life per observation type (in milliseconds) */
|
|
@@ -74,25 +76,10 @@ export const TYPE_QUALITY_CASE = `(
|
|
|
74
76
|
* @param {string} [alias='o'] Table alias for the observations row. Use '' for unqualified.
|
|
75
77
|
* @returns {string} SQL boolean expression (already parenthesized; safe to combine with AND/OR)
|
|
76
78
|
*/
|
|
79
|
+
// β refactor (#7877 applied): delegated to lib/low-signal-patterns.mjs.
|
|
80
|
+
// The SQL path (this), the regex path (utils.mjs::LOW_SIGNAL_TITLE), and the
|
|
81
|
+
// pre-tool-recall.js inline SQL now all derive from one authoritative
|
|
82
|
+
// pattern list. Previously hand-mirrored with "keep in sync" comments.
|
|
77
83
|
export function notLowSignalTitleClause(alias = 'o') {
|
|
78
|
-
|
|
79
|
-
// Bug #2 fix: replace `title != '(error)'` (exact match only) with
|
|
80
|
-
// `title NOT LIKE '%(error)'` (suffix match) so titles like
|
|
81
|
-
// "gh release list ... (error)" — produced when makeEntryDesc tags a failed
|
|
82
|
-
// tool invocation — are excluded too. The LIKE form subsumes the exact match.
|
|
83
|
-
// Keep in sync with LOW_SIGNAL_TITLE regex in utils.mjs.
|
|
84
|
-
return `(
|
|
85
|
-
${p}title NOT LIKE 'Modified %'
|
|
86
|
-
AND ${p}title NOT LIKE 'Worked on %'
|
|
87
|
-
AND ${p}title NOT LIKE 'Reviewed % files:%'
|
|
88
|
-
AND ${p}title NOT LIKE 'Error while working%'
|
|
89
|
-
AND ${p}title NOT LIKE 'Error in %'
|
|
90
|
-
AND ${p}title NOT LIKE 'Error: %'
|
|
91
|
-
AND ${p}title NOT LIKE '# %'
|
|
92
|
-
AND ${p}title NOT LIKE 'node %'
|
|
93
|
-
AND ${p}title NOT LIKE 'npm %'
|
|
94
|
-
AND ${p}title NOT LIKE 'npx %'
|
|
95
|
-
AND ${p}title NOT LIKE '(no description)%'
|
|
96
|
-
AND ${p}title NOT LIKE '%(error)'
|
|
97
|
-
)`;
|
|
84
|
+
return buildNotLowSignalSql(alias);
|
|
98
85
|
}
|
|
@@ -70,10 +70,11 @@ try {
|
|
|
70
70
|
// (notably sdscc) silently drop plain-text stdout from PreToolUse — the previous
|
|
71
71
|
// console.log() form would render on stock CC but no-op on those variants.
|
|
72
72
|
// Token budget: ~4 chars per token, 4000 token limit = 16000 chars.
|
|
73
|
+
const portablePath = resolvedPath.startsWith(homedir()) ? '~' + resolvedPath.slice(homedir().length) : resolvedPath;
|
|
73
74
|
let additionalContext;
|
|
74
75
|
if (content.length > 16000) {
|
|
75
76
|
const summary = content.slice(0, 800);
|
|
76
|
-
additionalContext = `<skill-bridge name="${row.name}" source="managed" truncated="true">\n${summary}\n...\n</skill-bridge>\n\nSkill content truncated.
|
|
77
|
+
additionalContext = `<skill-bridge name="${row.name}" source="managed" truncated="true">\n${summary}\n...\n</skill-bridge>\n\nSkill content truncated. Read("${portablePath}") to load full content.`;
|
|
77
78
|
} else {
|
|
78
79
|
additionalContext = `<skill-bridge name="${row.name}" source="managed">\n${content}\n</skill-bridge>\n\nThis skill was loaded from the managed registry. Follow the instructions above.`;
|
|
79
80
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// claude-mem-lite: PreToolUse file recall — injects lessons before Edit/Write
|
|
3
|
-
// Lightweight standalone (~30ms): only imports better-sqlite3, fs, path, os
|
|
3
|
+
// Lightweight standalone (~30ms): only imports better-sqlite3, fs, path, os,
|
|
4
|
+
// and the pure-data lib/low-signal-patterns.mjs (zero runtime deps, ~1ms overhead).
|
|
4
5
|
// Safety: readonly DB, exit 0 always, 3s timeout
|
|
5
6
|
|
|
6
7
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'fs';
|
|
7
8
|
import { basename, join } from 'path';
|
|
8
9
|
import { homedir } from 'os';
|
|
10
|
+
import { buildNotLowSignalSql } from '../lib/low-signal-patterns.mjs';
|
|
9
11
|
|
|
10
12
|
// CLAUDE_MEM_DB_PATH / CLAUDE_MEM_RUNTIME_DIR env overrides allow tests and debug tools to
|
|
11
13
|
// point the hook at an isolated DB + cooldown dir without touching the user's real state.
|
|
@@ -149,11 +151,19 @@ try {
|
|
|
149
151
|
// fallback — decision/bugfix WITHOUT lesson add context noise to passive Reads
|
|
150
152
|
// where the agent isn't committed to a change). Edit/Write keep the wider
|
|
151
153
|
// filter for decision-point context.
|
|
154
|
+
// LOW_SIGNAL title patterns — auto-generated hook-llm fallback titles carry
|
|
155
|
+
// no actionable guidance. β refactor (#7877 applied): derived from
|
|
156
|
+
// lib/low-signal-patterns.mjs so this cold-start script, utils.mjs regex,
|
|
157
|
+
// and scoring-sql.mjs SQL share one authoritative list.
|
|
158
|
+
const notLowSignalSql = buildNotLowSignalSql('o');
|
|
159
|
+
// Edit: bugfix/decision without lesson_learned is admitted only when the
|
|
160
|
+
// title isn't a LOW_SIGNAL auto-fallback (those carry pipe-delimited raw
|
|
161
|
+
// output or filename-stubs, no guidance value for the about-to-Edit agent).
|
|
152
162
|
const typeFallback = isRead
|
|
153
163
|
? 'AND o.lesson_learned IS NOT NULL AND o.lesson_learned != \'\''
|
|
154
164
|
: `AND (
|
|
155
165
|
(o.lesson_learned IS NOT NULL AND o.lesson_learned != '')
|
|
156
|
-
OR o.type IN ('bugfix', 'decision')
|
|
166
|
+
OR (o.type IN ('bugfix', 'decision') AND ${notLowSignalSql})
|
|
157
167
|
)`;
|
|
158
168
|
const obsLimit = isRead ? 1 : 2;
|
|
159
169
|
const rows = db.prepare(`
|
package/server.mjs
CHANGED
|
@@ -105,9 +105,22 @@ const RECENCY_HALF_LIFE_MS = DEFAULT_DECAY_HALF_LIFE_MS;
|
|
|
105
105
|
|
|
106
106
|
// ─── MCP Server ─────────────────────────────────────────────────────────────
|
|
107
107
|
|
|
108
|
+
// Emit one-line instructions-mode trace on stderr so debugging the "why did
|
|
109
|
+
// the server send BASE instead of BASE+VERBOSE?" path doesn't require reading
|
|
110
|
+
// three files (server.mjs → hook-shared.mjs → memdir.mjs). CLAUDE_MEM_QUIET_TRACE=0
|
|
111
|
+
// opts out. stderr doesn't pollute the MCP stdio protocol channel.
|
|
112
|
+
const _quiet = effectiveQuiet();
|
|
113
|
+
if (process.env.CLAUDE_MEM_QUIET_TRACE !== '0') {
|
|
114
|
+
const reason = process.env.MEM_QUIET_HOOKS === '1'
|
|
115
|
+
? 'env:MEM_QUIET_HOOKS=1'
|
|
116
|
+
: _quiet ? 'adopted:MEMORY.md-sentinel' : 'none';
|
|
117
|
+
const mode = _quiet ? 'BASE' : 'BASE+VERBOSE';
|
|
118
|
+
process.stderr.write(`[mem] instructions: ${mode} reason=${reason}\n`);
|
|
119
|
+
}
|
|
120
|
+
|
|
108
121
|
const server = new McpServer(
|
|
109
122
|
{ name: 'claude-mem-lite', version: PKG_VERSION },
|
|
110
|
-
{ instructions: buildServerInstructions(
|
|
123
|
+
{ instructions: buildServerInstructions(_quiet) },
|
|
111
124
|
);
|
|
112
125
|
|
|
113
126
|
// Track MCP request activity for idle-time cleanup (see idle timer below)
|
|
@@ -558,7 +571,13 @@ server.registerTool(
|
|
|
558
571
|
if (args.project) args = { ...args, project: resolveProject(args.project) };
|
|
559
572
|
const limit = args.limit ?? 20;
|
|
560
573
|
const offset = args.offset ?? 0;
|
|
561
|
-
|
|
574
|
+
// args.or (Batch A CLI↔MCP alignment): force OR from start, matching
|
|
575
|
+
// CLI `search --or`. The default path still does AND with OR-fallback
|
|
576
|
+
// inside searchObservations when AND returns 0.
|
|
577
|
+
let ftsQuery = sanitizeFtsQuery(args.query);
|
|
578
|
+
if (ftsQuery && args.or) {
|
|
579
|
+
ftsQuery = relaxFtsQueryToOr(ftsQuery) || ftsQuery;
|
|
580
|
+
}
|
|
562
581
|
const searchType = args.type;
|
|
563
582
|
const currentProject = inferProject();
|
|
564
583
|
|
|
@@ -1083,6 +1102,16 @@ server.registerTool(
|
|
|
1083
1102
|
safeHandler(async (args) => {
|
|
1084
1103
|
if (args.project) args = { ...args, project: resolveProject(args.project) };
|
|
1085
1104
|
const days = args.days ?? 30;
|
|
1105
|
+
|
|
1106
|
+
// Batch A CLI↔MCP alignment: quality:true → quality dashboard (lesson
|
|
1107
|
+
// rate, LOW_SIGNAL rate, per-type hit/lesson %, top lessons, R-2 watchdog).
|
|
1108
|
+
// Same computation + format as CLI `stats --quality` via lib/stats-quality.mjs.
|
|
1109
|
+
if (args.quality) {
|
|
1110
|
+
const { computeQualityStats, formatQualityReport } = await import('./lib/stats-quality.mjs');
|
|
1111
|
+
const data = computeQualityStats(db, { project: args.project, days });
|
|
1112
|
+
return { content: [{ type: 'text', text: formatQualityReport(data) }] };
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1086
1115
|
const cutoff = Date.now() - days * 86400000;
|
|
1087
1116
|
const projectFilter = args.project ? 'AND project = ?' : '';
|
|
1088
1117
|
const baseParams = args.project ? [args.project] : [];
|
package/source-files.mjs
CHANGED
|
@@ -34,6 +34,9 @@ export const SOURCE_FILES = [
|
|
|
34
34
|
'lib/git-state.mjs',
|
|
35
35
|
'lib/startup-dashboard.mjs',
|
|
36
36
|
'lib/doctor-benchmark.mjs',
|
|
37
|
+
'lib/doctor-drift.mjs',
|
|
38
|
+
'lib/stats-quality.mjs',
|
|
39
|
+
'lib/low-signal-patterns.mjs',
|
|
37
40
|
// v2.32 invited-memory: memdir primitives + adopt/unadopt CLI
|
|
38
41
|
'memdir.mjs',
|
|
39
42
|
'adopt-content.mjs',
|
package/tool-schemas.mjs
CHANGED
|
@@ -42,6 +42,7 @@ export const memSearchSchema = {
|
|
|
42
42
|
offset: coerceInt.pipe(z.number().int().min(0)).optional().describe('Offset for pagination'),
|
|
43
43
|
sort: z.enum(['relevance', 'time', 'importance']).optional().describe('Sort order: relevance (default, BM25), time (newest first), importance (highest first)'),
|
|
44
44
|
include_noise: z.boolean().optional().describe('Include hook-llm fallback titles ("Modified X", "Worked on X", raw error logs) — hidden by default as they have ~3% access rate'),
|
|
45
|
+
or: coerceBool.optional().describe('Force OR semantics between query terms from the start (default: AND with automatic OR-fallback when AND returns 0). Aligns with CLI --or.'),
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
export const memRecentSchema = {
|
|
@@ -81,6 +82,7 @@ export const memSaveSchema = {
|
|
|
81
82
|
export const memStatsSchema = {
|
|
82
83
|
project: z.string().optional().describe('Filter by project'),
|
|
83
84
|
days: coerceInt.pipe(z.number().int().min(1).max(365)).optional().describe('Look back N days (default 30)'),
|
|
85
|
+
quality: coerceBool.optional().describe('Return quality dashboard (lesson rate, LOW_SIGNAL rate, per-type hit/lesson %, top-accessed lessons, R-2 watchdog) instead of default stats. Aligns with CLI --quality.'),
|
|
84
86
|
};
|
|
85
87
|
|
|
86
88
|
export const memCompressSchema = {
|
package/utils.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { basename, dirname, resolve, sep } from 'path';
|
|
6
6
|
import { execSync } from 'child_process';
|
|
7
|
+
import { buildLowSignalRegex } from './lib/low-signal-patterns.mjs';
|
|
7
8
|
|
|
8
9
|
// ─── Re-exports from extracted modules ──────────────────────────────────────
|
|
9
10
|
// Backward compatibility: all consumers import from utils.mjs
|
|
@@ -94,8 +95,12 @@ export const EDIT_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit']);
|
|
|
94
95
|
// 2. \(error\)$ — title ends with '(error)' (Bug #2 fix: previously this was
|
|
95
96
|
// inside the prefix group with a meaningless $, so only the exact title '(error)' matched.
|
|
96
97
|
// Tool-fragment titles like 'gh release list ... (error)' leaked through.)
|
|
97
|
-
//
|
|
98
|
-
|
|
98
|
+
//
|
|
99
|
+
// β refactor (#7877 applied): derived from lib/low-signal-patterns.mjs so the
|
|
100
|
+
// regex path (this) and the SQL NOT LIKE path (scoring-sql.mjs::notLowSignalTitleClause)
|
|
101
|
+
// and pre-tool-recall.js inline SQL all share one authoritative pattern list.
|
|
102
|
+
// Previously these were hand-mirrored via "keep in sync" comments.
|
|
103
|
+
export const LOW_SIGNAL_TITLE = buildLowSignalRegex();
|
|
99
104
|
|
|
100
105
|
export function computeRuleImportance(episode) {
|
|
101
106
|
let importance = 1;
|