claude-mem-lite 3.15.0 → 3.16.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.15.0",
13
+ "version": "3.16.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.15.0",
3
+ "version": "3.16.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/claudemd.mjs CHANGED
@@ -200,7 +200,18 @@ export function removeManaged(cwd, slug) {
200
200
  if (blockAtStart) raw = raw.replace(/^\s+/, '');
201
201
  action = 'removed';
202
202
  }
203
- if (action === 'removed') atomicWrite(p, raw);
203
+ if (action === 'removed') {
204
+ // When the managed block was the ENTIRE file (adopt created CLAUDE.md
205
+ // because none existed), removing it leaves nothing but whitespace.
206
+ // Delete the now-empty file rather than writing a 0-byte CLAUDE.md, so
207
+ // unadopt fully restores the pre-adopt state — mirrors the emptied-.claude/
208
+ // cleanup below ("unadopt leaves no trace").
209
+ if (raw.trim() === '') {
210
+ try { unlinkSync(p); } catch { atomicWrite(p, raw); }
211
+ } else {
212
+ atomicWrite(p, raw);
213
+ }
214
+ }
204
215
  }
205
216
  const dp = detailDocPath(cwd, slug);
206
217
  if (existsSync(dp)) try { unlinkSync(dp); } catch { /* best-effort */ }
package/format-utils.mjs CHANGED
@@ -61,6 +61,11 @@ export function fmtDate(iso) {
61
61
  export function fmtTime(iso) {
62
62
  if (!iso) return '';
63
63
  const d = new Date(iso);
64
+ // Guard against an unparseable timestamp (e.g. corrupt/imported created_at):
65
+ // a bare new Date('garbage') yields Invalid Date → getUTCHours() is NaN →
66
+ // "NaN:NaN" leaking into the SessionStart Recent table. Degrade to '' like the
67
+ // falsy-input case above.
68
+ if (Number.isNaN(d.getTime())) return '';
64
69
  return `${String(d.getUTCHours()).padStart(2, '0')}:${String(d.getUTCMinutes()).padStart(2, '0')}`;
65
70
  }
66
71
 
package/hook-context.mjs CHANGED
@@ -27,6 +27,15 @@ function inferProjectDir() {
27
27
  * @param {string} project Project name to check velocity for
28
28
  * @returns {{tier1: number, tier2: number, tier3: number, sessWindow: number}} Time window durations in ms
29
29
  */
30
+ // Sanitize a string for a GitHub-flavored-markdown table cell: a literal `|`
31
+ // in a title (e.g. "grep | sort | uniq") would otherwise open phantom columns
32
+ // and corrupt the <claude-mem-context> Recent table the model+user see every
33
+ // SessionStart. Escape pipes and collapse any CR/LF/tab to a space so one obs
34
+ // stays one row/cell.
35
+ function mdCell(s) {
36
+ return String(s ?? '').replace(/[\r\n\t]+/g, ' ').replace(/\|/g, '\\|');
37
+ }
38
+
30
39
  export function computeAdaptiveWindows(db, project) {
31
40
  const sevenDaysAgo = Date.now() - 7 * 86400000;
32
41
  const row = db.prepare(`
@@ -446,7 +455,7 @@ export function buildSessionContextLines(db, project, now = new Date(), currentC
446
455
  obsLines.push('|----|------|---|-------|');
447
456
  for (const o of obsToShow) {
448
457
  const proj = o.project && o.project !== project ? ` (${o.project})` : '';
449
- obsLines.push(`| #${o.id} | ${fmtTime(o.created_at)} | ${typeIcon(o.type)} | ${truncate(o.title || '(untitled)', 60)}${proj} |`);
458
+ obsLines.push(`| #${o.id} | ${fmtTime(o.created_at)} | ${typeIcon(o.type)} | ${mdCell(truncate(o.title || '(untitled)', 60) + proj)} |`);
450
459
  }
451
460
  }
452
461
 
package/hook-memory.mjs CHANGED
@@ -16,8 +16,23 @@ const MEMORY_TYPE_BOOST = { decision: 1.5, discovery: 1.3, bugfix: 1.1, feature:
16
16
  // Adaptive BM25 thresholds — scale with corpus size to filter noise.
17
17
  // Larger corpora produce more weak matches from common words.
18
18
  const BM25_THRESHOLD = { TINY: 0, SMALL: 1.5, MEDIUM: 2.5, LARGE: 3.5 };
19
- // OR fallback max token count — queries with 3+ tokens that fail AND are likely off-topic
20
- const OR_FALLBACK_MAX_TOKENS = 2;
19
+ // OR fallback max token count — when an AND query returns nothing, retry as OR
20
+ // only if the query has at most this many significant terms. Natural-language
21
+ // user prompts ("how did we fix the parser null deref bug?") almost always
22
+ // exceed this after synonym expansion, so the old hard 2 silently denied them
23
+ // the OR rescue and the UPS injection path returned 0 results for real prompts
24
+ // (semantic-keyed recall measured at 26%, #8255; 0/11 on a realistic NL corpus).
25
+ // Precision for OR results is already enforced downstream by three independent
26
+ // gates — the 0.4x OR penalty, the adaptive BM25 threshold, and the 40%
27
+ // term-coverage filter (v27) — so the token gate is a redundant, overly-strict
28
+ // fourth guard predating term-coverage. Default raised to 8 (covers typical NL
29
+ // prompts); env override `MEM_OR_FALLBACK_MAX_TOKENS` ∈ [0, 50], invalid → 8.
30
+ function getOrFallbackMaxTokens() {
31
+ const raw = process.env.MEM_OR_FALLBACK_MAX_TOKENS;
32
+ if (raw === undefined || raw === '') return 8;
33
+ const n = parseInt(raw, 10);
34
+ return Number.isInteger(n) && n >= 0 && n <= 50 ? n : 8;
35
+ }
21
36
 
22
37
  // v27: term-coverage post-filter. Drops high-BM25 candidates whose visible
23
38
  // fields (title + lesson_learned) cover <N% of the query's significant terms.
@@ -215,9 +230,10 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [])
215
230
  // unconditionally; mirror that here. Noise is contained downstream (0.4x OR
216
231
  // penalty + BM25 threshold + term-coverage filter). queryIsCjkDominant (not
217
232
  // mere CJK presence) is the gate — see its definition at the function top.
233
+ const orFallbackMaxTokens = getOrFallbackMaxTokens();
218
234
  if (rows.length === 0) {
219
235
  const orQuery = relaxFtsQueryToOr(ftsQuery);
220
- if (orQuery && (queryIsCjkDominant || queryTokenCount <= OR_FALLBACK_MAX_TOKENS)) {
236
+ if (orQuery && (queryIsCjkDominant || queryTokenCount <= orFallbackMaxTokens)) {
221
237
  try { rows = selectStmt.all(orQuery, project, cutoff); usedOrFallback = true; } catch {}
222
238
  }
223
239
  }
@@ -248,7 +264,7 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [])
248
264
  crossRows = crossStmt.all(ftsQuery, project, cutoff);
249
265
  if (crossRows.length === 0) {
250
266
  const orQuery = relaxFtsQueryToOr(ftsQuery);
251
- if (orQuery && (queryIsCjkDominant || queryTokenCount <= OR_FALLBACK_MAX_TOKENS)) {
267
+ if (orQuery && (queryIsCjkDominant || queryTokenCount <= orFallbackMaxTokens)) {
252
268
  try { crossRows = crossStmt.all(orQuery, project, cutoff); crossUsedOr = true; } catch {}
253
269
  }
254
270
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.15.0",
3
+ "version": "3.16.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",