claude-mem-lite 3.25.0 → 3.27.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.25.0",
13
+ "version": "3.27.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.25.0",
3
+ "version": "3.27.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"
@@ -290,7 +290,7 @@ export async function coreRunSearchPipeline(ctx, opts) {
290
290
  const {
291
291
  db, currentProject = null, env = process.env,
292
292
  searchObservationsHybrid, deepSearch, shouldEscalateToDeep,
293
- autoDeepLlmReady, reRankWithContext, markSuperseded,
293
+ autoDeepLlmReady, reRankWithContext,
294
294
  llm = null, rerankLlm = undefined,
295
295
  } = ctx;
296
296
  const {
@@ -437,14 +437,13 @@ export async function coreRunSearchPipeline(ctx, opts) {
437
437
  else if (crossSourceEpochSortNoFts) results.sort((a, b) => (b.created_at_epoch ?? 0) - (a.created_at_epoch ?? 0));
438
438
  }
439
439
 
440
- // ── Context re-rank + superseded marking (markSuperseded is pure stale-tagging) ──
440
+ // ── Context re-rank ──
441
441
  const hasObs = results.some((r) => r.source === 'obs');
442
442
  const rerankGate = rerankPolicy === 'mcp' ? ((ftsQuery || isDeep) && hasObs) : hasObs;
443
443
  if (rerankGate) {
444
444
  const obsResults = results.filter((r) => r.source === 'obs');
445
445
  const doReRank = rerankPolicy === 'mcp' ? (ftsQuery && !deepReranked) : !deepReranked;
446
446
  if (doReRank) reRankWithContext(db, obsResults, rerankProject);
447
- markSuperseded(obsResults);
448
447
  // CLI single-source path must also re-sort when a context re-rank actually ran,
449
448
  // else reRankWithContext's score boost mutates scores but never reorders output
450
449
  // (audit #9). MCP branch unchanged. doReRank already implies a rerank happened.
package/mem-cli.mjs CHANGED
@@ -8,7 +8,7 @@ import { truncate, typeIcon, inferProject, scrubSecrets } from './utils.mjs';
8
8
  import { resolveProject } from './project-utils.mjs';
9
9
  import { TIER_CASE_SQL, tierSqlParams } from './tier.mjs';
10
10
  import { _resetVocabCache } from './tfidf.mjs';
11
- import { autoBoostIfNeeded, reRankWithContext, markSuperseded } from './search-scoring.mjs';
11
+ import { autoBoostIfNeeded, reRankWithContext } from './search-scoring.mjs';
12
12
  import { searchObservationsHybrid } from './search-engine.mjs';
13
13
  import { deepSearch, resolveDeepMode, shouldEscalateToDeep, autoDeepLlmReady } from './deep-search.mjs';
14
14
  import { ensureRegistryDb, upsertResource } from './registry.mjs';
@@ -178,7 +178,7 @@ async function cmdSearch(db, args, { llm } = {}) {
178
178
  {
179
179
  db, currentProject: project ? null : inferProject(), env: process.env,
180
180
  searchObservationsHybrid, deepSearch, shouldEscalateToDeep, autoDeepLlmReady,
181
- reRankWithContext, markSuperseded, llm,
181
+ reRankWithContext, llm,
182
182
  },
183
183
  {
184
184
  query, ftsQuery, effectiveSource, deepMode, rerank,
@@ -262,7 +262,6 @@ async function cmdSearch(db, args, { llm } = {}) {
262
262
  title: r.title || r.subtitle || null,
263
263
  lesson_learned: r.lesson_learned || null,
264
264
  importance: r.importance ?? null,
265
- superseded: Boolean(r.superseded),
266
265
  files_modified: r.files_modified || null,
267
266
  body_tokens: r.bodyTokens ?? null,
268
267
  };
@@ -300,8 +299,7 @@ async function cmdSearch(db, args, { llm } = {}) {
300
299
  } else {
301
300
  const date = fmtDateShort(r.created_at);
302
301
  const title = truncate(r.title || r.subtitle || '(untitled)', 80);
303
- const supersededTag = r.superseded ? ' [SUPERSEDED]' : '';
304
- out(`#${r.id} ${typeIcon(r.type)} ${date}${timeStr} ${title}${supersededTag}${tok(r)}`);
302
+ out(`#${r.id} ${typeIcon(r.type)} ${date}${timeStr} ${title}${tok(r)}`);
305
303
  if (r.lesson_learned) {
306
304
  out(` -> ${truncate(r.lesson_learned, 80)}`);
307
305
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.25.0",
3
+ "version": "3.27.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",
@@ -211,23 +211,25 @@ export function hasExplicitSignal(text, { errSig, files, intent } = {}) {
211
211
  return false;
212
212
  }
213
213
 
214
- // ─── Identifier-exact-match precision bypass (default OFF) ───────────────────
214
+ // ─── Identifier-exact-match precision bypass (default ON since v3.26.0) ──────
215
215
  //
216
- // 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
217
217
  // (OR_TOP_BM25_FLOOR / TOP_REL_FLOOR) drop the WHOLE FTS set when the top row's
218
218
  // magnitude is weak. But a rare code identifier (camelCase / snake_case / CONST_CASE
219
219
  // / kebab≥3) match has high *semantic* precision even at modest BM25 — a df=1 term
220
220
  // like `sanitizeFtsQuery` scores only ~23 raw yet is an unambiguous hit. So an obs
221
221
  // whose title/lesson EXACT-matches an identifier the prompt names is a precision pass
222
222
  // — the same independent-signal rationale that already exempts sigRows (error
223
- // signatures) and fileRows (file names) from these floors. When enabled, such rows are
224
- // restored after the floors run.
223
+ // signatures) and fileRows (file names) from these floors. Such rows are restored
224
+ // after the floors run.
225
225
  //
226
- // Default OFFopt-in. benchmark/ups-ab.mjs measured it TRADEOFF on the real corpus
227
- // (+3 recall recoveries / +4 injections over 12 positives / 8 hard-negatives); the
228
- // "cost" was eager surfacing of on-identifier obs on non-recall-framed prompts, not
229
- // off-topic noise. Kept opt-in (below the NET-POSITIVE/NEUTRAL ship bar). See #8858.
230
- 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';
231
233
  const TECH_IDENTIFIER_RE_G = new RegExp(TECH_IDENTIFIER_RE.source, 'g');
232
234
 
233
235
  // All tech-identifier tokens in `text`, lowercased + de-duped (for case-insensitive
@@ -615,8 +617,8 @@ async function main() {
615
617
  const signalPresent = hasExplicitSignal(promptText, {
616
618
  errSig, files: filesForGate, intent,
617
619
  });
618
- // Identifier tokens the prompt names (for the precision bypass below). Empty
619
- // 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.
620
622
  const promptIdentifiers = IDENTIFIER_BYPASS ? extractTechIdentifiers(promptText) : [];
621
623
 
622
624
  if (intent?.useRecent) {
@@ -649,7 +651,7 @@ async function main() {
649
651
  typeof r.relevance === 'number' && Math.abs(r.relevance) >= bm25Floor
650
652
  );
651
653
 
652
- // Identifier-exact-match precision bypass (default off — see IDENTIFIER_BYPASS).
654
+ // Identifier-exact-match precision bypass (default on — see IDENTIFIER_BYPASS).
653
655
  // Capture rows that exact-match a prompt identifier BEFORE the set-floors below;
654
656
  // they carry independent precision signal (sigRows/fileRows rationale) and are
655
657
  // restored after the floors so a low top-score can't drop a named-identifier hit.
@@ -127,53 +127,6 @@ export function reRankWithContext(db, results, project) {
127
127
  // Note: caller re-sorts the main results array after this — no sort needed here
128
128
  }
129
129
 
130
- /**
131
- * Mark older, lower-importance observations as superseded when multiple touch the same file.
132
- * Mutates result objects in-place by adding superseded=true flag.
133
- * @param {object[]} results Array of search result objects with source, files_modified,
134
- * created_at_epoch (or created_at / legacy date), importance
135
- */
136
- export function markSuperseded(results) {
137
- if (!results || results.length === 0) return;
138
- // Build map: file → [result objects], only for obs with files
139
- const fileMap = new Map();
140
- for (const r of results) {
141
- if (r.source !== 'obs' || !r.files_modified) continue;
142
- let files;
143
- try { files = JSON.parse(r.files_modified || '[]'); } catch (e) { debugCatch(e, 'markSuperseded-parse'); continue; }
144
- for (const f of files) {
145
- if (!fileMap.has(f)) fileMap.set(f, []);
146
- fileMap.get(f).push(r);
147
- }
148
- }
149
- // For each file with 2+ observations: mark older lower-importance as superseded (in-memory only)
150
- // Note: DB persistence removed — search is a read operation and should not write.
151
- // Persistent superseding belongs in mem_maintain/mem_compress write paths.
152
- for (const [, obsForFile] of fileMap) {
153
- if (obsForFile.length < 2) continue;
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.
162
- obsForFile.sort((a, b) => {
163
- const ka = a.created_at_epoch ?? a.created_at ?? a.date ?? '';
164
- const kb = b.created_at_epoch ?? b.created_at ?? b.date ?? '';
165
- if (typeof ka === 'number' && typeof kb === 'number') return kb - ka;
166
- return String(kb).localeCompare(String(ka));
167
- });
168
- const newest = obsForFile[0];
169
- for (let i = 1; i < obsForFile.length; i++) {
170
- if ((obsForFile[i].importance ?? 1) <= (newest.importance ?? 1)) {
171
- obsForFile[i].superseded = true;
172
- }
173
- }
174
- }
175
- }
176
-
177
130
  // ─── Pseudo-Relevance Feedback (PRF) ────────────────────────────────────────
178
131
  // Two-phase document-level expansion: extract discriminative terms from top
179
132
  // results' full text (title + narrative), filter out query terms + stop words,
package/server.mjs CHANGED
@@ -8,7 +8,7 @@ import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
8
8
  import { truncate, typeIcon, inferProject, scrubSecrets, fmtDate, debugLog, debugCatch, isPathConfined } from './utils.mjs';
9
9
  import { resolveProject as _resolveProjectShared } from './project-utils.mjs';
10
10
  import { ensureDb, DB_PATH, DB_DIR, REGISTRY_DB_PATH } from './schema.mjs';
11
- import { reRankWithContext, markSuperseded, autoBoostIfNeeded, runIdleCleanup, buildServerInstructions } from './search-scoring.mjs';
11
+ import { reRankWithContext, autoBoostIfNeeded, runIdleCleanup, buildServerInstructions } from './search-scoring.mjs';
12
12
  import { searchObservationsHybrid } from './search-engine.mjs';
13
13
  import { deepSearch, resolveDeepMode, shouldEscalateToDeep, autoDeepLlmReady } from './deep-search.mjs';
14
14
  import { selectCompressionCandidates, groupByProjectWeek, compressGroup } from './lib/compress-core.mjs';
@@ -231,8 +231,7 @@ function formatSearchOutput(paginatedResults, args, ftsQuery, totalCount, orFall
231
231
  const tok = r => (r.bodyTokens ? ` ~${r.bodyTokens}t` : '');
232
232
  for (const r of paginatedResults) {
233
233
  if (r.source === 'obs') {
234
- const supersededTag = r.superseded ? ' [SUPERSEDED]' : '';
235
- lines.push(`#${r.id} ${typeIcon(r.type)} [${r.type}] ${truncate(r.title || r.subtitle || '(untitled)')} | ${r.project} | ${fmtDate(r.date)}${supersededTag}${tok(r)}`);
234
+ lines.push(`#${r.id} ${typeIcon(r.type)} [${r.type}] ${truncate(r.title || r.subtitle || '(untitled)')} | ${r.project} | ${fmtDate(r.date)}${tok(r)}`);
236
235
  if (r.snippet && r.snippet.length > 10 && r.snippet !== r.title) {
237
236
  lines.push(` ${truncate(r.snippet, 100)}`);
238
237
  }
@@ -292,7 +291,7 @@ async function runSearchPipeline(db, args, { llm, rerankLlm } = {}) {
292
291
  {
293
292
  db, currentProject, env: process.env,
294
293
  searchObservationsHybrid, deepSearch, shouldEscalateToDeep, autoDeepLlmReady,
295
- reRankWithContext, markSuperseded, llm, rerankLlm,
294
+ reRankWithContext, llm, rerankLlm,
296
295
  },
297
296
  {
298
297
  query: args.query, ftsQuery, effectiveSource: effectiveType, deepMode, rerank,