claude-mem-lite 3.85.0 → 3.85.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": "3.85.0",
13
+ "version": "3.85.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.85.0",
3
+ "version": "3.85.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/hook-memory.mjs CHANGED
@@ -24,23 +24,65 @@ const MEMORY_LOOKBACK_MS = 60 * DAY_MS; // 60 days
24
24
  * importance × cross-project × OR × noise × cite). So whatever these numbers are, a row
25
25
  * outside the window cannot be picked however high its composite score would have been.
26
26
  *
27
- * The window has to be wide because the composite spread is enormous. Multiplying the
28
- * extremes of the JS factors (same-project, AND mode): best = 1.5 decision × 1.5 lesson
29
- * × 1.0 importance × 1.0 noise × 3.0 cite = 6.75; worst = 0.5 change × 1.0 no-lesson
30
- * × 0.6 importance × 0.2 noise × 0.4 cite = 0.024. That is a **281× spread**, so a row
31
- * ranked below the window on raw bm25 can outscore the window's contents by a wide
32
- * margin. (The audit estimated ">10×"; the factor tables say 281×.)
27
+ * The window has to be wide because the composite spread is wide 281× by the tables,
28
+ * 60.0× as realised over the rows this pool can actually return. Multiplying the extremes
29
+ * of the JS factors
30
+ * (same-project, AND mode): best = 1.5 decision × 1.5 lesson × 1.0 importance × 1.0 noise
31
+ * × 3.0 cite = 6.75; worst = 0.5 change × 1.0 no-lesson × 0.6 importance × 0.2 noise ×
32
+ * 0.4 cite = 0.024, i.e. a **281× DECLARED range**. That is an upper bound off the factor
33
+ * tables, not a measurement: `citeFactor = 0.4` requires `uncited_streak >= 3`, and
34
+ * citation-decay resets the streak at 3 after demoting importance, so the steady state is
35
+ * bounded by [0,2] (scoring-sql.mjs, citeFactorJs docblock). Measured 2026-09-01 over the
36
+ * 2284 rows that clear `liveObsFilterSql` — the one predicate in the WHERE of BOTH SELECTs
37
+ * below — 0 are at streak >= 3, and recomputing the factor per row gives a REALISED range
38
+ * of 0.1125 … 6.750: a **60.0× spread** (81 rows hit the full best case, 0 the full worst).
39
+ * Each leg then narrows further and the spread survives the narrowing, which is why one
40
+ * number is quotable: live+`importance >= 1` n=2249 and +`notLowSignalTitleClause` n=2245
41
+ * both still read 60.00×. The CROSS leg is the exception — its own population
42
+ * (`type IN ('decision','discovery') AND importance >= 2`) is n=444 at **17.31×**, so if
43
+ * you are reasoning about `RERANK_POOL_CROSS_PROJECT` specifically, 60× is the wrong figure.
33
44
  *
34
- * HONEST LIMIT OF THIS FIX: because the spread is 281× and bm25 magnitude decays slowly
35
- * across a top-N window, NO finite pool size proves sufficiency. 30/15 is a widening
36
- * chosen where cost stays flat (the SELECT carries `narrative`, so the pool is the
37
- * expensive term, not the sort) it makes the bound loose, it does not remove it.
45
+ * COUNT THAT POPULATION WITH THE POOL'S OWN FILTER. Over the raw `observations` table it
46
+ * reads 0.0780 6.750 = 86.5×, and that is the number the first draft of this comment
47
+ * shipped: 1458 of 3742 rows (39.0%) are compressed or superseded, the row supplying the
48
+ * 0.0780 minimum (`id 10239`) carries `compressed_into = 10713`, and no such row can enter
49
+ * the pool, be scored, or be an endpoint of a range describing what the LIMIT cuts. Same
50
+ * error as v3.82.0's raw `importance = 3` count, overstating by 44% instead of a third.
38
51
  *
