claude-mem-lite 3.81.0 → 3.82.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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "3.81.0",
13
+ "version": "3.82.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.81.0",
3
+ "version": "3.82.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/hook-memory.mjs CHANGED
@@ -412,6 +412,57 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [])
412
412
  // injection query is guarded by the subprocess cases in
413
413
  // tests/pre-tool-recall.test.mjs. Do not reintroduce an in-process twin here.
414
414
 
415
+ /**
416
+ * Upper bound on the imperative candidate pool. This is an OOM/latency BACKSTOP, not a
417
+ * ranking gate — read that literally before changing it (D#172, authorised 2026-08-25).
418
+ *
419
+ * The bound is applied in SQL, i.e. BEFORE the identifier-overlap filter below, so
420
+ * whatever it is set to is a hard REACHABILITY bound: a lesson outside the window cannot
421
+ * be picked however well it matches the prompt. At its original value of 50 that made
422
+ * this face reachable only from the 50 newest `importance >= 2` rows, and in five
423
+ * projects on this machine the importance=3 population ALONE exceeds 50 — so every
424
+ * importance=2 lesson in those projects was structurally unreachable, and a
425
+ * citation-decay demotion 3->2 EVICTED a row from the pool instead of down-ranking it.
426
+ * That eviction loop is the risk D#172 was filed on; raising the bound above any
427
+ * plausible per-project population is what closes it, because a 3->2 demotion then only
428
+ * changes the row's score multiplier, which is what the decay design intends.
429
+ *
430
+ * COUNT THE POPULATION WITH THE POOL'S OWN FILTER. Those figures are
431
+ * `liveObsFilterSql` + the `importance >= 2` + non-empty-lesson gates, i.e. what the query
432
+ * below can actually return — 327 / 121 / 56 / 53 / 51 for projects--mem, code-graph-mcp,
433
+ * ubuntu-sec, daagu, agentsmd. The first version of this note published the RAW
434
+ * importance=3 counts instead (365 / 131 / 62 / 69 / 51), which include superseded and
435
+ * compressed rows the pool can never see and overstated one project by a third; the
436
+ * pre-tag review caught it, and caught that those wrong numbers had replaced correct ones
437
+ * in lib/citation-tracker.mjs. Re-measure with `node benchmark/imperative-pool-replay.mjs
438
+ * --population`, never with a bare `SELECT ... WHERE importance = 3`.
439
+ *
440
+ * 3->2 IS NOW A DOWN-RANK; 2->1 IS STILL AN EVICTION. The pool gate is
441
+ * `COALESCE(importance, 1) >= 2`, so a row demoted to the IMPORTANCE_FLOOR of 1 leaves
442
+ * this face's reach until some other face cites it back up. Widening the bound is also
443
+ * what first makes importance=2 rows reachable here (56 of projects--mem's 383 eligible),
444
+ * so it creates the injections that can walk one down to 1. Measured exposure: of the
445
+ * picks the widening newly surfaces, one is importance=2 — `score = importance x overlap`
446
+ * keeps importance=3 rows ahead nearly always — so this is a known small edge, not a
447
+ * closed loop.
448
+ *
449
+ * MEASURED, and reproducible: `node benchmark/imperative-pool-replay.mjs`. Over 373 real
450
+ * user prompts replayed against their OWN project's live corpus (85 produced a candidate
451
+ * at all), the 50-row bound destroyed 7 picks outright (8.2%) and changed the top-1 in 3
452
+ * of 78 (3.8%). Small n, so the load-bearing argument is not that one: the wide pool is a
453
+ * SUPERSET of the narrow one, so its top-1 score is always >= the narrow one's and a
454
+ * stable sort keeps the incumbent on a tie — a different pick therefore always means a
455
+ * strictly higher score under this face's own objective. That harness attacks the claim on
456
+ * every prompt and exits non-zero on a counterexample; it currently finds none.
457
+ *
458
+ * COST, from the same harness against projects--mem (383 eligible rows under the shipped
459
+ * predicate): 0.44 ms/prompt at 50, 1.50 ms/prompt at 5000, on a UserPromptSubmit path
460
+ * budgeted in seconds. A synthetic 8000-row pool measured 8.3 ms/prompt, so the bound is
461
+ * doing real work at the top of its range and should not be raised casually. It is ~13x
462
+ * the largest eligible population on this machine.
463
+ */
464
+ export const IMPERATIVE_POOL_BACKSTOP = 5000;
465
+
415
466
  /**
416
467
  * Phase-2 task-imperative ranking (spec 2026-06-29 §4.1): score every candidate lesson
417
468
  * relevant to THIS prompt (importance>=2 + non-empty lesson + identifier overlap with the
@@ -428,6 +479,13 @@ export function rankImperativeCandidates(db, userPrompt, project, excludeIds = [
428
479
  if (promptIdents.size === 0) return []; // no symbol anchor → no imperative (precision-first)
429
480
  const exclude = new Set(excludeIds);
430
481
  let rows;
482
+ // The ORDER BY ends in `id DESC` to make it a TOTAL order. Without that tiebreaker two
483
+ // rows sharing (importance, created_at_epoch) may come back in either relative order,
484
+ // and SQLite is free to use a bounded top-N sorter at a small LIMIT and a full sort at a
485
+ // large one — so "the narrow pool is a prefix of the wide pool", which the v3.82.0
486
+ // widening argument rests on, was an empirical property of this corpus rather than a
487
+ // guaranteed one. There are zero such collisions live, so it changes no behaviour here;
488
+ // it makes the guarantee hold on corpora nobody has seen.
431
489
  try {
432
490
  rows = db.prepare(`
433
491
  SELECT id, title, lesson_learned, importance
@@ -439,8 +497,8 @@ export function rankImperativeCandidates(db, userPrompt, project, excludeIds = [
439
497
  AND TRIM(lesson_learned) != ''
440
498
  AND LOWER(TRIM(lesson_learned)) != 'none'
441
499
  AND (? IS NULL OR created_at_epoch <= ?)
442
- ORDER BY importance DESC, created_at_epoch DESC
443
- LIMIT 50
500
+ ORDER BY importance DESC, created_at_epoch DESC, id DESC
501
+ LIMIT ${IMPERATIVE_POOL_BACKSTOP}
444
502
  `).all(project, epochTo, epochTo);
445
503
  } catch { return []; }
446
504
  const out = [];
@@ -586,19 +586,31 @@ export function extractAllInjected(transcriptPath, opts = {}) {
586
586
  * The product's own CLI calls that state "Active decay queue (uncited_streak >= 2, next
587
587
  * miss -> demote)"; a release note claiming three sessions of margin contradicted it.
588
588
  *
589
- * THE RESIDUAL RISK, unresolved and deliberately shipped: all 22 marginal rows are
590
- * importance=3, because rankImperativeCandidates orders by importance DESC and takes 50,
591
- * and in the five largest projects here the imp=3 population alone exceeds that limit
592
- * (projects--mem 326, code-graph-mcp 121). So a demotion 3->2 EVICTS a lesson from this
593
- * face's candidate pool rather than merely down-ranking it a feedback loop the four
594
- * original denominator faces do not have, because they select on FTS relevance. It is
595
- * NOT permanent: 3->2 still clears the pool's `>= 2` gate (it only loses the LIMIT 50
596
- * race), and updatePromote restores importance on the next citation from ANY face, so a
597
- * row returns the moment something else surfaces and cites it. If lessons start
598
- * disappearing from imperative picks, this is the first place to look, and the cap
599
- * belongs in rankImperativeCandidates (raise LIMIT above the imp=3 population, or exempt
600
- * this face's picks from demotion) rather than back here. Tracked with `subagent` in
601
- * D#172.
589
+ * THE RESIDUAL RISK CLOSED 2026-08-25, in rankImperativeCandidates rather than here.
590
+ * All 22 marginal rows are importance=3, and the pool took `LIMIT 50` ordered by
591
+ * importance DESC, so in the five largest projects (projects--mem 327, code-graph-mcp
592
+ * 121, ubuntu-sec 56, daagu 53, agentsmd 51 counted with the pool's OWN
593
+ * `liveObsFilterSql`, which is the only count that means anything here) the imp=3
594
+ * population alone exceeded the limit: a demotion 3->2 EVICTED a lesson from this face's
595
+ * candidate pool rather than down-ranking it a feedback loop the four original
596
+ * denominator faces do not have, because they select on FTS relevance.
597
+ *
598
+ * Measuring it turned up a bigger fact than the one filed. The limit sits in SQL,
599
+ * BEFORE the identifier-overlap filter, so it was never a ranking bound at all: the
600
+ * face could only ever pick from the 50 newest `importance >= 2` rows, which made every
601
+ * importance=2 lesson in those five projects unreachable no matter how well it matched.
602
+ * Replaying 373 real prompts against their own project's corpus
603
+ * (`benchmark/imperative-pool-replay.mjs`), the cap destroyed 7 of 85 picks outright and
604
+ * changed the top-1 in 3 of 78. The bound is now IMPERATIVE_POOL_BACKSTOP = 5000,
605
+ * documented there as an OOM backstop and not a relevance gate.
606
+ *
607
+ * A 3->2 demotion is a down-rank again. A 2->1 demotion is still an EVICTION, because
608
+ * the pool gate is `>= 2` and IMPORTANCE_FLOOR is 1 — and widening is what first makes
609
+ * importance=2 rows reachable by this face at all, so it creates the injections that can
610
+ * walk one there. Measured exposure is one row; see the constant's docblock. `subagent`
611
+ * shares that pool and inherits both halves. Still open in D#172: admitting `subagent`
612
+ * to the denominator, which is a separate decision needing the receiver-attributed cites
613
+ * merged asymmetrically.
602
614
  */
