claude-mem-lite 2.71.0 → 2.71.2

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.71.0",
13
+ "version": "2.71.2",
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.71.0",
3
+ "version": "2.71.2",
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/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const CLI_COMMANDS = new Set(['search', 'recent', 'recall', 'get', 'timeline', 'save', 'stats', 'context', 'browse', 'delete', 'update', 'export', 'compress', 'maintain', 'optimize', 'fts-check', 'registry', 'import', 'enrich', 'activity', 'adopt', 'unadopt', 'memdir-audit', 'defer', 'help']);
2
+ const CLI_COMMANDS = new Set(['search', 'recent', 'recall', 'get', 'timeline', 'save', 'stats', 'context', 'browse', 'delete', 'update', 'export', 'compress', 'maintain', 'optimize', 'fts-check', 'registry', 'import', 'import-jsonl', 'enrich', 'activity', 'adopt', 'unadopt', 'memdir-audit', 'defer', 'help']);
3
3
  const INSTALL_COMMANDS = new Set(['install', 'uninstall', 'status', 'doctor', 'cleanup', 'cleanup-hooks', 'self-update', 'release']);
4
4
 
5
5
  const cmd = process.argv[2];
@@ -34,12 +34,21 @@ function parseLine(line) {
34
34
  try { return JSON.parse(line); } catch { return null; }
35
35
  }
36
36
 
