claude-mem-lite 3.24.0 → 3.26.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": "3.24.0",
13
+ "version": "3.26.0",
14
14
  "source": "./",
15
15
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.24.0",
3
+ "version": "3.26.0",
4
4
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
5
5
  "author": {
6
6
  "name": "sdsrss"
package/hook.mjs CHANGED
@@ -1424,7 +1424,10 @@ async function handleUserPrompt() {
1424
1424
  process.stdout.write(lines.join('\n') + '\n');
1425
1425
  }
1426
1426
  if (imperativePick) {
1427
- process.stdout.write(formatTaskImperative(imperativePick.lesson_learned, imperativePick.id) + '\n');
1427
+ // Guard the write on a non-empty return — formatTaskImperative yields '' for a
1428
+ // lesson that strips to empty (e.g. "."), which would otherwise emit a bare line.
1429
+ const imperativeLine = formatTaskImperative(imperativePick.lesson_learned, imperativePick.id);
1430
+ if (imperativeLine) process.stdout.write(imperativeLine + '\n');
1428
1431
  }
1429
1432
  } catch (e) { debugCatch(e, 'handleUserPrompt-memory'); }
1430
1433
  } finally {
package/mem-cli.mjs CHANGED
@@ -2867,7 +2867,10 @@ async function cmdOptimize(db, args) {
2867
2867
  // Sibling-command flag footgun: optimize executes with --run (compress uses
2868
2868
  // --execute, maintain uses positional `execute`). A bare --execute previously fell
2869
2869
  // through to a silent preview, so the user thought they ran a mutation but didn't.
2870
- if (args.includes('--execute') && !run && !runAll) {
2870
+ // Catch both `--execute` and the `--execute=value` form (the latter slipped a bare
2871
+ // `args.includes('--execute')` and fell through to a silent preview — review minor).
2872
+ const hasExecuteFlag = args.some(a => a === '--execute' || a.startsWith('--execute='));
2873
+ if (hasExecuteFlag && !run && !runAll) {
2871
2874
  fail("[mem] optimize executes with --run, not --execute (--execute is compress's flag). Re-run: claude-mem-lite optimize --run");
2872
2875
  return;
2873
2876
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.24.0",
3
+ "version": "3.26.0",
4
4
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",
@@ -116,7 +116,7 @@ function inferProject() {
116
116
  const parent = basename(join(dir, '..'));
117
117
  const raw = (parent && parent !== '.' && parent !== '/')
118
118
  ? `${parent}--${base}` : base;
119
- return raw.replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 100) || 'unknown';
119
+ return raw.replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 100);
120
120
  }
121
121
 
122
122
  function readCooldown(cooldownPath) {
@@ -172,6 +172,20 @@ function isFollowUpSession() {
172
172
  // catches the prompt via those channels rather than the identifier itself.
173
173
  const TECH_IDENTIFIER_RE = /\b(?:[a-z][a-z0-9]*_[a-z0-9_]+|[A-Z][A-Z0-9]*_[A-Z0-9_]+|[A-Z]{2,}[0-9][A-Z0-9_]*|[a-z]{2,}[A-Z][a-zA-Z0-9]+|[a-z]+(?:-[a-z]+){2,})\b/;
174
174
 
175
+ // Reviewer #2 (v3.25.0): the kebab (≥3-seg) and camelCase arms above structurally
176
+ // match a handful of ordinary English phrases / product names that are NOT code
177
+ // identifiers — `up-to-date`, `state-of-the-art`, `macOS`, etc. Left unfiltered they
178
+ // (a) widen the default-ON hasExplicitSignal gate on prose-only prompts and (b) let
179
+ // such a token drive the opt-in identifier bypass. Stop-list them (lowercased). Real
180
+ // 3-segment kebab identifiers (`pre-tool-use`, `user-prompt-search`) are deliberately
181
+ // NOT here — only attested non-identifier prose.
182
+ const IDENTIFIER_STOPWORDS = new Set([
183
+ 'up-to-date', 'out-of-date', 'up-to-speed', 'out-of-the-box', 'state-of-the-art',
184
+ 'end-to-end', 'off-by-one', 'easy-to-use', 'day-to-day', 'step-by-step',
185
+ 'face-to-face', 'one-to-one', 'one-on-one', 'back-to-back', 'side-by-side',
186
+ 'apples-to-apples', 'macos',
187
+ ]);
188
+
175
189
  // CJK presence channel (Important #2): bilingual users (project memory
176
190
  // `feedback_*` calls this out explicitly) ask CJK questions that may carry
177
191
  // genuine debug intent without containing an English identifier. CJK is
@@ -192,34 +206,37 @@ export function hasExplicitSignal(text, { errSig, files, intent } = {}) {
192
206
  if (errSig === undefined && extractErrorSignature(text)) return true;
193
207
  if (files === undefined && extractFiles(text).length > 0) return true;
194
208
  if (intent === undefined && detectIntent(text)) return true;
195
- if (TECH_IDENTIFIER_RE.test(text)) return true;
209
+ if (extractTechIdentifiers(text).length > 0) return true;
196
210
  if (CJK_CHAR_RE.test(text) && computeEffectiveLen(text) >= CJK_MIN_EFFECTIVE_LEN) return true;
197
211
  return false;
198
212
  }
199
213
 
200
- // ─── Identifier-exact-match precision bypass (default OFF) ───────────────────
214
+ // ─── Identifier-exact-match precision bypass (default ON since v3.26.0) ──────
201
215
  //
202
- // CLAUDE_MEM_UPS_IDENTIFIER_BYPASS=1 enables it. Rationale: the score-floors below
216
+ // Set CLAUDE_MEM_UPS_IDENTIFIER_BYPASS=0 to disable. Rationale: the score-floors below
203
217
  // (OR_TOP_BM25_FLOOR / TOP_REL_FLOOR) drop the WHOLE FTS set when the top row's
204
218
  // magnitude is weak. But a rare code identifier (camelCase / snake_case / CONST_CASE
205
219
  // / kebab≥3) match has high *semantic* precision even at modest BM25 — a df=1 term
206
220
  // like `sanitizeFtsQuery` scores only ~23 raw yet is an unambiguous hit. So an obs
207
221
  // whose title/lesson EXACT-matches an identifier the prompt names is a precision pass
208
222
  // — the same independent-signal rationale that already exempts sigRows (error
209
- // signatures) and fileRows (file names) from these floors. When enabled, such rows are
210
- // restored after the floors run.
223
+ // signatures) and fileRows (file names) from these floors. Such rows are restored
224
+ // after the floors run.
211
225
  //
212
- // Default OFFopt-in. benchmark/ups-ab.mjs measured it TRADEOFF on the real corpus
213
- // (+3 recall recoveries / +4 injections over 12 positives / 8 hard-negatives); the
214
- // "cost" was eager surfacing of on-identifier obs on non-recall-framed prompts, not
215
- // off-topic noise. Kept opt-in (below the NET-POSITIVE/NEUTRAL ship bar). See #8858.
216
- export const IDENTIFIER_BYPASS = process.env.CLAUDE_MEM_UPS_IDENTIFIER_BYPASS === '1';
226
+ // Default ONflipped in v3.26.0 after a corrected benchmark/ups-ab.mjs re-measurement
227
+ // (#8858 two-bucket: TRUE off-topic FP isolated from on-topic eagerness). Over 12
228
+ // positives / 7 true-precision hard-negatives: recall up, TRUE off-topic FP = 0 (stable
229
+ // ×3 runs) VERDICT NET-POSITIVE. The only behavior delta is on-topic eagerness (naming
230
+ // an identifier surfaces its obs) — the highest-precision injection trigger there is. The
231
+ // prose stop-list (IDENTIFIER_STOPWORDS) keeps the extractor off ordinary English.
232
+ export const IDENTIFIER_BYPASS = process.env.CLAUDE_MEM_UPS_IDENTIFIER_BYPASS !== '0';
217
233
  const TECH_IDENTIFIER_RE_G = new RegExp(TECH_IDENTIFIER_RE.source, 'g');
218
234
 
219
235
  // All tech-identifier tokens in `text`, lowercased + de-duped (for case-insensitive
220
236
  // row matching). Empty array when none — callers treat that as "no bypass candidates".
221
237
  export function extractTechIdentifiers(text) {
222
- return [...new Set((String(text || '').match(TECH_IDENTIFIER_RE_G) || []).map(s => s.toLowerCase()))];
238
+ return [...new Set((String(text || '').match(TECH_IDENTIFIER_RE_G) || []).map(s => s.toLowerCase()))]
239
+ .filter(s => !IDENTIFIER_STOPWORDS.has(s));
223
240
  }
224
241
 
225
242
  // True when the obs row's title or lesson contains any of `idsLower` as a standalone
@@ -600,8 +617,8 @@ async function main() {
600
617
  const signalPresent = hasExplicitSignal(promptText, {
601
618
  errSig, files: filesForGate, intent,
602
619
  });
603
- // Identifier tokens the prompt names (for the precision bypass below). Empty
604
- // unless CLAUDE_MEM_UPS_IDENTIFIER_BYPASS=1, so this is a no-op when disabled.
620
+ // Identifier tokens the prompt names (for the precision bypass below). Empty only
621
+ // when CLAUDE_MEM_UPS_IDENTIFIER_BYPASS=0 (bypass is default-on), then it is a no-op.
605
622
  const promptIdentifiers = IDENTIFIER_BYPASS ? extractTechIdentifiers(promptText) : [];
606
623
 
607
624
  if (intent?.useRecent) {
@@ -634,7 +651,7 @@ async function main() {
634
651
  typeof r.relevance === 'number' && Math.abs(r.relevance) >= bm25Floor
635
652
  );
636
653
 
637
- // Identifier-exact-match precision bypass (default off — see IDENTIFIER_BYPASS).
654
+ // Identifier-exact-match precision bypass (default on — see IDENTIFIER_BYPASS).
638
655
  // Capture rows that exact-match a prompt identifier BEFORE the set-floors below;
639
656
  // they carry independent precision signal (sigRows/fileRows rationale) and are
640
657
  // restored after the floors so a low top-score can't drop a named-identifier hit.
@@ -151,12 +151,14 @@ export function markSuperseded(results) {
151
151
  // Persistent superseding belongs in mem_maintain/mem_compress write paths.
152
152
  for (const [, obsForFile] of fileMap) {
153
153
  if (obsForFile.length < 2) continue;
154
- // Sort newest-first by recency. The production search pipeline supplies
155
- // created_at_epoch (numeric) + created_at (ISO) NOT `date`; the prior
156
- // `b.date` sort silently no-op'd there (every key undefined localeCompare 0),
157
- // so "newest" degraded to relevance order and a current top-importance obs got
158
- // a false [SUPERSEDED] tag (#8821). Prefer numeric epoch, fall back to the ISO
159
- // string (lexically chronological), then legacy `date` for older callers/tests.
154
+ // Sort newest-first by recency. Every production obs row carries `date`
155
+ // (= created_at ISO, set by ftsRowToResult); the FTS/type paths also carry
156
+ // created_at_epoch, the vector/type-list paths carry date-only. So the prior
157
+ // `b.date` sort already ordered correctly in production this is defensive
158
+ // hardening, not a prod no-op fix (a post-release review corrected the original
159
+ // #8821 "missing-`date`" diagnosis). Prefer the numeric epoch, fall back to the
160
+ // ISO created_at (lexically chronological), then legacy `date`, so the key holds
161
+ // for any caller regardless of which recency fields it supplies.
160
162
  obsForFile.sort((a, b) => {
161
163
  const ka = a.created_at_epoch ?? a.created_at ?? a.date ?? '';
162
164
  const kb = b.created_at_epoch ?? b.created_at ?? b.date ?? '';