claude-mem-lite 3.15.0 → 3.16.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/claudemd.mjs +12 -1
- package/format-utils.mjs +5 -0
- package/hook-context.mjs +10 -1
- package/hook-memory.mjs +20 -4
- package/hook-optimize.mjs +13 -1
- package/mem-cli.mjs +26 -7
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.16.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.
|
|
3
|
+
"version": "3.16.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/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')
|
|
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)
|
|
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 —
|
|
20
|
-
|
|
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 <=
|
|
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 <=
|
|
267
|
+
if (orQuery && (queryIsCjkDominant || queryTokenCount <= orFallbackMaxTokens)) {
|
|
252
268
|
try { crossRows = crossStmt.all(orQuery, project, cutoff); crossUsedOr = true; } catch {}
|
|
253
269
|
}
|
|
254
270
|
}
|
package/hook-optimize.mjs
CHANGED
|
@@ -203,10 +203,22 @@ search_aliases: 2-6 alternative search terms (include CJK if applicable).`;
|
|
|
203
203
|
const NORMALIZE_GATE_FILE = join(RUNTIME_DIR, 'last-normalize.json');
|
|
204
204
|
const NORMALIZE_INTERVAL_MS = 7 * 86400000; // 7 days
|
|
205
205
|
|
|
206
|
+
// Pure gate decision (no IO) — exported for testing. Fail-OPEN on a
|
|
207
|
+
// malformed-but-valid-JSON gate: a missing/non-numeric `epoch` makes
|
|
208
|
+
// `now - epoch` NaN, and `NaN >= INTERVAL` is false — which would PERMANENTLY
|
|
209
|
+
// block normalize with no recovery, contradicting the catch-branch's fail-open
|
|
210
|
+
// intent (a corrupt file that fails JSON.parse already returns true). A future
|
|
211
|
+
// epoch (clock skew / NTP correction) is equally suspect → run.
|
|
212
|
+
export function _normalizeGateOpen(last, now) {
|
|
213
|
+
const epoch = last?.epoch;
|
|
214
|
+
if (typeof epoch !== 'number' || !Number.isFinite(epoch) || epoch > now) return true;
|
|
215
|
+
return now - epoch >= NORMALIZE_INTERVAL_MS;
|
|
216
|
+
}
|
|
217
|
+
|
|
206
218
|
export function shouldRunNormalize() {
|
|
207
219
|
try {
|
|
208
220
|
const last = JSON.parse(readFileSync(NORMALIZE_GATE_FILE, 'utf8'));
|
|
209
|
-
return Date.now()
|
|
221
|
+
return _normalizeGateOpen(last, Date.now());
|
|
210
222
|
} catch {
|
|
211
223
|
return true;
|
|
212
224
|
}
|
package/mem-cli.mjs
CHANGED
|
@@ -1636,12 +1636,26 @@ function cmdRestore(db, argv) {
|
|
|
1636
1636
|
const trimmed = raw.trim();
|
|
1637
1637
|
if (!trimmed) { out('[mem] Empty file — nothing to restore.'); return; }
|
|
1638
1638
|
let rows;
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1639
|
+
let parseFailures = 0;
|
|
1640
|
+
if (trimmed[0] === '[') {
|
|
1641
|
+
// Whole-array JSON is a single document — partial parse is impossible, so
|
|
1642
|
+
// a syntax error rejects the file (unchanged behavior).
|
|
1643
|
+
try { rows = JSON.parse(trimmed); }
|
|
1644
|
+
catch (e) { fail(`[mem] "${file}" is not valid export JSON/JSONL: ${e.message}`); return; }
|
|
1645
|
+
} else {
|
|
1646
|
+
// JSONL: tolerate per-line syntax errors so one corrupt line in a large
|
|
1647
|
+
// backup doesn't discard every valid row. Parse failures fold into the
|
|
1648
|
+
// malformed tally below (the loop already skips valid-JSON-but-wrong-shape
|
|
1649
|
+
// rows the same way) — a backup tool must recover what it can.
|
|
1650
|
+
rows = [];
|
|
1651
|
+
for (const line of trimmed.split('\n')) {
|
|
1652
|
+
if (!line.trim()) continue;
|
|
1653
|
+
try { rows.push(JSON.parse(line)); }
|
|
1654
|
+
catch { parseFailures++; }
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
if (!Array.isArray(rows) || (rows.length === 0 && parseFailures === 0)) { out('[mem] No observations in file.'); return; }
|
|
1658
|
+
if (rows.length === 0) { fail(`[mem] "${file}": all ${parseFailures} line(s) failed to parse as JSONL.`); return; }
|
|
1645
1659
|
|
|
1646
1660
|
const projOverride = flags.project ? resolveProject(db, flags.project) : null;
|
|
1647
1661
|
const dryRun = flags['dry-run'] === true || flags['dry-run'] === 'true';
|
|
@@ -1693,7 +1707,12 @@ function cmdRestore(db, argv) {
|
|
|
1693
1707
|
if (process.env.CLAUDE_MEM_DEBUG) process.stderr.write(`[mem] restore row failed: ${e.message}\n`);
|
|
1694
1708
|
}
|
|
1695
1709
|
}
|
|
1696
|
-
|
|
1710
|
+
// Fold JSONL per-line syntax failures into the malformed tally and the
|
|
1711
|
+
// denominator so the report reflects every non-blank input line, not just the
|
|
1712
|
+
// ones that parsed.
|
|
1713
|
+
const totalMalformed = malformed + parseFailures;
|
|
1714
|
+
const totalLines = rows.length + parseFailures;
|
|
1715
|
+
out(`[mem] Restore${dryRun ? ' (dry-run)' : ''}: ${restored} restored, ${skipped} duplicate(s) skipped, ${totalMalformed} malformed/failed from ${totalLines} row(s).`);
|
|
1697
1716
|
}
|
|
1698
1717
|
|
|
1699
1718
|
// ─── Compress ────────────────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.16.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
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|