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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/cli.mjs +1 -1
- package/lib/import-jsonl.mjs +39 -4
- package/lib/stats-quality.mjs +10 -10
- package/mem-cli.mjs +6 -6
- package/package.json +1 -1
- package/schema.mjs +19 -1
- package/scripts/pre-tool-recall.js +6 -4
- package/server.mjs +6 -6
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];
|
package/lib/import-jsonl.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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') {
|
package/lib/stats-quality.mjs
CHANGED
|
@@ -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
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
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
const
|
|
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);
|