603
615
  const DECAY_EXCLUDED_SURFACES = new Set();
604
616
 
@@ -633,9 +645,11 @@ export const DECAY_DENOMINATOR_SURFACES = ATTACHMENT_SURFACES.filter((f) => !DEC
633
645
  * sampled (0.28%), or 3 of 1935 (0.16%) if you widen to every subagent-bearing
634
646
  * session in the corpus; state which denominator you mean, the phrase "main-face
635
647
  * ids" alone does not pin it; (2) the 33 rows it would newly add cite at 15.2% and are
636
- * 94% importance=3 same LIMIT-50 eviction loop described under
637
- * `task_imperative` above, since both faces share selectImperativeLesson. Three
638
- * of those 33 would actually demote over that corpus.
648
+ * 94% importance=3. That second cost was the LIMIT-50 eviction loop described under
649
+ * `task_imperative` above, shared because both faces select through
650
+ * rankImperativeCandidates CLOSED 2026-08-25 by raising that pool's bound out of
651
+ * relevance range, so it is no longer an argument against admitting this face. Three
652
+ * of those 33 would still demote over that corpus, as down-ranks rather than evictions.
639
653
  *
640
654
  * Its sibling was admitted on 2026-08-25; this one deliberately was NOT, and the
641
655
  * difference is not the rate. task_imperative needed one line and no change to
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.81.0",
3
+ "version": "3.82.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "claude-mem-lite",
9
- "version": "3.81.0",
9
+ "version": "3.82.0",
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.81.0",
3
+ "version": "3.82.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",