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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "5.5.0",
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.0",
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. The single-query baseline returns 1-2 rows on the same
195
- * negatives the flood is the UNION across paraphrase variants, which is also where deep's
196
- * recall win comes from, so this is not a bug to be thresholded away. Three gates were
197
- * tested against both arms and rejected; suppressing OR-fallback on rewrites takes deep
198
- * R@10 from 0.7383 to 0.3962, because the vocab-mismatch win IS that fallback. rrfFuseN
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
- ORDER BY created_at_epoch DESC
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
- ORDER BY created_at_epoch DESC
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 access_count
880
- // alone, so a critical (importance=3) but never-accessed observation lost the keeper
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
- return (o.access_count || 0) > (best.access_count || 0) ? o : best;
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
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "5.5.0",
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.0",
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.0",
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",