claude-mem-lite 3.26.0 → 3.28.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/lib/search-core.mjs +8 -5
- package/mem-cli.mjs +3 -5
- package/package.json +1 -1
- package/search-scoring.mjs +0 -47
- package/server.mjs +3 -4
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.28.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.
|
|
3
|
+
"version": "3.28.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/lib/search-core.mjs
CHANGED
|
@@ -290,7 +290,7 @@ export async function coreRunSearchPipeline(ctx, opts) {
|
|
|
290
290
|
const {
|
|
291
291
|
db, currentProject = null, env = process.env,
|
|
292
292
|
searchObservationsHybrid, deepSearch, shouldEscalateToDeep,
|
|
293
|
-
autoDeepLlmReady, reRankWithContext,
|
|
293
|
+
autoDeepLlmReady, reRankWithContext,
|
|
294
294
|
llm = null, rerankLlm = undefined,
|
|
295
295
|
} = ctx;
|
|
296
296
|
const {
|
|
@@ -437,14 +437,17 @@ export async function coreRunSearchPipeline(ctx, opts) {
|
|
|
437
437
|
else if (crossSourceEpochSortNoFts) results.sort((a, b) => (b.created_at_epoch ?? 0) - (a.created_at_epoch ?? 0));
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
-
// ── Context re-rank
|
|
440
|
+
// ── Context re-rank ──
|
|
441
441
|
const hasObs = results.some((r) => r.source === 'obs');
|
|
442
442
|
const rerankGate = rerankPolicy === 'mcp' ? ((ftsQuery || isDeep) && hasObs) : hasObs;
|
|
443
443
|
if (rerankGate) {
|
|
444
|
-
const obsResults = results.filter((r) => r.source === 'obs');
|
|
445
444
|
const doReRank = rerankPolicy === 'mcp' ? (ftsQuery && !deepReranked) : !deepReranked;
|
|
446
|
-
if (doReRank)
|
|
447
|
-
|
|
445
|
+
if (doReRank) {
|
|
446
|
+
// obsResults is only consumed by reRankWithContext — build it inside the gate so
|
|
447
|
+
// the no-rerank path (deepReranked / MCP isDeep+!ftsQuery) skips the wasted filter.
|
|
448
|
+
const obsResults = results.filter((r) => r.source === 'obs');
|
|
449
|
+
reRankWithContext(db, obsResults, rerankProject);
|
|
450
|
+
}
|
|
448
451
|
// CLI single-source path must also re-sort when a context re-rank actually ran,
|
|
449
452
|
// else reRankWithContext's score boost mutates scores but never reorders output
|
|
450
453
|
// (audit #9). MCP branch unchanged. doReRank already implies a rerank happened.
|
package/mem-cli.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { truncate, typeIcon, inferProject, scrubSecrets } from './utils.mjs';
|
|
|
8
8
|
import { resolveProject } from './project-utils.mjs';
|
|
9
9
|
import { TIER_CASE_SQL, tierSqlParams } from './tier.mjs';
|
|
10
10
|
import { _resetVocabCache } from './tfidf.mjs';
|
|
11
|
-
import { autoBoostIfNeeded, reRankWithContext
|
|
11
|
+
import { autoBoostIfNeeded, reRankWithContext } from './search-scoring.mjs';
|
|
12
12
|
import { searchObservationsHybrid } from './search-engine.mjs';
|
|
13
13
|
import { deepSearch, resolveDeepMode, shouldEscalateToDeep, autoDeepLlmReady } from './deep-search.mjs';
|
|
14
14
|
import { ensureRegistryDb, upsertResource } from './registry.mjs';
|
|
@@ -178,7 +178,7 @@ async function cmdSearch(db, args, { llm } = {}) {
|
|
|
178
178
|
{
|
|
179
179
|
db, currentProject: project ? null : inferProject(), env: process.env,
|
|
180
180
|
searchObservationsHybrid, deepSearch, shouldEscalateToDeep, autoDeepLlmReady,
|
|
181
|
-
reRankWithContext,
|
|
181
|
+
reRankWithContext, llm,
|
|
182
182
|
},
|
|
183
183
|
{
|
|
184
184
|
query, ftsQuery, effectiveSource, deepMode, rerank,
|
|
@@ -262,7 +262,6 @@ async function cmdSearch(db, args, { llm } = {}) {
|
|
|
262
262
|
title: r.title || r.subtitle || null,
|
|
263
263
|
lesson_learned: r.lesson_learned || null,
|
|
264
264
|
importance: r.importance ?? null,
|
|
265
|
-
superseded: Boolean(r.superseded),
|
|
266
265
|
files_modified: r.files_modified || null,
|
|
267
266
|
body_tokens: r.bodyTokens ?? null,
|
|
268
267
|
};
|
|
@@ -300,8 +299,7 @@ async function cmdSearch(db, args, { llm } = {}) {
|
|
|
300
299
|
} else {
|
|
301
300
|
const date = fmtDateShort(r.created_at);
|
|
302
301
|
const title = truncate(r.title || r.subtitle || '(untitled)', 80);
|
|
303
|
-
|
|
304
|
-
out(`#${r.id} ${typeIcon(r.type)} ${date}${timeStr} ${title}${supersededTag}${tok(r)}`);
|
|
302
|
+
out(`#${r.id} ${typeIcon(r.type)} ${date}${timeStr} ${title}${tok(r)}`);
|
|
305
303
|
if (r.lesson_learned) {
|
|
306
304
|
out(` -> ${truncate(r.lesson_learned, 80)}`);
|
|
307
305
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.28.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",
|
package/search-scoring.mjs
CHANGED
|
@@ -127,53 +127,6 @@ export function reRankWithContext(db, results, project) {
|
|
|
127
127
|
// Note: caller re-sorts the main results array after this — no sort needed here
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
/**
|
|
131
|
-
* Mark older, lower-importance observations as superseded when multiple touch the same file.
|
|
132
|
-
* Mutates result objects in-place by adding superseded=true flag.
|
|
133
|
-
* @param {object[]} results Array of search result objects with source, files_modified,
|
|
134
|
-
* created_at_epoch (or created_at / legacy date), importance
|
|
135
|
-
*/
|
|
136
|
-
export function markSuperseded(results) {
|
|
137
|
-
if (!results || results.length === 0) return;
|
|
138
|
-
// Build map: file → [result objects], only for obs with files
|
|
139
|
-
const fileMap = new Map();
|
|
140
|
-
for (const r of results) {
|
|
141
|
-
if (r.source !== 'obs' || !r.files_modified) continue;
|
|
142
|
-
let files;
|
|
143
|
-
try { files = JSON.parse(r.files_modified || '[]'); } catch (e) { debugCatch(e, 'markSuperseded-parse'); continue; }
|
|
144
|
-
for (const f of files) {
|
|
145
|
-
if (!fileMap.has(f)) fileMap.set(f, []);
|
|
146
|
-
fileMap.get(f).push(r);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
// For each file with 2+ observations: mark older lower-importance as superseded (in-memory only)
|
|
150
|
-
// Note: DB persistence removed — search is a read operation and should not write.
|
|
151
|
-
// Persistent superseding belongs in mem_maintain/mem_compress write paths.
|
|
152
|
-
for (const [, obsForFile] of fileMap) {
|
|
153
|
-
if (obsForFile.length < 2) continue;
|
|
154
|
-
// Sort newest-first by recency. Every production obs row carries `date`
|
|
155
|
-
// (= created_at ISO, set by ftsRowToResult); the FTS/type paths also carry
|
|
156
|
-
// created_at_epoch, the vector/type-list paths carry date-only. So the prior
|
|
157
|
-
// `b.date` sort already ordered correctly in production — this is defensive
|
|
158
|
-
// hardening, not a prod no-op fix (a post-release review corrected the original
|
|
159
|
-
// #8821 "missing-`date`" diagnosis). Prefer the numeric epoch, fall back to the
|
|
160
|
-
// ISO created_at (lexically chronological), then legacy `date`, so the key holds
|
|
161
|
-
// for any caller regardless of which recency fields it supplies.
|
|
162
|
-
obsForFile.sort((a, b) => {
|
|
163
|
-
const ka = a.created_at_epoch ?? a.created_at ?? a.date ?? '';
|
|
164
|
-
const kb = b.created_at_epoch ?? b.created_at ?? b.date ?? '';
|
|
165
|
-
if (typeof ka === 'number' && typeof kb === 'number') return kb - ka;
|
|
166
|
-
return String(kb).localeCompare(String(ka));
|
|
167
|
-
});
|
|
168
|
-
const newest = obsForFile[0];
|
|
169
|
-
for (let i = 1; i < obsForFile.length; i++) {
|
|
170
|
-
if ((obsForFile[i].importance ?? 1) <= (newest.importance ?? 1)) {
|
|
171
|
-
obsForFile[i].superseded = true;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
130
|
// ─── Pseudo-Relevance Feedback (PRF) ────────────────────────────────────────
|
|
178
131
|
// Two-phase document-level expansion: extract discriminative terms from top
|
|
179
132
|
// results' full text (title + narrative), filter out query terms + stop words,
|
package/server.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
|
8
8
|
import { truncate, typeIcon, inferProject, scrubSecrets, fmtDate, debugLog, debugCatch, isPathConfined } from './utils.mjs';
|
|
9
9
|
import { resolveProject as _resolveProjectShared } from './project-utils.mjs';
|
|
10
10
|
import { ensureDb, DB_PATH, DB_DIR, REGISTRY_DB_PATH } from './schema.mjs';
|
|
11
|
-
import { reRankWithContext,
|
|
11
|
+
import { reRankWithContext, autoBoostIfNeeded, runIdleCleanup, buildServerInstructions } from './search-scoring.mjs';
|
|
12
12
|
import { searchObservationsHybrid } from './search-engine.mjs';
|
|
13
13
|
import { deepSearch, resolveDeepMode, shouldEscalateToDeep, autoDeepLlmReady } from './deep-search.mjs';
|
|
14
14
|
import { selectCompressionCandidates, groupByProjectWeek, compressGroup } from './lib/compress-core.mjs';
|
|
@@ -231,8 +231,7 @@ function formatSearchOutput(paginatedResults, args, ftsQuery, totalCount, orFall
|
|
|
231
231
|
const tok = r => (r.bodyTokens ? ` ~${r.bodyTokens}t` : '');
|
|
232
232
|
for (const r of paginatedResults) {
|
|
233
233
|
if (r.source === 'obs') {
|
|
234
|
-
|
|
235
|
-
lines.push(`#${r.id} ${typeIcon(r.type)} [${r.type}] ${truncate(r.title || r.subtitle || '(untitled)')} | ${r.project} | ${fmtDate(r.date)}${supersededTag}${tok(r)}`);
|
|
234
|
+
lines.push(`#${r.id} ${typeIcon(r.type)} [${r.type}] ${truncate(r.title || r.subtitle || '(untitled)')} | ${r.project} | ${fmtDate(r.date)}${tok(r)}`);
|
|
236
235
|
if (r.snippet && r.snippet.length > 10 && r.snippet !== r.title) {
|
|
237
236
|
lines.push(` ${truncate(r.snippet, 100)}`);
|
|
238
237
|
}
|
|
@@ -292,7 +291,7 @@ async function runSearchPipeline(db, args, { llm, rerankLlm } = {}) {
|
|
|
292
291
|
{
|
|
293
292
|
db, currentProject, env: process.env,
|
|
294
293
|
searchObservationsHybrid, deepSearch, shouldEscalateToDeep, autoDeepLlmReady,
|
|
295
|
-
reRankWithContext,
|
|
294
|
+
reRankWithContext, llm, rerankLlm,
|
|
296
295
|
},
|
|
297
296
|
{
|
|
298
297
|
query: args.query, ftsQuery, effectiveSource: effectiveType, deepMode, rerank,
|