37
+ // Distinct mem-internal id derived from the CC session UUID. The schema
38
+ // trigger `sdk_sessions_id_mix_check_*` aborts when memory_session_id ==
39
+ // content_session_id and both look like a CC UUID (length 36 + hyphen
40
+ // pattern); writing the raw UUID into both columns would reproduce the
41
+ // v2.33.1 mix bug. The `import-` prefix makes the origin obvious in audits.
42
+ function memId(sessionId) {
43
+ return `import-${sessionId}`;
44
+ }
45
+
37
46
  function ensureSession(db, sessionId, project, ts) {
38
47
  db.prepare(`
39
48
  INSERT OR IGNORE INTO sdk_sessions
40
49
  (content_session_id, memory_session_id, project, started_at, started_at_epoch, status)
41
50
  VALUES (?, ?, ?, ?, ?, 'completed')
42
- `).run(sessionId, sessionId, project, ts, Date.parse(ts) || Date.now());
51
+ `).run(sessionId, memId(sessionId), project, ts, Date.parse(ts) || Date.now());
43
52
  }
44
53
 
45
54
  function importPrompt(db, ev, project, seenPrompts) {
@@ -109,7 +118,7 @@ function importToolPair(db, toolUse, toolResult, project) {
109
118
  (memory_session_id, project, text, type, title, subtitle, narrative, concepts, facts, files_read, files_modified, importance, created_at, created_at_epoch)
110
119
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
111
120
  `).run(
112
- sessionId, project, safe.text, type, safe.title, safe.subtitle,
121
+ memId(sessionId), project, safe.text, type, safe.title, safe.subtitle,
113
122
  safe.narrative, safe.concepts, safe.facts,
114
123
  JSON.stringify(filesRead), JSON.stringify(filesModified),
115
124
  1, ts, Date.parse(ts) || Date.now(),
@@ -171,7 +180,9 @@ export async function importJsonl(db, path, { project }) {
171
180
  const toolName = useEv.name || 'unknown';
172
181
  const titlePreview = `${toolName}: ${(useEv.input?.command || useEv.input?.file_path || '').slice(0, 80)}`;
173
182
  const ts = useEv.timestamp || new Date().toISOString();
174
- const crossKey = dedupKey([sessionId, `existing:${titlePreview}:${ts}`]);
183
+ // Match the storage convention from importToolPair (memId-prefixed) so
184
+ // the seenObs entries seeded from the DB can be matched on a re-run.
185
+ const crossKey = dedupKey([memId(sessionId), `existing:${titlePreview}:${ts}`]);
175
186
  if (seenObs.has(crossKey)) return false;
176
187
 
177
188
  return importToolPair(db, useEv, resultEv, project);
@@ -183,7 +194,31 @@ export async function importJsonl(db, path, { project }) {
183
194
  const ev = parseLine(line);
184
195
  if (!ev) { skipped++; continue; }
185
196
  if (ev.type === 'user') {
186
- if (importPrompt(db, ev, project, seenPrompts)) prompts++; else skipped++;
197
+ // Real Claude Code transcripts wrap tool_result inside a user-typed
198
+ // event's message.content array (alongside the rare text part). The
199
+ // top-level {"type":"tool_result"} shape only appears in our test
200
+ // fixtures. Consume any embedded tool_result parts here; only fall
201
+ // through to importPrompt when the event is an actual user prompt.
202
+ const content = ev?.message?.content;
203
+ let consumedAsToolResult = false;
204
+ if (Array.isArray(content)) {
205
+ for (const part of content) {
206
+ if (part?.type === 'tool_result' && part.tool_use_id) {
207
+ consumedAsToolResult = true;
208
+ const useEv = pendingToolUse.get(part.tool_use_id);
209
+ if (useEv) {
210
+ const synth = { content: part.content, tool_use_id: part.tool_use_id, timestamp: ev.timestamp };
211
+ if (tryImportToolPair(useEv, synth)) observations++;
212
+ pendingToolUse.delete(part.tool_use_id);
213
+ } else {
214
+ skipped++;
215
+ }
216
+ }
217
+ }
218
+ }
219
+ if (!consumedAsToolResult) {
220
+ if (importPrompt(db, ev, project, seenPrompts)) prompts++; else skipped++;
221
+ }
187
222
  } else if (ev.type === 'assistant' && Array.isArray(ev.message?.content)) {
188
223
  for (const part of ev.message.content) {
189
224
  if (part.type === 'tool_use') {
@@ -30,10 +30,10 @@ export function computeQualityStats(db, { project, days }) {
30
30
  const windowRow = db.prepare(`
31
31
  SELECT
32
32
  COUNT(*) as total,
33
- SUM(CASE WHEN lesson_learned IS NOT NULL AND lesson_learned != '' THEN 1 ELSE 0 END) as with_lesson,
34
- SUM(CASE WHEN ${lowSignalIsMatchExpr} THEN 1 ELSE 0 END) as low_signal,
35
- SUM(CASE WHEN type = 'bugfix' THEN 1 ELSE 0 END) as bugfix_total,
36
- SUM(CASE WHEN type = 'bugfix' AND ${unresolvedNarrativeExpr} THEN 1 ELSE 0 END) as bugfix_unresolved
33
+ COALESCE(SUM(CASE WHEN lesson_learned IS NOT NULL AND lesson_learned != '' THEN 1 ELSE 0 END), 0) as with_lesson,
34
+ COALESCE(SUM(CASE WHEN ${lowSignalIsMatchExpr} THEN 1 ELSE 0 END), 0) as low_signal,
35
+ COALESCE(SUM(CASE WHEN type = 'bugfix' THEN 1 ELSE 0 END), 0) as bugfix_total,
36
+ COALESCE(SUM(CASE WHEN type = 'bugfix' AND ${unresolvedNarrativeExpr} THEN 1 ELSE 0 END), 0) as bugfix_unresolved
37
37
  FROM observations
38
38
  WHERE created_at_epoch >= ? ${projectFilter}
39
39
  `).get(cutoff, ...baseParams);
@@ -41,8 +41,8 @@ export function computeQualityStats(db, { project, days }) {
41
41
  const allTimeRow = db.prepare(`
42
42
  SELECT
43
43
  COUNT(*) as total,
44
- SUM(CASE WHEN lesson_learned IS NOT NULL AND lesson_learned != '' THEN 1 ELSE 0 END) as with_lesson,
45
- SUM(CASE WHEN ${lowSignalIsMatchExpr} THEN 1 ELSE 0 END) as low_signal
44
+ COALESCE(SUM(CASE WHEN lesson_learned IS NOT NULL AND lesson_learned != '' THEN 1 ELSE 0 END), 0) as with_lesson,
45
+ COALESCE(SUM(CASE WHEN ${lowSignalIsMatchExpr} THEN 1 ELSE 0 END), 0) as low_signal
46
46
  FROM observations
47
47
  WHERE 1=1 ${projectFilter}
48
48
  `).get(...baseParams);
@@ -51,8 +51,8 @@ export function computeQualityStats(db, { project, days }) {
51
51
  SELECT
52
52
  type,
53
53
  COUNT(*) as total,
54
- SUM(CASE WHEN COALESCE(access_count, 0) > 0 THEN 1 ELSE 0 END) as accessed,
55
- SUM(CASE WHEN lesson_learned IS NOT NULL AND lesson_learned != '' THEN 1 ELSE 0 END) as with_lesson
54
+ COALESCE(SUM(CASE WHEN COALESCE(access_count, 0) > 0 THEN 1 ELSE 0 END), 0) as accessed,
55
+ COALESCE(SUM(CASE WHEN lesson_learned IS NOT NULL AND lesson_learned != '' THEN 1 ELSE 0 END), 0) as with_lesson
56
56
  FROM observations
57
57
  WHERE created_at_epoch >= ? ${projectFilter}
58
58
  GROUP BY type
@@ -75,8 +75,8 @@ export function computeQualityStats(db, { project, days }) {
75
75
  // age > 37d, so a sudden write surge inflates this until the cohort ages out.
76
76
  const purgeRow = db.prepare(`
77
77
  SELECT
78
- SUM(CASE WHEN compressed_into IS NOT NULL AND compressed_into != 0 THEN 1 ELSE 0 END) as compressed,
79
- SUM(CASE WHEN compressed_into = ${COMPRESSED_PENDING_PURGE} THEN 1 ELSE 0 END) as pending_purge
78
+ COALESCE(SUM(CASE WHEN compressed_into IS NOT NULL AND compressed_into != 0 THEN 1 ELSE 0 END), 0) as compressed,
79
+ COALESCE(SUM(CASE WHEN compressed_into = ${COMPRESSED_PENDING_PURGE} THEN 1 ELSE 0 END), 0) as pending_purge
80
80
  FROM observations
81
81
  WHERE 1=1 ${projectFilter}
82
82
  `).get(...baseParams);
package/mem-cli.mjs CHANGED
@@ -1832,12 +1832,12 @@ function cmdMaintain(db, args) {
1832
1832
  const stats = db.prepare(`
1833
1833
  SELECT
1834
1834
  COUNT(*) as total,
1835
- SUM(CASE WHEN COALESCE(importance, 1) = 1 AND COALESCE(access_count, 0) = 0
1836
- AND created_at_epoch < ? THEN 1 ELSE 0 END) as stale,
1837
- SUM(CASE WHEN (title IS NULL OR title = '') AND (narrative IS NULL OR narrative = '')
1838
- THEN 1 ELSE 0 END) as broken,
1839
- SUM(CASE WHEN COALESCE(access_count, 0) > 3 AND COALESCE(importance, 1) < 3
1840
- THEN 1 ELSE 0 END) as boostable
1835
+ COALESCE(SUM(CASE WHEN COALESCE(importance, 1) = 1 AND COALESCE(access_count, 0) = 0
1836
+ AND created_at_epoch < ? THEN 1 ELSE 0 END), 0) as stale,
1837
+ COALESCE(SUM(CASE WHEN (title IS NULL OR title = '') AND (narrative IS NULL OR narrative = '')
1838
+ THEN 1 ELSE 0 END), 0) as broken,
1839
+ COALESCE(SUM(CASE WHEN COALESCE(access_count, 0) > 3 AND COALESCE(importance, 1) < 3
1840
+ THEN 1 ELSE 0 END), 0) as boostable
1841
1841
  FROM observations
1842
1842
  WHERE COALESCE(compressed_into, 0) = 0 ${projectFilter}
1843
1843
  `).get(staleAge, ...baseParams);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.71.0",
3
+ "version": "2.71.2",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",
package/schema.mjs CHANGED
@@ -181,6 +181,21 @@ export function initSchema(db) {
181
181
  if (e.message?.startsWith('DB schema is v')) throw e;
182
182
  }
183
183
 
184
+ // Concurrent-init guard: serialize schema setup against peer processes via
185
+ // BEGIN IMMEDIATE (busy_timeout=3000 from ensureDb makes peers wait). Required
186
+ // because the sdk_sessions_id_mix_check_{ai,au} migration uses DROP+CREATE
187
+ // without IF NOT EXISTS to update the trigger body, which races at cold-start.
188
+ // Re-check schema_version under the lock — a peer may have completed init
189
+ // while we were blocked. Connection close auto-rollbacks if body throws.
190
+ db.exec('BEGIN IMMEDIATE');
191
+ try {
192
+ const underlock = db.prepare('SELECT version FROM schema_version LIMIT 1').get();
193
+ if (underlock && underlock.version === CURRENT_SCHEMA_VERSION) {
194
+ db.exec('COMMIT');
195
+ return db;
196
+ }
197
+ } catch { /* table absent — proceed */ }
198
+
184
199
  // Create core tables
185
200
  db.exec(CORE_SCHEMA);
186
201
 
@@ -264,7 +279,6 @@ export function initSchema(db) {
264
279
  });
265
280
  dedupAndIndex();
266
281
  }
267
- db.pragma('foreign_keys = ON');
268
282
 
269
283
  // Performance indexes
270
284
  db.exec(`CREATE INDEX IF NOT EXISTS idx_obs_epoch_project ON observations(created_at_epoch DESC, project)`);
@@ -580,6 +594,10 @@ export function initSchema(db) {
580
594
  db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(CURRENT_SCHEMA_VERSION);
581
595
  })();
582
596
 
597
+ db.exec('COMMIT');
598
+ // PRAGMA foreign_keys must run OUTSIDE the transaction (no-op inside).
599
+ db.pragma('foreign_keys = ON');
600
+
583
601
  return db;
584
602
  }
585
603
 
@@ -9,10 +9,12 @@ import { basename, join } from 'path';
9
9
  import { homedir } from 'os';
10
10
  import { buildNotLowSignalSql } from '../lib/low-signal-patterns.mjs';
11
11
 
12
- // CLAUDE_MEM_DB_PATH / CLAUDE_MEM_RUNTIME_DIR env overrides allow tests and debug tools to
13
- // point the hook at an isolated DB + cooldown dir without touching the user's real state.
14
- const DB_PATH = process.env.CLAUDE_MEM_DB_PATH || join(homedir(), '.claude-mem-lite', 'claude-mem-lite.db');
15
- const RUNTIME_DIR = process.env.CLAUDE_MEM_RUNTIME_DIR || join(homedir(), '.claude-mem-lite', 'runtime');
12
+ // CLAUDE_MEM_DIR matches schema.mjs / main CLI one env var sandboxes the
13
+ // whole system. CLAUDE_MEM_DB_PATH / CLAUDE_MEM_RUNTIME_DIR remain as
14
+ // per-component overrides for tests that mix isolated + real paths.
15
+ const DATA_DIR = process.env.CLAUDE_MEM_DIR || join(homedir(), '.claude-mem-lite');
16
+ const DB_PATH = process.env.CLAUDE_MEM_DB_PATH || join(DATA_DIR, 'claude-mem-lite.db');
17
+ const RUNTIME_DIR = process.env.CLAUDE_MEM_RUNTIME_DIR || join(DATA_DIR, 'runtime');
16
18
  // v2.33.1: cooldown path is session-scoped so same-file-twice within one
17
19
  // session never re-injects (was: global file, 5-min window). Cross-session:
18
20
  // fresh file, fresh nudges — this is intended. No session_id → fall back to
package/server.mjs CHANGED
@@ -1329,12 +1329,12 @@ server.registerTool(
1329
1329
  const stats = db.prepare(`
1330
1330
  SELECT
1331
1331
  COUNT(*) as total,
1332
- SUM(CASE WHEN COALESCE(importance, 1) = 1 AND COALESCE(access_count, 0) = 0
1333
- AND created_at_epoch < ? THEN 1 ELSE 0 END) as stale,
1334
- SUM(CASE WHEN (title IS NULL OR title = '') AND (narrative IS NULL OR narrative = '')
1335
- THEN 1 ELSE 0 END) as broken,
1336
- SUM(CASE WHEN COALESCE(access_count, 0) > 3 AND COALESCE(importance, 1) < 3
1337
- THEN 1 ELSE 0 END) as boostable
1332
+ COALESCE(SUM(CASE WHEN COALESCE(importance, 1) = 1 AND COALESCE(access_count, 0) = 0
1333
+ AND created_at_epoch < ? THEN 1 ELSE 0 END), 0) as stale,
1334
+ COALESCE(SUM(CASE WHEN (title IS NULL OR title = '') AND (narrative IS NULL OR narrative = '')
1335
+ THEN 1 ELSE 0 END), 0) as broken,
1336
+ COALESCE(SUM(CASE WHEN COALESCE(access_count, 0) > 3 AND COALESCE(importance, 1) < 3
1337
+ THEN 1 ELSE 0 END), 0) as boostable
1338
1338
  FROM observations
1339
1339
  WHERE COALESCE(compressed_into, 0) = 0 ${projectFilter}
1340
1340
  `).get(staleAge, ...baseParams);