claude-mem-lite 2.43.0 → 2.45.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 +20 -5
- package/install.mjs +1 -1
- package/mem-cli.mjs +5 -1
- package/package.json +1 -1
- package/scripts/prompt-search-utils.mjs +15 -0
- package/scripts/user-prompt-search.js +60 -8
- package/server.mjs +3 -0
- package/tool-schemas.mjs +1 -0
package/hook-llm.mjs
CHANGED
|
@@ -431,15 +431,30 @@ export function buildImmediateObservation(episode) {
|
|
|
431
431
|
}
|
|
432
432
|
|
|
433
433
|
const ruleImportance = computeRuleImportance(episode);
|
|
434
|
-
// Low-signal degraded titles ("
|
|
435
|
-
//
|
|
434
|
+
// Low-signal degraded titles ("Modified X", "Worked on X", "Reviewed N files")
|
|
435
|
+
// should not inflate importance. computeRuleImportance's file-name heuristics
|
|
436
|
+
// (schema.*, migration, auth.*, .env, .pem) fire on any matching file in the
|
|
437
|
+
// episode, so a 5-file review that incidentally reads one schema.js triggers
|
|
438
|
+
// imp=3 even though schema.js was one of 5 scanned — not the focus. Combined
|
|
439
|
+
// with a LOW_SIGNAL title (Haiku couldn't extract meaning), we can't justify
|
|
440
|
+
// imp=3; cap at 2 so rule says "notable" but not "critical".
|
|
441
|
+
//
|
|
442
|
+
// Production baseline (2026-04-23, projects--mem): 34/100 discovery/imp=3
|
|
443
|
+
// obs were LOW_SIGNAL titles; 7 change/imp=3 same. Prior cap `rule<=2 → 1`
|
|
444
|
+
// only fired when rule was weak, letting rule=3 leak through. New cap:
|
|
445
|
+
// isReviewPattern → 2 (was Math.max(2, rule) → rule=3 leaked as 3)
|
|
446
|
+
// isLowSignal & !review:
|
|
447
|
+
// rule=3 → 2 (was 3) — the fix
|
|
448
|
+
// rule<=2 → 1 (unchanged) — original cap preserved
|
|
436
449
|
const LOW_SIGNAL = LOW_SIGNAL_TITLE;
|
|
437
450
|
const isLowSignal = LOW_SIGNAL.test(title);
|
|
438
451
|
let importance;
|
|
439
452
|
if (isReviewPattern) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
importance =
|
|
453
|
+
// Review titles are auto-generated from file count — can't distinguish
|
|
454
|
+
// "critical file was primary focus" from "one of N files read". Cap at 2.
|
|
455
|
+
importance = 2;
|
|
456
|
+
} else if (isLowSignal) {
|
|
457
|
+
importance = ruleImportance === 3 ? 2 : 1;
|
|
443
458
|
} else {
|
|
444
459
|
importance = ruleImportance;
|
|
445
460
|
}
|
package/install.mjs
CHANGED
|
@@ -1205,7 +1205,7 @@ async function doctor() {
|
|
|
1205
1205
|
if (r.drift) {
|
|
1206
1206
|
const names = r.details.join(', ');
|
|
1207
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)`);
|
|
1208
|
+
warn(`Dev drift: ${r.plainCount} non-symlink file(s) in dev install: ${names}${suffix} (re-run: node ${join(PROJECT_DIR, 'install.mjs')} install --dev)`);
|
|
1209
1209
|
issues++;
|
|
1210
1210
|
} else if (r.devMode) {
|
|
1211
1211
|
ok(`Dev drift: clean (${r.symlinkCount} symlinks, 0 plain)`);
|
package/mem-cli.mjs
CHANGED
|
@@ -484,23 +484,26 @@ function cmdRecall(db, args) {
|
|
|
484
484
|
const { positional, flags } = parseArgs(args);
|
|
485
485
|
const file = positional.join(' ');
|
|
486
486
|
if (!file) {
|
|
487
|
-
fail('[mem] Usage: mem recall <file>');
|
|
487
|
+
fail('[mem] Usage: mem recall <file> [--limit N] [--include-noise]');
|
|
488
488
|
return;
|
|
489
489
|
}
|
|
490
490
|
|
|
491
491
|
const filename = basename(file);
|
|
492
492
|
const rawLimit = flags.limit !== undefined ? parseInt(flags.limit, 10) : NaN;
|
|
493
493
|
const limit = Number.isInteger(rawLimit) ? Math.max(1, rawLimit) : 10;
|
|
494
|
+
const includeNoise = flags['include-noise'] === true || flags['include-noise'] === 'true';
|
|
494
495
|
|
|
495
496
|
// Search via observation_files junction table for indexed filename lookups
|
|
496
497
|
const escaped = filename.replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
497
498
|
const likePattern = `%${escaped}`;
|
|
499
|
+
const noiseClause = includeNoise ? '' : `AND ${notLowSignalTitleClause('o')}`;
|
|
498
500
|
const rows = db.prepare(`
|
|
499
501
|
SELECT DISTINCT o.id, o.type, o.title, o.lesson_learned, o.created_at, o.project
|
|
500
502
|
FROM observations o
|
|
501
503
|
JOIN observation_files of2 ON of2.obs_id = o.id
|
|
502
504
|
WHERE COALESCE(o.compressed_into, 0) = 0
|
|
503
505
|
AND (of2.filename = ? OR of2.filename LIKE ? ESCAPE '\\')
|
|
506
|
+
${noiseClause}
|
|
504
507
|
ORDER BY o.created_at_epoch DESC
|
|
505
508
|
LIMIT ?
|
|
506
509
|
`).all(filename, likePattern, limit);
|
|
@@ -1972,6 +1975,7 @@ Commands:
|
|
|
1972
1975
|
|
|
1973
1976
|
recall <file> Show observations related to a file
|
|
1974
1977
|
--limit N Max results (default 10)
|
|
1978
|
+
--include-noise Include hook-llm fallback titles ("Modified X", raw error logs)
|
|
1975
1979
|
|
|
1976
1980
|
get <id1,id2,...> Get full details by ID
|
|
1977
1981
|
IDs accept search-output prefixes: #123 (obs), P#123 (prompt), S#123 (session).
|
package/package.json
CHANGED
|
@@ -9,6 +9,19 @@ const CONFIRM_RE = /^(y(es)?|no?|ok|done|go|sure|lgtm|thanks?|ty|继续|确认|
|
|
|
9
9
|
const SLASH_CMD_RE = /^\//;
|
|
10
10
|
const PURE_OP_RE = /^(git\s+(commit|push|merge)|npm\s+(publish|deploy))\b/i;
|
|
11
11
|
|
|
12
|
+
// v2.43.x: pure continuation directives — "keep going on what you were doing"
|
|
13
|
+
// with no new topic. Long enough to evade CONFIRM_RE / length gate but
|
|
14
|
+
// semantically empty for memory-recall purposes; injecting [mem] context
|
|
15
|
+
// here reads like a turn boundary and can prematurely end the model's
|
|
16
|
+
// in-flight tool chain. Conservative match: must be SOLELY the directive,
|
|
17
|
+
// not directive + new instruction (those keep getting injection).
|
|
18
|
+
const CONTINUATION_RE = /^(继续|接着|继续做|接着做|继续干|继续做下一步|接着做下一步|别停|不要停|next|continue|go\s*on|keep\s+going|carry\s+on|proceed|more(?:\s+please)?)\s*[??!!。.,,]*\s*$/i;
|
|
19
|
+
|
|
20
|
+
// v2.43.x: meta-pause questions — user is asking the model to reflect on
|
|
21
|
+
// its own pause/stop, then continue. No new topic = no useful memory hit;
|
|
22
|
+
// injection just adds reminder noise on top of an already-reflective turn.
|
|
23
|
+
const META_PAUSE_RE = /(怎么停|为什么停|为何停|你怎么停|工作停下来|刚才停|why\s+(?:did\s+you\s+)?(?:stop|pause|halt))/i;
|
|
24
|
+
|
|
12
25
|
/**
|
|
13
26
|
* CJK-weighted effective length. CJK characters (CJK Unified Ideographs
|
|
14
27
|
* main + extension A) carry ~3x the semantic token density of Latin
|
|
@@ -30,6 +43,8 @@ export function shouldSkip(text) {
|
|
|
30
43
|
if (CONFIRM_RE.test(trimmed)) return true;
|
|
31
44
|
if (SLASH_CMD_RE.test(trimmed)) return true;
|
|
32
45
|
if (PURE_OP_RE.test(trimmed)) return true;
|
|
46
|
+
if (CONTINUATION_RE.test(trimmed)) return true;
|
|
47
|
+
if (META_PAUSE_RE.test(trimmed)) return true;
|
|
33
48
|
return false;
|
|
34
49
|
}
|
|
35
50
|
|
|
@@ -66,6 +66,34 @@ const FOLLOWUP_BM25_MIN_SCORE = Number(process.env.CLAUDE_MEM_UPS_BM25_MIN_FOLLO
|
|
|
66
66
|
// gate exists to drop.
|
|
67
67
|
const TOP_REL_FLOOR = Number(process.env.CLAUDE_MEM_UPS_TOP_MIN || 50);
|
|
68
68
|
|
|
69
|
+
// v2.43.x: OR-fallback raw BM25 magnitude floor. The composite TOP_REL_FLOOR
|
|
70
|
+
// above gates on `bm25 × importance × type_quality × decay × noise_penalty`.
|
|
71
|
+
// For importance=3 bugfix obs, those multipliers compound to ~6×, so a modest
|
|
72
|
+
// BM25 of -17..-22 can clear a composite floor of 50 via inflation alone.
|
|
73
|
+
// When the FTS query relaxes to OR (AND returned 0), a single strongly-
|
|
74
|
+
// matching stem on a big multi-topic prompt leaks through — observed
|
|
75
|
+
// failure mode: broad Chinese prompts surfacing unrelated importance=3
|
|
76
|
+
// bugfix obs whose concepts share exactly one stem with the prompt.
|
|
77
|
+
//
|
|
78
|
+
// Empirical OR-mode distribution (11-prompt probe, 2026-04-23):
|
|
79
|
+
// real signal top-|bm25_raw| ≥ 41
|
|
80
|
+
// broad/meta noise top-|bm25_raw| ≤ 22
|
|
81
|
+
// below threshold top-|bm25_raw| < 12
|
|
82
|
+
// Default 30 sits in the clean 22→41 gap. AND mode bypasses this gate —
|
|
83
|
+
// AND's all-stems-must-match constraint is already a precision signal,
|
|
84
|
+
// and there are legitimate AND hits (GOOD-narrow probe: bm25_raw=19.3,
|
|
85
|
+
// rel=81) that we must not drop.
|
|
86
|
+
//
|
|
87
|
+
// CLAUDE_MEM_UPS_TOP_MIN=0 disables this too: on small test corpora (1–2
|
|
88
|
+
// seeded obs) absolute BM25 magnitudes collapse to near-zero (observed
|
|
89
|
+
// |bm25|≈4e-6) because FTS5 IDF normalization needs a real document
|
|
90
|
+
// distribution. The existing TOP_REL_FLOOR knob already encodes the
|
|
91
|
+
// "seed-mode: kill absolute floors" semantic for integration tests, so
|
|
92
|
+
// we piggy-back on it rather than introducing a second override env.
|
|
93
|
+
const OR_TOP_BM25_FLOOR = TOP_REL_FLOOR === 0
|
|
94
|
+
? 0
|
|
95
|
+
: Number(process.env.CLAUDE_MEM_UPS_OR_BM25_MIN || 30);
|
|
96
|
+
|
|
69
97
|
function isFollowUpSession() {
|
|
70
98
|
try {
|
|
71
99
|
const raw = readFileSync(INJECTED_IDS_FILE, 'utf8');
|
|
@@ -77,9 +105,15 @@ function isFollowUpSession() {
|
|
|
77
105
|
|
|
78
106
|
// ─── DB Query Functions ─────────────────────────────────────────────────────
|
|
79
107
|
|
|
108
|
+
// Returns { rows, mode } where mode is 'AND' (initial pass), 'OR' (fallback
|
|
109
|
+
// after AND returned 0), or null (no FTS query / sanitize rejected). Callers
|
|
110
|
+
// use `mode` to apply OR-specific gates — see OR_TOP_BM25_FLOOR rationale.
|
|
111
|
+
// Each row includes `bm25_raw` (pre-multiplier bm25 magnitude) alongside the
|
|
112
|
+
// composite `relevance`, so callers can distinguish raw-match strength from
|
|
113
|
+
// importance/type/decay inflation.
|
|
80
114
|
function searchByFts(db, queryText, project, limit, typeFilter) {
|
|
81
115
|
const ftsQuery = sanitizeFtsQuery(queryText);
|
|
82
|
-
if (!ftsQuery) return [];
|
|
116
|
+
if (!ftsQuery) return { rows: [], mode: null };
|
|
83
117
|
|
|
84
118
|
const cutoff = Date.now() - LOOKBACK_MS;
|
|
85
119
|
|
|
@@ -92,6 +126,7 @@ function searchByFts(db, queryText, project, limit, typeFilter) {
|
|
|
92
126
|
// docs/p0-injection-noise-baseline.txt.
|
|
93
127
|
const sql = `
|
|
94
128
|
SELECT o.id, o.type, o.title, o.lesson_learned,
|
|
129
|
+
${OBS_BM25} as bm25_raw,
|
|
95
130
|
${OBS_BM25}
|
|
96
131
|
* (1.0 + EXP(-0.693 * (? - o.created_at_epoch) / ${TYPE_DECAY_CASE}))
|
|
97
132
|
* ${TYPE_QUALITY_CASE}
|
|
@@ -115,6 +150,7 @@ function searchByFts(db, queryText, project, limit, typeFilter) {
|
|
|
115
150
|
params.push(limit);
|
|
116
151
|
|
|
117
152
|
let rows = db.prepare(sql).all(...params);
|
|
153
|
+
let mode = 'AND';
|
|
118
154
|
|
|
119
155
|
// OR fallback if AND query returned nothing
|
|
120
156
|
if (rows.length === 0) {
|
|
@@ -122,10 +158,11 @@ function searchByFts(db, queryText, project, limit, typeFilter) {
|
|
|
122
158
|
if (orQuery) {
|
|
123
159
|
params[1] = orQuery;
|
|
124
160
|
rows = db.prepare(sql).all(...params);
|
|
161
|
+
mode = 'OR';
|
|
125
162
|
}
|
|
126
163
|
}
|
|
127
164
|
|
|
128
|
-
return rows;
|
|
165
|
+
return { rows, mode };
|
|
129
166
|
}
|
|
130
167
|
|
|
131
168
|
function searchByFile(db, files, project, limit) {
|
|
@@ -256,7 +293,7 @@ const QUIET_HOOKS = process.env.MEM_QUIET_HOOKS === '1';
|
|
|
256
293
|
function formatResults(rows) {
|
|
257
294
|
if (!rows || rows.length === 0) return null;
|
|
258
295
|
|
|
259
|
-
const lines = ['[mem] Related memories:'];
|
|
296
|
+
const lines = ['[mem] FYI — Related memories (continue your task):'];
|
|
260
297
|
for (const r of rows) {
|
|
261
298
|
const icon = typeIcon(r.type);
|
|
262
299
|
const title = truncate(r.title || '', 70);
|
|
@@ -272,7 +309,7 @@ function formatResults(rows) {
|
|
|
272
309
|
// chars (slightly longer than obs titles because prompts carry more context).
|
|
273
310
|
function formatPromptResults(rows) {
|
|
274
311
|
if (!rows || rows.length === 0) return null;
|
|
275
|
-
const lines = ['[mem] Past similar questions:'];
|
|
312
|
+
const lines = ['[mem] FYI — Past similar questions (continue your task):'];
|
|
276
313
|
for (const r of rows) {
|
|
277
314
|
const text = truncate((r.prompt_text || '').replace(/\s+/g, ' '), 80);
|
|
278
315
|
lines.push(`P#${r.id} 💬 ${text}`);
|
|
@@ -375,7 +412,7 @@ async function main() {
|
|
|
375
412
|
// take priority slots in the merged output.
|
|
376
413
|
const errSig = extractErrorSignature(promptText);
|
|
377
414
|
const sigRows = errSig
|
|
378
|
-
? searchByFts(db, errSig.signature, project, 2, 'bugfix').filter(r =>
|
|
415
|
+
? searchByFts(db, errSig.signature, project, 2, 'bugfix').rows.filter(r =>
|
|
379
416
|
typeof r.relevance === 'number' && Math.abs(r.relevance) >= bm25Floor
|
|
380
417
|
)
|
|
381
418
|
: [];
|
|
@@ -386,11 +423,13 @@ async function main() {
|
|
|
386
423
|
} else {
|
|
387
424
|
// FTS search: use the prompt as query, optionally type-filtered
|
|
388
425
|
const files = extractFiles(promptText);
|
|
389
|
-
let
|
|
426
|
+
let ftsResult = searchByFts(db, promptText, project, intent?.limit || MAX_RESULTS, intent?.type || null);
|
|
390
427
|
// Fallback: if typed search returned nothing, retry without type filter
|
|
391
|
-
if (
|
|
392
|
-
|
|
428
|
+
if (ftsResult.rows.length === 0 && intent?.type) {
|
|
429
|
+
ftsResult = searchByFts(db, promptText, project, intent.limit || MAX_RESULTS, null);
|
|
393
430
|
}
|
|
431
|
+
let ftsRows = ftsResult.rows;
|
|
432
|
+
const ftsMode = ftsResult.mode;
|
|
394
433
|
const fileRows = files.length > 0 ? searchByFile(db, files, project, 2) : [];
|
|
395
434
|
|
|
396
435
|
// T3 (v2.31): BM25 magnitude threshold — drop FTS hits whose relevance
|
|
@@ -403,6 +442,19 @@ async function main() {
|
|
|
403
442
|
typeof r.relevance === 'number' && Math.abs(r.relevance) >= bm25Floor
|
|
404
443
|
);
|
|
405
444
|
|
|
445
|
+
// v2.43.x: OR-mode raw-BM25 floor. In OR-fallback mode the composite
|
|
446
|
+
// TOP_REL_FLOOR below is inflated by importance × type_quality × decay
|
|
447
|
+
// multipliers — a weak single-stem hit on an importance=3 bugfix obs
|
|
448
|
+
// can reach composite rel=66 while raw |bm25|=19. Gate on raw bm25
|
|
449
|
+
// magnitude for OR mode only; AND mode's all-stems-match constraint
|
|
450
|
+
// is a precision signal and routinely produces legitimate AND hits
|
|
451
|
+
// below raw |bm25|=20 that we do not want to drop (see GOOD-narrow
|
|
452
|
+
// probe). Skip gate when OR_TOP_BM25_FLOOR is set to 0 (test hook).
|
|
453
|
+
if (ftsMode === 'OR' && OR_TOP_BM25_FLOOR > 0 && ftsRows.length > 0) {
|
|
454
|
+
const topBm25 = Math.abs(ftsRows[0].bm25_raw || 0);
|
|
455
|
+
if (topBm25 < OR_TOP_BM25_FLOOR) ftsRows = [];
|
|
456
|
+
}
|
|
457
|
+
|
|
406
458
|
// v2.34.3: top-|rel| sanity gate. Per-row filtering above leaves noise
|
|
407
459
|
// prompts intact when many rows share a weak stem (all in 25..48 range).
|
|
408
460
|
// If the best remaining FTS match is below the top floor, drop the
|
package/server.mjs
CHANGED
|
@@ -2176,15 +2176,18 @@ server.registerTool(
|
|
|
2176
2176
|
safeHandler(async (args) => {
|
|
2177
2177
|
const filename = basename(args.file);
|
|
2178
2178
|
const limit = args.limit ?? 10;
|
|
2179
|
+
const includeNoise = args.include_noise === true;
|
|
2179
2180
|
|
|
2180
2181
|
const escaped = filename.replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
2181
2182
|
const likePattern = `%${escaped}`;
|
|
2183
|
+
const noiseClause = includeNoise ? '' : `AND ${notLowSignalTitleClause('o')}`;
|
|
2182
2184
|
const rows = db.prepare(`
|
|
2183
2185
|
SELECT DISTINCT o.id, o.type, o.title, o.lesson_learned, o.created_at, o.project
|
|
2184
2186
|
FROM observations o
|
|
2185
2187
|
JOIN observation_files of2 ON of2.obs_id = o.id
|
|
2186
2188
|
WHERE COALESCE(o.compressed_into, 0) = 0
|
|
2187
2189
|
AND (of2.filename = ? OR of2.filename LIKE ? ESCAPE '\\')
|
|
2190
|
+
${noiseClause}
|
|
2188
2191
|
ORDER BY o.created_at_epoch DESC
|
|
2189
2192
|
LIMIT ?
|
|
2190
2193
|
`).all(filename, likePattern, limit);
|
package/tool-schemas.mjs
CHANGED
|
@@ -211,6 +211,7 @@ export const memExportSchema = {
|
|
|
211
211
|
export const memRecallSchema = {
|
|
212
212
|
file: z.string().min(1).describe('File path or filename to recall observations for'),
|
|
213
213
|
limit: coerceInt.pipe(z.number().int().min(1).max(50)).optional().describe('Max results (default 10)'),
|
|
214
|
+
include_noise: z.boolean().optional().describe('Include hook-llm fallback titles ("Modified X", "Worked on X", raw error logs) — hidden by default for parity with mem_search'),
|
|
214
215
|
};
|
|
215
216
|
|
|
216
217
|
export const memFtsCheckSchema = {
|