claude-mem-lite 3.6.0 → 3.7.1

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.6.0",
13
+ "version": "3.7.1",
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.6.0",
3
+ "version": "3.7.1",
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/README.md CHANGED
@@ -144,7 +144,7 @@ How claude-mem-lite differs from the major neighbors in the LLM-memory space (ve
144
144
 
145
145
  ## Requirements
146
146
 
147
- - **Node.js** >= 18
147
+ - **Node.js** >= 20
148
148
  - **Claude Code** CLI installed and configured (`claude` command available)
149
149
  - **SQLite3** support (provided by `better-sqlite3`, compiled on install)
150
150
  - **Platform**: Linux or macOS (see [Platform Support](#platform-support))
@@ -582,7 +582,7 @@ claude-mem-lite/
582
582
  commands/
583
583
  mem.md # /mem command definition
584
584
  server.mjs # MCP server: tool definitions, FTS5 search, database init
585
- server-internals.mjs # Extracted search helpers: re-ranking, PRF, concept expansion
585
+ search-scoring.mjs # Extracted search helpers: re-ranking, PRF, concept expansion
586
586
  hook.mjs # Claude Code hooks: episode capture, error recall, session management
587
587
  hook-llm.mjs # Background LLM workers: episode extraction, session summaries
588
588
  hook-shared.mjs # Shared hook infrastructure: session management, DB access, LLM calls
@@ -632,17 +632,25 @@ claude-mem-lite/
632
632
 
633
633
  ## Search Quality
634
634
 
635
- Benchmarked on 200 observations across 30 queries (standard + hard-negative categories):
636
-
637
- | Metric | Score |
638
- |--------|-------|
639
- | Recall@10 | 0.88 |
640
- | Precision@10 | 0.96 |
641
- | nDCG@10 | 0.95 |
642
- | MRR@10 | 0.95 |
643
- | P95 search latency | 0.15ms |
644
-
645
- The benchmark suite runs as a CI gate (`npm run benchmark:gate`) to prevent search quality regressions.
635
+ Benchmarked on 200 observations across 30 queries (standard + hard-negative categories),
636
+ measuring the **production-hybrid** retriever (FTS5 BM25 + TF-IDF vector + RRF) — the path
637
+ `mem_search` / `recall` actually use. The CI gate (`npm run benchmark:gate`) runs this same
638
+ path and fails on regression.
639
+
640
+ | Metric | Score (production-hybrid) |
641
+ |--------|---------------------------|
642
+ | Recall@10 | 0.90 |
643
+ | Precision@10 | 0.79 |
644
+ | nDCG@10 | 0.97 |
645
+ | MRR@10 | 0.97 |
646
+ | P95 search latency | ~3ms |
647
+
648
+ > **Note on the path measured.** Earlier versions of this table reported the *lexical*
649
+ > FTS-only path (Precision@10 0.96, P95 0.15ms). The hybrid vector arm trades raw
650
+ > precision@10 for higher recall / nDCG / MRR by surfacing semantically-related candidates
651
+ > beyond exact lexical matches; the gate now measures the hybrid path so these numbers
652
+ > reflect real `mem_search` behavior. For field-comparable recall, see the LongMemEval
653
+ > section below.
646
654
 
647
655
  ### Recall on LongMemEval (standard benchmark)
648
656
 
package/README.zh-CN.md CHANGED
@@ -524,7 +524,7 @@ claude-mem-lite/
524
524
  commands/
525
525
  mem.md # /mem 命令定义
526
526
  server.mjs # MCP 服务器:工具定义、FTS5 搜索、数据库初始化
527
- server-internals.mjs # 搜索辅助模块:重排序、PRF、概念扩展
527
+ search-scoring.mjs # 搜索辅助模块:重排序、PRF、概念扩展
528
528
  hook.mjs # Claude Code 钩子:episode 捕获、错误回忆、会话管理
529
529
  hook-llm.mjs # 后台 LLM worker:episode 提取、会话摘要
530
530
  hook-shared.mjs # 共享钩子基础设施:会话管理、数据库访问、LLM 调用
package/deep-search.mjs CHANGED
@@ -103,7 +103,9 @@ export function autoDeepLlmReady(env = process.env, injectedLlm) {
103
103
  * an LLM, so the decision itself is free — only a positive verdict costs a
104
104
  * Haiku call (the escalation).
105
105
  *
106
- * Weak when: too few results (count below minResults floor).
106
+ * Weak when: too few results (count below minResults floor) AND the corpus is
107
+ * large enough that deep search could plausibly find more (see corpus guard
108
+ * below).
107
109
  *
108
110
  * NOTE: ctx.orFallbackFired was intentionally removed as an escalation trigger.
109
111
  * orFallbackFired fires on SUCCESSFUL AND→OR recovery — when the fallback
@@ -114,17 +116,37 @@ export function autoDeepLlmReady(env = process.env, injectedLlm) {
114
116
  * fails, OR also fails) is still caught: if OR recovers nothing, count is 0-2
115
117
  * → escalates on count alone.
116
118
  *
119
+ * Corpus guard (folded in): the count-based trigger above is correct for a real
120
+ * corpus, but on a near-empty / brand-new / benchmark project EVERY 0-hit query
121
+ * looks "weak", so a caller that only checks the count would auto-escalate (and
122
+ * fire a Haiku rewrite) on a store HyDE/multi-query can't possibly rescue — the
123
+ * "[mem] auto-escalated … 0 hits" spam. hasEscalatableCorpus used to be a
124
+ * SEPARATE function each caller had to remember to AND in; folding it in here
125
+ * means passing `db` self-suppresses escalation when the corpus is too small,
126
+ * without changing the (correct) count trigger for real corpora. Backward-
127
+ * compatible: callers that omit `db` keep the pure count behaviour (and may
128
+ * still AND hasEscalatableCorpus themselves — double-gating with the same
129
+ * predicate is idempotent, never a regression).
130
+ *
117
131
  * @param {Array} results normal-search rows
118
132
  * @param {object} ctx the hybrid ctx the engine mutated (unused; kept for
119
133
  * backward-compat with callers that pass it)
120
134
  * @param {object} [opts]
121
135
  * @param {number} [opts.minResults=AUTO_DEEP_MIN_RESULTS]
136
+ * @param {Database} [opts.db] open handle — when given, the corpus-size guard is
137
+ * evaluated here so escalation is suppressed on a
138
+ * too-small store. Omit to keep pure count behaviour.
139
+ * @param {string} [opts.project] project scope for the corpus count (when db given)
140
+ * @param {number} [opts.minCorpus=AUTO_DEEP_MIN_CORPUS] corpus-size floor (when db given)
122
141
  * @returns {boolean}
123
142
  */
124
- export function shouldEscalateToDeep(results, _ctx, { minResults = AUTO_DEEP_MIN_RESULTS } = {}) {
143
+ export function shouldEscalateToDeep(results, _ctx, { minResults = AUTO_DEEP_MIN_RESULTS, db, project = null, minCorpus = AUTO_DEEP_MIN_CORPUS } = {}) {
125
144
  const n = Array.isArray(results) ? results.length : 0;
126
- if (n < minResults) return true;
127
- return false;
145
+ if (n >= minResults) return false;
146
+ // Count is weak. If a db was supplied, also require an escalatable corpus —
147
+ // this is the fold-in that stops 0-hit escalation on a near-empty store.
148
+ if (db && !hasEscalatableCorpus(db, project, minCorpus)) return false;
149
+ return true;
128
150
  }
129
151
 
130
152
  /**
package/hook-update.mjs CHANGED
@@ -13,6 +13,8 @@ import { debugCatch, debugLog } from './utils.mjs';
13
13
  // extracted tarball's own source-files.mjs inside installExtractedRelease.
14
14
  // See loadReleaseManifest below.
15
15
  import { SOURCE_FILES as LOCAL_SOURCE_FILES, HOOK_SCRIPT_FILES as LOCAL_HOOK_SCRIPT_FILES } from './source-files.mjs';
16
+ import { acquireLock } from './lib/proc-lock.mjs';
17
+ import { atomicWriteFileSync } from './lib/atomic-write.mjs';
16
18
 
17
19
  // ── Configuration ──────────────────────────────────────────
18
20
  const GITHUB_REPO = 'sdsrss/claude-mem-lite';
@@ -379,6 +381,16 @@ export function validateExtractedTarball(sourceDir, expectedVersion, expectedNam
379
381
  // the target's node_modules untouched. Dependency bumps still flow through the
380
382
  // GitHub-tarball path (downloadAndInstall), which keeps skipNpmInstall=false.
381
383
  export async function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR, opts = {}) {
384
+ // Cross-process lock: concurrent SessionStart self-heals / auto-updates must
385
+ // not interleave the rename loop below (→ mixed-version install). A live peer
386
+ // holding the lock means an install is already in flight — skip rather than
387
+ // race. Shared path with install.mjs so direct install + repair + auto-update
388
+ // are mutually exclusive.
389
+ const release = acquireLock(join(STATE_DIR, 'runtime', 'install.lock'));
390
+ if (!release) {
391
+ debugLog('DEBUG', 'hook-update', 'installExtractedRelease: another install/update is in progress — skipping');
392
+ return false;
393
+ }
382
394
  const ts = `${Date.now()}-${process.pid}`;
383
395
  const stagingDir = join(targetDir, `.update-staging-${ts}`);
384
396
  const backupDir = join(targetDir, `.update-backup-${ts}`);
@@ -439,7 +451,9 @@ export async function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR
439
451
  debugLog('DEBUG', 'hook-update', `Post-update: removed stale global MCP "${k}"`);
440
452
  }
441
453
  }
442
- if (changed) writeFileSync(claudeJsonPath, JSON.stringify(cfg, null, 2) + '\n');
454
+ // Atomic + one-time backup: ~/.claude.json is the user's ENTIRE Claude
455
+ // Code config; a torn write here breaks them outside our control.
456
+ if (changed) atomicWriteFileSync(claudeJsonPath, JSON.stringify(cfg, null, 2) + '\n', { backup: true });
443
457
  }
444
458
  } catch (e) { debugCatch(e, 'post-update-mcp-dedup'); }
445
459
 
@@ -477,6 +491,8 @@ export async function installExtractedRelease(sourceDir, targetDir = INSTALL_DIR
477
491
  try { rmSync(stagingDir, { recursive: true, force: true }); } catch {}
478
492
  try { rmSync(backupDir, { recursive: true, force: true }); } catch {}
479
493
  return false;
494
+ } finally {
495
+ release();
480
496
  }
481
497
  }
482
498