39
- * The bound is REMOVABLE, and deliberately was not removed: ordering both SELECTs by the
40
- * composite instead of raw bm25 is expressible in SQL today (every factor already has a
41
- * clause TYPE_QUALITY_CASE / noisePenaltyClause / citeFactorClause — and the two
42
- * remaining factors, cross-project and OR, are per-QUERY constants that cannot affect
43
- * within-query order). That would make LIMIT a true ranking bound. It is not done here
52
+ * Quote whichever population you mean, and say which. Either is wide enough that a row
53
+ * ranked below the window on raw bm25 can outscore the window's contents by a wide margin.
54
+ * (The audit estimated ">10×".)
55
+ *
56
+ * HONEST LIMIT OF THIS FIX: because the spread is wide and bm25 magnitude decays slowly
57
+ * across a top-N window, NO finite pool size proves sufficiency. 30/15 makes the bound
58
+ * loose; it does not remove it. And it is bought, not free — the first draft of this
59
+ * comment claimed "cost stays flat" in the same breath as a parenthetical saying the pool
60
+ * is the expensive term, which is its own refutation. Measured instead:
61
+ * `node benchmark/rerank-pool-replay.mjs --cost` reads **+5% to +16% depending on caliber,
62
+ * and +6% to +10% with this one**. Whole-corpus runs of `--cost` on this machine: 1.058,
63
+ * 1.068, 1.078, 1.080, 1.083, 1.102 — same code, same corpus, pure machine variance, and
64
+ * the absolute ms/prompt moved 3.04 -> 1.80 across the same runs. Other calibers:
65
+ * 1.054–1.065 with the arm order held fixed, 1.063–1.156 with each arm alone in its own
66
+ * process (the closest shape to production).
67
+ *
68
+ * **Quote the range, re-measure, and never quote the absolute ms** — they vary by 2x with
69
+ * load while the ratio holds. The first draft of this comment quoted a flat 1.058x and said
70
+ * it reproduced to three digits; it does not, and every later run came in above it. See
71
+ * `costCompare`'s docblock for which caliber biases which way. Timing the SELECT alone
72
+ * reports ~1.00x and misses the JS scoring that the widened pool feeds — a different
73
+ * question, not a better answer.
74
+ *
75
+ * The bound is REMOVABLE, and deliberately was not removed. Ordering both SELECTs by the
76
+ * composite instead of raw bm25 is close to expressible in SQL, but "every factor already
77
+ * has a clause" overstated it: of the SEVEN factors, three have named clauses
78
+ * (TYPE_QUALITY_CASE / noisePenaltyClause / citeFactorClause); two more — the 1.5× lesson
79
+ * bonus and the `importance >= 2` step — still need one written, because the SQL forms
80
+ * that exist encode different weights and shapes (`1.0 + 0.3·lesson` and
81
+ * `0.5 + 0.5·importance` in search-engine.mjs's FULL_SCORE); and the last two,
82
+ * cross-project and OR, are constant WITHIN EACH SELECT — they differ between the
83
+ * same-project and cross-project legs, so they are not per-CALL constants, but they never
84
+ * vary among the rows any one LIMIT cuts, which is the only thing this argument needs.
85
+ * That would make LIMIT a true ranking bound. It is not done here
44
86
  * because `lib/inject-search-core.mjs:23-25` records this surface's "BM25-sort + JS
45
87
  * scoring" composition as a deliberate per-surface asymmetry (#8786), and this face is
46
88
  * one `benchmark/denoise-ab.mjs` is structurally blind to (its suites drive the
@@ -48,8 +90,13 @@ const MEMORY_LOOKBACK_MS = 60 * DAY_MS; // 60 days
48
90
  * project has repeatedly shipped regressions. Widening is monotone and provable;
49
91
  * re-ranking needs a ruler that does not exist yet.
50
92
  *
51
- * WHY WIDENING IS SAFE: the old window is a strict PREFIX of the new one (same ORDER BY,
52
- * larger LIMIT), so the new candidate set is a superset. `scored` sorts by composite and
93
+ * WHY WIDENING IS SAFE: in practice the old window is a PREFIX of the new one (same plan,
94
+ * same ORDER BY, larger LIMIT), so the new candidate set is a superset. "Strict" would be
95
+ * overclaiming — `ORDER BY bm25(...)` carries no tiebreaker, and this release's own
96
+ * fixture lesson is that a degenerate corpus makes `bm25()` return 0.000 for every row and
97
+ * ranking fall to rowid. What is measured rather than argued: `rerank-pool-replay.mjs`
98
+ * reports nonEmptyToEmpty = 0 across the whole corpus, i.e. no prompt loses its injection
99
+ * to the widening. `scored` sorts by composite and
53
100
  * the threshold filter is monotone in that score, so every row returned is at least as
54
101
  * good as the row it displaced. The only non-monotone stage is the term-coverage filter,
55
102
  * which is exactly why the pool needs slack rather than just `MAX_MEMORY_INJECTIONS`.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.85.0",
3
+ "version": "3.85.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "claude-mem-lite",
9
- "version": "3.85.0",
9
+ "version": "3.85.1",
10
10
  "dependencies": {
11
11
  "@modelcontextprotocol/sdk": "^1.26.0",
12
12
  "better-sqlite3": "^12.6.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.85.0",
3
+ "version": "3.85.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",
@@ -63,10 +63,28 @@ const RUNTIME_DIR = process.env.CLAUDE_MEM_RUNTIME_DIR || join(DATA_DIR, 'runtim
63
63
  import { DEDUP_STALE_MS as CROSS_HOOK_DEDUP_MS } from './prompt-search-utils.mjs';
64
64
  // Upper bound on the over-fetch the cross-hook dedup buys itself (ALGO-4). The dedup
65
65
  // runs in JS after the SELECTs, so each LIMIT is raised by the seen-set size to keep the
66
- // dedup a re-ranking rather than a truncation. This cap exists because the seen-set is
67
- // read from a file on disk: it is bounded by UPS's own per-prompt budget in practice
68
- // (MAX_RESULTS 3), but an unbounded value read off disk must never size a query. 5 is
69
- // well above that budget and still leaves the worst case at 2+5=7 rows per SELECT.
66
+ // dedup a re-ranking rather than a truncation. This cap exists for one reason only: the
67
+ // seen-set is read from a file on disk, and a number off disk must never size a query.
68
+ //
69
+ // It is NOT a sufficiency argument, and the first version of this comment claimed one —
70
+ // "bounded by UPS's own per-prompt budget in practice (MAX_RESULTS 3)". That premise is
71
+ // false. `crossHookInjectedFile` is a UNION across hooks and calls inside the staleness
72
+ // window: `mergeCrossHookInjected` unions new ids into the old ones, UPS contributes up
73
+ // to MAX_RESULTS per prompt and this script contributes up to `mergeCap` per trigger, so
74
+ // nothing holds it at 3. Measured on this machine's `runtime/.claude-mem-injected-*`
75
+ // markers (2026-09-01): id-count histogram 1x9, 2x1, 3x2, 16x1 over n=13, and 1x11, 2x1,
76
+ // 3x1, 15x1 over n=14 an hour later. Read that as "3 is not a bound", not as a
77
+ // distribution: it is one developer machine, the tail entry is a single long agent session
78
+ // (on the re-measure the top entry was the measuring session itself), and the count is of
79
+ // ids IN THE FILE while `readCrossHookInjected` returns an EMPTY set for a payload whose
80
+ // `ts` is outside DEDUP_STALE_MS — file size and runtime seen-set size are not the same
81
+ // quantity.
82
+ //
83
+ // The residual failure mode that premise was hiding, derived from the arithmetic and NOT
84
+ // observed in the wild: at a seen-set of 16 the slack still caps at 5, so a Read fetches
85
+ // obsLimit = 6, and if all 6 are in the seen-set the face goes silent again — the exact
86
+ // failure ALGO-4 exists to fix. The cap is right (an unbounded LIMIT is worse), the
87
+ // reassurance was wrong.
70
88
  const CROSS_HOOK_DEDUP_SLACK_MAX = 5;
71
89
  // v2.33.1: cooldown path is session-scoped so same-file-twice within one
72
90
  // session never re-injects (was: global file, 5-min window). Cross-session:
@@ -465,8 +483,11 @@ try {
465
483
  // On a Read (obsLimit 1 / eventsLimit 1) one dedup hit silenced the whole face.
466
484
  // Read the seen-set FIRST and over-fetch by its size so the dedup removes rows
467
485
  // from a pool that still has enough left to fill the cap. Capped at
468
- // CROSS_HOOK_DEDUP_SLACK_MAX: the seen-set is bounded by UPS's own per-prompt
469
- // budget in practice, but it is read off disk and must not size a query.
486
+ // CROSS_HOOK_DEDUP_SLACK_MAX purely because the seen-set is read off disk and a
487
+ // number off disk must not size a query NOT because the seen-set is small. It is
488
+ // a cross-hook union over the staleness window and was measured at up to 16 ids on
489
+ // this machine, so with the slack saturated a Read can still fetch fewer rows than
490
+ // the seen-set holds and go silent. See the constant's docblock.
470
491
  const crossHookSeen = readCrossHookInjected(project, sessionId);
471
492
  const dedupSlack = Math.min(crossHookSeen.size, CROSS_HOOK_DEDUP_SLACK_MAX);
472
493
  const obsLimit = (isRead ? 1 : 2) + dedupSlack;
@@ -815,6 +815,21 @@ async function main() {
815
815
  // tail of the SAME query is the same reach with one fewer FTS scan, and it cannot
816
816
  // regress the head — a flat cap of 2 over the merged set could have, by evicting
817
817
  // a third head row that ships today.
818
+ //
819
+ // "Additive" scopes to THIS SET, not to what finally ships. Downstream the merge
820
+ // appends `fileRows` after `ftsRows` (dedup by id) and then slices to MAX_RESULTS, and
821
+ // `deep` rows sort LAST within `ftsRows` (weaker |bm25|, ascending sort) — so a deep
822
+ // row takes a slot ahead of a file-recall row whenever `|head| < MAX_RESULTS` AND
823
+ // `|head| + |deep| + |fileRows| > MAX_RESULTS`, with at least one deep row and one
824
+ // fileRow present. The first condition is not redundant: `mainLimit` is
825
+ // `intent?.limit || MAX_RESULTS`, so head can already be 3 — and at head=3 the fileRow
826
+ // never boarded in the first place, so nothing is displaced. At head=2/deep=1/
827
+ // fileRows=1 the output is [h1, h2, d1] where it was [h1, h2, f1]; at
828
+ // head=1/deep=1/fileRows=1 nothing is displaced. The trade is one
829
+ // "filename matched, presumed deliberate" row for one "weak overall bm25, admitted
830
+ // only on an identifier hit" row. It is a real quality judgement and it is UNMEASURED
831
+ // — denoise-ab is structurally blind to this face. v3.85.0's release note called the
832
+ // whole change "strictly additive"; true of the bypass set, false of the output.
818
833
  const bypassFloorOk = (r) => typeof r.relevance === 'number' && Math.abs(r.relevance) >= bm25Floor;
819
834
  let bypassRows = [];
820
835
  if (IDENTIFIER_BYPASS && promptIdentifiers.length > 0) {