claude-mem-lite 5.5.0 → 5.5.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/deep-search.mjs +11 -5
- package/hook-optimize.mjs +61 -11
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "5.5.
|
|
13
|
+
"version": "5.5.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": "5.5.
|
|
3
|
+
"version": "5.5.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/deep-search.mjs
CHANGED
|
@@ -191,12 +191,18 @@ export function resolveDeepMode(explicitDeep, { surface, env = process.env } = {
|
|
|
191
191
|
* D#3. benchmark/deep-search-holdout.mjs asks the suite's own queries of a corpus with
|
|
192
192
|
* their relevant_ids deleted, so the correct answer is zero rows and every returned row is
|
|
193
193
|
* a false positive by construction. It reads mean FP@10 = 10.00 across 12/12 queries: deep
|
|
194
|
-
* fills every slot, every time.
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
194
|
+
* fills every slot, every time. THE FLOOD IS NOT THE PARAPHRASE UNION, and an earlier
|
|
195
|
+
* version of this paragraph said it was ("the single-query baseline returns 1-2 rows on the
|
|
196
|
+
* same negatives"). Measured 2026-09-07, same fixture: the single-variant baseline already
|
|
197
|
+
* returns mean 9.42 of 10 (min 5, max 10, n=12), so fusion adds about half a slot to a page
|
|
198
|
+
* that was already full. A counterfactual names the real source — disabling the AND->OR
|
|
199
|
+
* fallback in search-engine.mjs takes mean FP@10 from 10.00 to 0.08, with 0/12 queries
|
|
200
|
+
* flooded instead of 12/12. Read that as a MECHANISM PROBE, not a candidate fix: the same
|
|
201
|
+
* fallback IS the vocab-mismatch recall win, and suppressing it on rewrites takes deep R@10
|
|
202
|
+
* from 0.7383 to 0.3962. Three gates were tested against both arms and rejected. rrfFuseN
|
|
199
203
|
* fuses by RANK, so no magnitude signal survives the merge for a downstream floor to read.
|
|
204
|
+
* The same counterfactual explains why auto-escalation never fires here (D#8): with the
|
|
205
|
+
* fallback off, plain hits drop to min 0 and the escalation reach goes 0/12 -> 12/12.
|
|
200
206
|
*
|
|
201
207
|
* The discrimination is not available at this layer, so the honest move is to hand the
|
|
202
208
|
* caller what the caller cannot otherwise see. Two things were missing:
|
package/hook-optimize.mjs
CHANGED
|
@@ -129,7 +129,8 @@ export function findReenrichCandidates(db, limit = 10, { scope = 'narrow', proje
|
|
|
129
129
|
${projectClause}
|
|
130
130
|
ORDER BY
|
|
131
131
|
CASE WHEN lesson_learned IS NOT NULL AND lesson_learned != '' THEN 0 ELSE 1 END,
|
|
132
|
-
created_at_epoch DESC
|
|
132
|
+
created_at_epoch DESC,
|
|
133
|
+
id DESC
|
|
133
134
|
LIMIT ?
|
|
134
135
|
`);
|
|
135
136
|
return project ? stmt.all(project, limit) : stmt.all(limit);
|
|
@@ -149,7 +150,7 @@ export function findReenrichCandidates(db, limit = 10, { scope = 'narrow', proje
|
|
|
149
150
|
AND LENGTH(COALESCE(narrative, '')) > 100
|
|
150
151
|
AND ${notLowSignalTitleClause('')}
|
|
151
152
|
${projectClause}
|
|
152
|
-
ORDER BY created_at_epoch DESC
|
|
153
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
153
154
|
LIMIT ?
|
|
154
155
|
`);
|
|
155
156
|
return project ? stmt.all(project, limit) : stmt.all(limit);
|
|
@@ -182,12 +183,15 @@ export function findReenrichCandidates(db, limit = 10, { scope = 'narrow', proje
|
|
|
182
183
|
AND LENGTH(COALESCE(narrative, '')) > 100
|
|
183
184
|
AND ${notLowSignalTitleClause('')}
|
|
184
185
|
${projectClause}
|
|
185
|
-
ORDER BY created_at_epoch DESC
|
|
186
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
186
187
|
LIMIT ?
|
|
187
188
|
`);
|
|
188
189
|
return project ? stmt.all(project, limit) : stmt.all(limit);
|
|
189
190
|
}
|
|
190
191
|
if (scope === 'wide') {
|
|
192
|
+
// This pool's ORDER BY leads with a CASE term and spans lines, which is exactly why the
|
|
193
|
+
// first pass of the D#9 tiebreaker missed it. The full note lives in the default pool at
|
|
194
|
+
// the bottom of this function -- read it before touching any ORDER BY here.
|
|
191
195
|
const stmt = db.prepare(`
|
|
192
196
|
SELECT id, title, narrative, type, subtitle, concepts, facts, search_aliases, importance, project
|
|
193
197
|
FROM observations
|
|
@@ -200,7 +204,8 @@ export function findReenrichCandidates(db, limit = 10, { scope = 'narrow', proje
|
|
|
200
204
|
${projectClause}
|
|
201
205
|
ORDER BY
|
|
202
206
|
CASE type WHEN 'decision' THEN 0 WHEN 'bugfix' THEN 1 WHEN 'refactor' THEN 2 ELSE 3 END,
|
|
203
|
-
created_at_epoch DESC
|
|
207
|
+
created_at_epoch DESC,
|
|
208
|
+
id DESC
|
|
204
209
|
LIMIT ?
|
|
205
210
|
`);
|
|
206
211
|
return project ? stmt.all(project, limit) : stmt.all(limit);
|
|
@@ -215,7 +220,36 @@ export function findReenrichCandidates(db, limit = 10, { scope = 'narrow', proje
|
|
|
215
220
|
AND search_aliases IS NULL
|
|
216
221
|
AND optimized_at IS NULL
|
|
217
222
|
${projectClause}
|
|
218
|
-
|
|
223
|
+
-- D#9: the id term is a REACHABILITY guard, not cosmetics. Every pool in this file is
|
|
224
|
+
-- ORDER BY created_at_epoch DESC LIMIT n feeding JS-side work, so a tie AT THE
|
|
225
|
+
-- BOUNDARY decides pool MEMBERSHIP. Measured 2026-09-07: two inserts land in the same
|
|
226
|
+
-- millisecond 272/300 times, and on a tie SQLite returns ASCENDING rowid -- the exact
|
|
227
|
+
-- opposite of the "newest first" this clause states -- so the newest rows fell out of
|
|
228
|
+
-- the pool whenever the clock had not ticked. SQLite's tie order is deterministic here
|
|
229
|
+
-- (8 rows on one epoch, 200 queries, one returned order), so this is not defending
|
|
230
|
+
-- against a varying plan; it is making the stated order total.
|
|
231
|
+
--
|
|
232
|
+
-- TWO DIFFERENT COUNTS, AND AN EARLIER DRAFT OF THIS COMMENT CONFLATED THEM. This
|
|
233
|
+
-- function, findReenrichCandidates, holds FIVE pools -- five db.prepare blocks:
|
|
234
|
+
-- 'scopes', 'aliases', 'concepts', 'wide', and this default 'narrow' one. The FILE
|
|
235
|
+
-- holds SEVEN "ORDER BY ... created_at_epoch DESC" sites: those five plus
|
|
236
|
+
-- extractUniqueConcepts and findMergeCandidates. All seven now carry the id term.
|
|
237
|
+
-- DO NOT GREP FOR THE ONE-LINE FORM: two of the seven ('scopes' and 'wide') lead with
|
|
238
|
+
-- a CASE ... term and span several lines, so grepping the joined
|
|
239
|
+
-- "created_at_epoch DESC, id DESC" spelling sees only five. That is how the first pass
|
|
240
|
+
-- read six and shipped 'wide' untiebroken -- the pool the DAILY unattended path passes
|
|
241
|
+
-- explicitly, on a budget of 6, where a boundary tie decides which rows reach the LLM
|
|
242
|
+
-- on a given run. Caught later by a test driving scope 'wide'; the original boundary
|
|
243
|
+
-- case drove only 'narrow', so nothing went red.
|
|
244
|
+
-- NOT FIXED, AND NAMED SO THE COMPLETENESS CLAIM IS TRUE: findSmartCompressCandidates
|
|
245
|
+
-- carries an eighth ordering, "ORDER BY project, created_at_epoch" -- ASCENDING, no id
|
|
246
|
+
-- term, no LIMIT. It is outside the seven by construction and is left alone under Iron
|
|
247
|
+
-- Law #1: it feeds clusterForCompression, whose vector branch seeds clusters in SQL
|
|
248
|
+
-- order, so a tie could move cluster membership -- but that branch needs
|
|
249
|
+
-- CLAUDE_MEM_VECTORS=1 and is off by default, and no failing case has been built.
|
|
250
|
+
-- Unjudged, not cleared.
|
|
251
|
+
-- This comment is INSIDE a template literal, so it must never contain a backtick.
|
|
252
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
219
253
|
LIMIT ?
|
|
220
254
|
`);
|
|
221
255
|
return project ? stmt.all(project, limit) : stmt.all(limit);
|
|
@@ -632,7 +666,7 @@ export function extractUniqueConcepts(db, limit = 500, { project } = {}) {
|
|
|
632
666
|
WHERE ${liveObsFilterSql('')}
|
|
633
667
|
AND concepts IS NOT NULL AND concepts != ''
|
|
634
668
|
${projectClause}
|
|
635
|
-
ORDER BY created_at_epoch DESC
|
|
669
|
+
ORDER BY created_at_epoch DESC, id DESC -- D#9: total order, see findReenrichCandidates
|
|
636
670
|
LIMIT 2000
|
|
637
671
|
`);
|
|
638
672
|
const rows = project ? stmt.all(project) : stmt.all();
|
|
@@ -807,7 +841,10 @@ export function findMergeCandidates(db, maxClusters = 5, { project } = {}) {
|
|
|
807
841
|
AND title IS NOT NULL AND title != ''
|
|
808
842
|
AND created_at_epoch > ?
|
|
809
843
|
${projectClause}
|
|
810
|
-
|
|
844
|
+
-- D#9: this pool's head is what the keeper reduce falls back to on a full tie, so an
|
|
845
|
+
-- arbitrary tie order decides WHICH DUPLICATE SURVIVES a merge. Same-episode rows are
|
|
846
|
+
-- exactly that tie (same project, same importance, access_count 0, same millisecond).
|
|
847
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
811
848
|
LIMIT 200
|
|
812
849
|
`);
|
|
813
850
|
const rows = project ? stmt.all(cutoff, project) : stmt.all(cutoff);
|
|
@@ -876,14 +913,27 @@ Return ONLY valid JSON:
|
|
|
876
913
|
});
|
|
877
914
|
if (!parsed || !parsed.should_merge) return { merged: false };
|
|
878
915
|
|
|
879
|
-
// Keeper = highest importance, then highest access_count. Previously
|
|
880
|
-
// alone, so a critical (importance=3) but never-accessed observation lost
|
|
881
|
-
// role to a trivial (importance=1) accessed one and was compressed away.
|
|
916
|
+
// Keeper = highest importance, then highest access_count, then highest id. Previously
|
|
917
|
+
// access_count alone, so a critical (importance=3) but never-accessed observation lost
|
|
918
|
+
// the keeper role to a trivial (importance=1) accessed one and was compressed away.
|
|
919
|
+
//
|
|
920
|
+
// D#9: the third term is the one that makes this TOTAL. Without it a full tie fell
|
|
921
|
+
// through to `cluster[0]` — the SQL head — and same-episode duplicates are exactly a
|
|
922
|
+
// full tie: same project, same importance, access_count 0, and a created_at_epoch in
|
|
923
|
+
// the same millisecond 272 times out of 300 (measured 2026-09-07). On a tie SQLite
|
|
924
|
+
// returns ASCENDING rowid while an untied pool returns the newest first, so which
|
|
925
|
+
// duplicate survived flipped on whether two writes straddled a millisecond. Ordering
|
|
926
|
+
// the pool alone would not have been enough: this reduce is exported to callers that
|
|
927
|
+
// build their own cluster, so it has to be total on its own. Highest id = written last
|
|
928
|
+
// = the version whose content the merged summary should be anchored on.
|
|
882
929
|
const keeper = cluster.reduce((best, o) => {
|
|
883
930
|
const oi = o.importance || 1,
|
|
884
931
|
bi = best.importance || 1;
|
|
885
932
|
if (oi !== bi) return oi > bi ? o : best;
|
|
886
|
-
|
|
933
|
+
const oa = o.access_count || 0,
|
|
934
|
+
ba = best.access_count || 0;
|
|
935
|
+
if (oa !== ba) return oa > ba ? o : best;
|
|
936
|
+
return (o.id || 0) > (best.id || 0) ? o : best;
|
|
887
937
|
}, cluster[0]);
|
|
888
938
|
const others = cluster.filter((o) => o.id !== keeper.id);
|
|
889
939
|
// Floor the merged importance at the cluster max — merging must never silently
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "5.5.
|
|
3
|
+
"version": "5.5.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-mem-lite",
|
|
9
|
-
"version": "5.5.
|
|
9
|
+
"version": "5.5.1",
|
|
10
10
|
"os": [
|
|
11
11
|
"darwin",
|
|
12
12
|
"linux"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "5.5.
|
|
3
|
+
"version": "5.5.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",
|