claude-mem-lite 2.43.0 → 2.44.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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "2.43.0",
13
+ "version": "2.44.0",
14
14
  "source": "./",
15
15
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall"
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.43.0",
3
+ "version": "2.44.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall",
5
5
  "author": {
6
6
  "name": "sdsrss"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.43.0",
3
+ "version": "2.44.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "engines": {
@@ -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 ftsRows = searchByFts(db, promptText, project, intent?.limit || MAX_RESULTS, intent?.type || null);
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 (ftsRows.length === 0 && intent?.type) {
392
- ftsRows = searchByFts(db, promptText, project, intent.limit || MAX_RESULTS, null);
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 = {