lethe-memory 0.2.0__tar.gz → 0.2.2__tar.gz

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.
Files changed (114) hide show
  1. lethe_memory-0.2.2/.release-please-manifest.json +3 -0
  2. lethe_memory-0.2.2/ARCHITECTURE.md +74 -0
  3. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/CHANGELOG.md +14 -0
  4. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/CLAUDE.md +4 -0
  5. lethe_memory-0.2.2/LICENSE +21 -0
  6. lethe_memory-0.2.2/PKG-INFO +97 -0
  7. lethe_memory-0.2.2/README.md +72 -0
  8. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/pyproject.toml +3 -1
  9. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/src/lethe/__init__.py +1 -1
  10. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/src/lethe/cli.py +8 -3
  11. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/src/lethe/db.py +6 -0
  12. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/src/lethe/markdown_store.py +1 -30
  13. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/tests/test_cli.py +0 -1
  14. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/tests/test_markdown_store.py +17 -16
  15. lethe_memory-0.2.0/.github/workflows/publish-pypi.yml +0 -40
  16. lethe_memory-0.2.0/.release-please-manifest.json +0 -3
  17. lethe_memory-0.2.0/PKG-INFO +0 -21
  18. lethe_memory-0.2.0/README.md +0 -288
  19. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/.claude-plugin/marketplace.json +0 -0
  20. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/.github/workflows/release-please.yml +0 -0
  21. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/.gitignore +0 -0
  22. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/BENCHMARKS.md +0 -0
  23. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/RESEARCH_JOURNEY.md +0 -0
  24. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/assets/demo.gif +0 -0
  25. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/assets/demo.mp4 +0 -0
  26. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/assets/logo.png +0 -0
  27. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/benchmarks/_lib/__init__.py +0 -0
  28. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/benchmarks/_lib/metrics.py +0 -0
  29. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/benchmarks/run_benchmark.py +0 -0
  30. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/benchmarks/run_lifecycle_benchmark.py +0 -0
  31. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/benchmarks/run_rif_benchmark.py +0 -0
  32. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/benchmarks/run_rif_clustered.py +0 -0
  33. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/benchmarks/run_rif_enriched.py +0 -0
  34. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/benchmarks/run_rif_explore.py +0 -0
  35. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/benchmarks/run_rif_explore_smart.py +0 -0
  36. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/benchmarks/run_rif_explore_threshold.py +0 -0
  37. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/benchmarks/run_rif_explore_validation.py +0 -0
  38. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/benchmarks/run_rif_extended_metrics.py +0 -0
  39. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/benchmarks/run_rif_gap.py +0 -0
  40. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/benchmarks/run_rif_sweep.py +0 -0
  41. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/demo/.gitignore +0 -0
  42. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/demo/package.json +0 -0
  43. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/demo/public/logo.png +0 -0
  44. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/demo/remotion.config.ts +0 -0
  45. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/demo/scripts/_pass.py +0 -0
  46. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/demo/scripts/_pass_injected.py +0 -0
  47. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/demo/scripts/ab_test.py +0 -0
  48. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/demo/scripts/collect.py +0 -0
  49. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/demo/src/Composition.tsx +0 -0
  50. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/demo/src/Intro.tsx +0 -0
  51. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/demo/src/LearningCurve.tsx +0 -0
  52. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/demo/src/Outro.tsx +0 -0
  53. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/demo/src/RifSchematic.tsx +0 -0
  54. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/demo/src/Root.tsx +0 -0
  55. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/demo/src/index.ts +0 -0
  56. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/demo/src/theme.ts +0 -0
  57. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/demo/src/types.ts +0 -0
  58. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/demo/tsconfig.json +0 -0
  59. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
  60. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/plugins/claude-code/README.md +0 -0
  61. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/plugins/claude-code/hooks/common.sh +0 -0
  62. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/plugins/claude-code/hooks/hooks.json +0 -0
  63. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/plugins/claude-code/hooks/parse-transcript.sh +0 -0
  64. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/plugins/claude-code/hooks/session-end.sh +0 -0
  65. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/plugins/claude-code/hooks/session-start.sh +0 -0
  66. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/plugins/claude-code/hooks/stop.sh +0 -0
  67. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/plugins/claude-code/hooks/user-prompt-submit.sh +0 -0
  68. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/plugins/claude-code/scripts/derive-collection.sh +0 -0
  69. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/plugins/claude-code/scripts/transcript.py +0 -0
  70. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/plugins/claude-code/skills/memory-recall/SKILL.md +0 -0
  71. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/release-please-config.json +0 -0
  72. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/research/__init__.py +0 -0
  73. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/research/gc_mutation/README.md +0 -0
  74. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/research/gc_mutation/__init__.py +0 -0
  75. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/research/gc_mutation/analyze.py +0 -0
  76. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/research/gc_mutation/baselines.py +0 -0
  77. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/research/gc_mutation/config.py +0 -0
  78. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/research/gc_mutation/graph.py +0 -0
  79. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/research/gc_mutation/rescue_index.py +0 -0
  80. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/research/gc_mutation/run_experiment.py +0 -0
  81. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/research/gc_mutation/segmentation.py +0 -0
  82. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/research/gc_mutation/store.py +0 -0
  83. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/research/sdm/README.md +0 -0
  84. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/research/sdm/dataset.py +0 -0
  85. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/research/sdm/embedding.py +0 -0
  86. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/research/sdm/eval.py +0 -0
  87. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/research/sdm/main.py +0 -0
  88. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/research/sdm/sdm.py +0 -0
  89. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/scripts/enrich_longmemeval.py +0 -0
  90. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/scripts/prep_longmemeval.py +0 -0
  91. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/src/lethe/_lock.py +0 -0
  92. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/src/lethe/_registry.py +0 -0
  93. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/src/lethe/dedup.py +0 -0
  94. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/src/lethe/encoders.py +0 -0
  95. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/src/lethe/enrichment.py +0 -0
  96. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/src/lethe/entry.py +0 -0
  97. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/src/lethe/memory_store.py +0 -0
  98. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/src/lethe/reranker.py +0 -0
  99. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/src/lethe/rif.py +0 -0
  100. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/src/lethe/union_store.py +0 -0
  101. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/src/lethe/vectors.py +0 -0
  102. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/tests/__init__.py +0 -0
  103. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/tests/conftest.py +0 -0
  104. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/tests/test_db.py +0 -0
  105. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/tests/test_dedup.py +0 -0
  106. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/tests/test_enrichment.py +0 -0
  107. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/tests/test_entry.py +0 -0
  108. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/tests/test_memory_store.py +0 -0
  109. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/tests/test_metrics.py +0 -0
  110. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/tests/test_reranker.py +0 -0
  111. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/tests/test_rif.py +0 -0
  112. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/tests/test_union_store.py +0 -0
  113. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/tests/test_vectors.py +0 -0
  114. {lethe_memory-0.2.0 → lethe_memory-0.2.2}/uv.lock +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.2.2"
3
+ }
@@ -0,0 +1,74 @@
1
+ # Architecture
2
+
3
+ ## Retrieval pipeline
4
+
5
+ ```
6
+ Query
7
+
8
+ ├── FAISS top-30 (dense vector similarity)
9
+ ├── BM25 top-30 (sparse keyword match)
10
+
11
+ └── Merge (RRF)
12
+
13
+ └── RIF suppression penalty (per-cluster, gap-based)
14
+
15
+ └── Cross-encoder rerank → top-k
16
+
17
+ └── Update suppression state, affinities, tier
18
+ ```
19
+
20
+ **Optional write-time LLM enrichment layer** (`src/lethe/enrichment.py`): before indexing, each memory can be processed by an LLM (default `claude-haiku-4-5`) to produce a gist, 3 anticipated queries, entities, and temporal markers. All fields index alongside the original text; cross-encoder still scores against original. Attacks the vocabulary-mismatch failure mode.
21
+
22
+ ## RIF: technical details
23
+
24
+ The formula that made it work (checkpoint 13 of 17):
25
+
26
+ ```
27
+ competition_strength = max(0, xenc_rank - initial_rank) / pool_size × sigmoid(-xenc_score)
28
+ suppression[eid, cluster] += learning_rate × competition_strength
29
+ score_adjusted = base_score - alpha × suppression[eid, cluster]
30
+ ```
31
+
32
+ The rank-gap term only penalises entries that **dropped in rank** between the initial hybrid retrieval and the cross-encoder rerank AND were actively rejected (negative xenc score). Entries that just lost a close race aren't suppressed. That prevents the system from forgetting legitimately useful context because it happened to rank second once.
33
+
34
+ k-means runs once (30 clusters) on the bi-encoder query embedding at retrieval time; the cluster assignment is the "cue" coordinate. Based on Anderson's inhibition theory (1994) and the SAM competitive-sampling model (Raaijmakers & Shiffrin, 1981).
35
+
36
+ ## Storage layers
37
+
38
+ | Layer | File | Purpose |
39
+ |-------|------|---------|
40
+ | DuckDB | `lethe.duckdb` | Entries, suppression state, rescue cache, stats. Many project DBs can be `ATTACH`ed simultaneously for cross-project search. |
41
+ | numpy + FAISS | `embeddings.npz`, `faiss.index` | Vector storage |
42
+ | BM25 | In-memory, rebuilt on startup | Sparse keyword index |
43
+
44
+ ## Entry lifecycle (germinal-center inspired)
45
+
46
+ ```
47
+ NAIVE → GC → MEMORY
48
+
49
+ APOPTOTIC
50
+ ```
51
+
52
+ - **Naive**: new entries, unproven
53
+ - **GC**: retrieved 3+ times, actively evaluated
54
+ - **Memory**: high affinity + frequently retrieved, stable, exempt from decay
55
+ - **Apoptotic**: low affinity + idle > 1000 steps, excluded from search
56
+
57
+ Useful for long-running agents; doesn't directly improve retrieval quality (that's what RIF and enrichment do).
58
+
59
+ ## Deduplication (on add)
60
+
61
+ 1. **Exact**: SHA-256 content hash (free)
62
+ 2. **Near-duplicate**: cosine similarity > 0.95 (keeps the longer entry)
63
+
64
+ ## Global search across projects
65
+
66
+ Every `lethe index` registers the project in `~/.lethe/projects.json`. `lethe search --all` then ATTACHes each registered `lethe.duckdb` as a read-only schema and runs the full hybrid + RIF + cross-encoder pipeline across the union.
67
+
68
+ ```bash
69
+ lethe search "mongodb pool" --all --top-k 5
70
+ lethe projects list
71
+ lethe projects prune
72
+ ```
73
+
74
+ DuckDB has no cap on attached databases, so the 10/125 ceiling in SQLite's `ATTACH DATABASE` doesn't apply.
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.2](https://github.com/teimurjan/lethe/compare/lethe-memory-v0.2.1...lethe-memory-v0.2.2) (2026-04-17)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * Remove chunk_map.json, expand reads from DuckDB directly ([5cb8c84](https://github.com/teimurjan/lethe/commit/5cb8c843213d91b5c561e97a5b058d14d52b0652))
9
+
10
+ ## [0.2.1](https://github.com/teimurjan/lethe/compare/lethe-memory-v0.2.0...lethe-memory-v0.2.1) (2026-04-17)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * Add README to PyPI, absolute logo URL, MIT license ([78188dd](https://github.com/teimurjan/lethe/commit/78188dd9642694f441f1a42a4ea99ae42a498dd7))
16
+
3
17
  ## [0.2.0](https://github.com/teimurjan/lethe/compare/lethe-memory-v0.1.0...lethe-memory-v0.2.0) (2026-04-17)
4
18
 
5
19
 
@@ -102,3 +102,7 @@ The 0.3680 is from BM25+vector+cross-encoder reranking (standard IR). The GC mec
102
102
  17 checkpoints. Checkpoints 1-10 (GC mechanisms): all failed. Checkpoint 11 (global RIF): +1.1%. Checkpoint 12 (clustered RIF): +5.8%. Checkpoint 13 (clustered + rank-gap): +6.5% NDCG, +9.5% recall@30 — retrieval-only best. Checkpoint 14 (exploration + rescue list): negative at full scale. Checkpoint 15 (SDM research prototype in `sdm/`): negative. Checkpoint 16 (extended metrics for checkpoint 13): RIF's gain is primarily from reducing wrong_family (−1.6pp); sibling_confusion and stale_fact unchanged. Checkpoint 17 (LLM enrichment layer, Haiku write-time gist + anticipated queries + entities + temporal markers): **+8.3pp NDCG on covered queries (+21% rel over RIF), largest single-lever gain in the journey**. Overall diluted to +1.2pp by partial coverage (15% of queries). Full corpus enrichment expected ~$16.
103
103
 
104
104
  Full research journey: [RESEARCH_JOURNEY.md](RESEARCH_JOURNEY.md)
105
+
106
+ ## git commits
107
+
108
+ Use conventional commits. release-please is configured to only bump on `feat:` (minor) and `fix:` (patch). Use `feat:` or `fix:` only when you want a PyPI release. For everything else use a plain message with no prefix (release-please ignores non-conventional commits entirely).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Teimur Gasanov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: lethe-memory
3
+ Version: 0.2.2
4
+ Summary: Self-improving memory store for LLM agents: hybrid retrieval, clustered retrieval-induced forgetting, optional LLM enrichment
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: anthropic>=0.30
9
+ Requires-Dist: duckdb>=0.10
10
+ Requires-Dist: faiss-cpu>=1.7
11
+ Requires-Dist: fastembed>=0.4
12
+ Requires-Dist: numpy>=1.24
13
+ Requires-Dist: rank-bm25>=0.2
14
+ Provides-Extra: benchmarks
15
+ Requires-Dist: beir>=2.0; extra == 'benchmarks'
16
+ Requires-Dist: datasets>=2.0; extra == 'benchmarks'
17
+ Requires-Dist: matplotlib>=3.7; extra == 'benchmarks'
18
+ Requires-Dist: sentence-transformers>=2.0; extra == 'benchmarks'
19
+ Requires-Dist: tqdm>=4.60; extra == 'benchmarks'
20
+ Provides-Extra: dev
21
+ Requires-Dist: mypy>=1.0; extra == 'dev'
22
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
23
+ Requires-Dist: pytest>=7.0; extra == 'dev'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # lethe
27
+
28
+ <div align="center">
29
+ <img src="https://raw.githubusercontent.com/teimurjan/lethe/main/assets/logo.png" width="300" height="300" />
30
+ </div>
31
+
32
+ > *Λήθη: the ancient Greek personification of forgetfulness, and one of the five rivers of the underworld.*
33
+
34
+ A memory store for LLM agents that **gets better the more you use it**. Hybrid BM25 + dense retrieval, cross-encoder reranking, clustered retrieval-induced forgetting (RIF), and optional LLM enrichment at write time.
35
+
36
+ Most memory tools are static caches - you put strings in, you get strings back by similarity, and the retrieval function never changes. lethe is different: every retrieval teaches it which entries are chronic distractors for which kinds of queries, and it quietly suppresses them over time. No fine-tuning, no extra LLM calls - just bookkeeping inspired by how human memory actually works ([Anderson, 1994](https://psycnet.apa.org/record/1994-29917-001)).
37
+
38
+ ## Benchmark
39
+
40
+ Numbers on the full 199,509-turn LongMemEval S corpus, **turn-level retrieval, NDCG@10, no leakage**. Most memory-tool benchmarks use ~50 sessions at session granularity - a ~2000× easier task. Those 99% numbers don't translate to this setup.
41
+
42
+ | Stage | NDCG@10 | notes |
43
+ |---|---|---|
44
+ | Hybrid BM25 + vector (RRF) | 0.217 | basic retrieval (most popular) |
45
+ | + cross-encoder reranking | 0.293 | +35% from semantic reranking |
46
+ | + clustered+gap RIF (checkpoint 13) | **0.312** | +6.5% from retrieval-induced forgetting |
47
+ | + LLM enrichment, on covered queries | **0.473** | +21% on the 75 queries where the answer turn was Haiku-enriched |
48
+
49
+ Full methodology in [BENCHMARKS.md](BENCHMARKS.md). 17 checkpoints (10 failed) in [RESEARCH_JOURNEY.md](RESEARCH_JOURNEY.md).
50
+
51
+
52
+ https://github.com/user-attachments/assets/ea6e9657-7525-4fed-ba40-df7a14e6b9f4
53
+
54
+
55
+ ## Install and quick start
56
+
57
+ ```bash
58
+ pip install lethe-memory
59
+ ```
60
+
61
+ ```python
62
+ from lethe import MemoryStore
63
+ from sentence_transformers import SentenceTransformer, CrossEncoder
64
+
65
+ store = MemoryStore(
66
+ "./my_memories",
67
+ bi_encoder=SentenceTransformer("all-MiniLM-L6-v2"),
68
+ cross_encoder=CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2"),
69
+ )
70
+
71
+ store.add("I prefer window seats on flights", session_id="trip")
72
+ store.add("My wife needs aisle seats", session_id="trip")
73
+ store.add("I work at Google as a software engineer", session_id="work")
74
+
75
+ results = store.retrieve("What are my travel preferences?", k=5)
76
+ for entry_id, content, score in results:
77
+ print(f" [{score:.1f}] {content}")
78
+
79
+ store.save()
80
+ store.close()
81
+ ```
82
+
83
+ As a CLI: `uv tool install lethe-memory && lethe --version`
84
+
85
+ As a Claude Code plugin: `/plugin marketplace add teimurjan/lethe`
86
+
87
+ See [plugins/claude-code/README.md](plugins/claude-code/README.md) for plugin details.
88
+
89
+ ## How it works
90
+
91
+ [ARCHITECTURE.md](ARCHITECTURE.md) - pipeline diagram, RIF formula, storage layers, entry lifecycle, cross-project search.
92
+
93
+ [RESEARCH_JOURNEY.md](RESEARCH_JOURNEY.md) - 17 checkpoints from biology-inspired mutation (all failed) through cognitive-science RIF (+6.5%) to LLM enrichment (+21% on covered queries).
94
+
95
+ ## License
96
+
97
+ MIT
@@ -0,0 +1,72 @@
1
+ # lethe
2
+
3
+ <div align="center">
4
+ <img src="https://raw.githubusercontent.com/teimurjan/lethe/main/assets/logo.png" width="300" height="300" />
5
+ </div>
6
+
7
+ > *Λήθη: the ancient Greek personification of forgetfulness, and one of the five rivers of the underworld.*
8
+
9
+ A memory store for LLM agents that **gets better the more you use it**. Hybrid BM25 + dense retrieval, cross-encoder reranking, clustered retrieval-induced forgetting (RIF), and optional LLM enrichment at write time.
10
+
11
+ Most memory tools are static caches - you put strings in, you get strings back by similarity, and the retrieval function never changes. lethe is different: every retrieval teaches it which entries are chronic distractors for which kinds of queries, and it quietly suppresses them over time. No fine-tuning, no extra LLM calls - just bookkeeping inspired by how human memory actually works ([Anderson, 1994](https://psycnet.apa.org/record/1994-29917-001)).
12
+
13
+ ## Benchmark
14
+
15
+ Numbers on the full 199,509-turn LongMemEval S corpus, **turn-level retrieval, NDCG@10, no leakage**. Most memory-tool benchmarks use ~50 sessions at session granularity - a ~2000× easier task. Those 99% numbers don't translate to this setup.
16
+
17
+ | Stage | NDCG@10 | notes |
18
+ |---|---|---|
19
+ | Hybrid BM25 + vector (RRF) | 0.217 | basic retrieval (most popular) |
20
+ | + cross-encoder reranking | 0.293 | +35% from semantic reranking |
21
+ | + clustered+gap RIF (checkpoint 13) | **0.312** | +6.5% from retrieval-induced forgetting |
22
+ | + LLM enrichment, on covered queries | **0.473** | +21% on the 75 queries where the answer turn was Haiku-enriched |
23
+
24
+ Full methodology in [BENCHMARKS.md](BENCHMARKS.md). 17 checkpoints (10 failed) in [RESEARCH_JOURNEY.md](RESEARCH_JOURNEY.md).
25
+
26
+
27
+ https://github.com/user-attachments/assets/ea6e9657-7525-4fed-ba40-df7a14e6b9f4
28
+
29
+
30
+ ## Install and quick start
31
+
32
+ ```bash
33
+ pip install lethe-memory
34
+ ```
35
+
36
+ ```python
37
+ from lethe import MemoryStore
38
+ from sentence_transformers import SentenceTransformer, CrossEncoder
39
+
40
+ store = MemoryStore(
41
+ "./my_memories",
42
+ bi_encoder=SentenceTransformer("all-MiniLM-L6-v2"),
43
+ cross_encoder=CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2"),
44
+ )
45
+
46
+ store.add("I prefer window seats on flights", session_id="trip")
47
+ store.add("My wife needs aisle seats", session_id="trip")
48
+ store.add("I work at Google as a software engineer", session_id="work")
49
+
50
+ results = store.retrieve("What are my travel preferences?", k=5)
51
+ for entry_id, content, score in results:
52
+ print(f" [{score:.1f}] {content}")
53
+
54
+ store.save()
55
+ store.close()
56
+ ```
57
+
58
+ As a CLI: `uv tool install lethe-memory && lethe --version`
59
+
60
+ As a Claude Code plugin: `/plugin marketplace add teimurjan/lethe`
61
+
62
+ See [plugins/claude-code/README.md](plugins/claude-code/README.md) for plugin details.
63
+
64
+ ## How it works
65
+
66
+ [ARCHITECTURE.md](ARCHITECTURE.md) - pipeline diagram, RIF formula, storage layers, entry lifecycle, cross-project search.
67
+
68
+ [RESEARCH_JOURNEY.md](RESEARCH_JOURNEY.md) - 17 checkpoints from biology-inspired mutation (all failed) through cognitive-science RIF (+6.5%) to LLM enrichment (+21% on covered queries).
69
+
70
+ ## License
71
+
72
+ MIT
@@ -4,8 +4,10 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "lethe-memory"
7
- version = "0.2.0" # x-release-please-version
7
+ version = "0.2.2" # x-release-please-version
8
8
  description = "Self-improving memory store for LLM agents: hybrid retrieval, clustered retrieval-induced forgetting, optional LLM enrichment"
9
+ readme = "README.md"
10
+ license = "MIT"
9
11
  requires-python = ">=3.11"
10
12
  dependencies = [
11
13
  "faiss-cpu>=1.7",
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.2.0" # x-release-please-version
3
+ __version__ = "0.2.2" # x-release-please-version
4
4
 
5
5
  from lethe.memory_store import MemoryStore
6
6
  from lethe.rif import RIFConfig
@@ -416,10 +416,15 @@ def _snippet(content: str, width: int = 160) -> str:
416
416
 
417
417
  def cmd_expand(args: argparse.Namespace) -> int:
418
418
  paths = resolve_paths(args.root)
419
- from lethe.markdown_store import MarkdownStore
419
+ db_path = paths.index / "lethe.duckdb"
420
+ if not db_path.exists():
421
+ print(f"chunk {args.chunk_id!r} not found", file=sys.stderr)
422
+ return 1
423
+ from lethe.db import MemoryDB
420
424
 
421
- md = MarkdownStore(memory_dir=paths.memory, index_dir=paths.index)
422
- content = md.get_chunk(args.chunk_id)
425
+ db = MemoryDB(db_path)
426
+ content = db.get_content(args.chunk_id)
427
+ db.close()
423
428
  if content is None:
424
429
  print(f"chunk {args.chunk_id!r} not found", file=sys.stderr)
425
430
  return 1
@@ -142,6 +142,12 @@ class MemoryDB:
142
142
  )
143
143
  return _rows_as_dicts(cur)
144
144
 
145
+ def get_content(self, entry_id: str) -> str | None:
146
+ row = self._conn.execute(
147
+ "SELECT content FROM entries WHERE id=?", [entry_id]
148
+ ).fetchone()
149
+ return str(row[0]) if row else None
150
+
145
151
  def delete_entry(self, entry_id: str) -> None:
146
152
  self._conn.execute("DELETE FROM entries WHERE id=?", [entry_id])
147
153
 
@@ -16,7 +16,6 @@ on a directory of markdown files and a :class:`~lethe.memory_store.MemoryStore`.
16
16
  from __future__ import annotations
17
17
 
18
18
  import hashlib
19
- import json
20
19
  import re
21
20
  from dataclasses import dataclass
22
21
  from pathlib import Path
@@ -165,14 +164,11 @@ class MarkdownStore:
165
164
  so ``lethe expand`` can return the full section.
166
165
  """
167
166
 
168
- CHUNK_MAP_FILENAME = "chunk_map.json"
169
-
170
167
  def __init__(self, memory_dir: Path, index_dir: Path) -> None:
171
168
  self.memory_dir = Path(memory_dir)
172
169
  self.index_dir = Path(index_dir)
173
170
  self.memory_dir.mkdir(parents=True, exist_ok=True)
174
171
  self.index_dir.mkdir(parents=True, exist_ok=True)
175
- self._chunk_map_path = self.index_dir / self.CHUNK_MAP_FILENAME
176
172
 
177
173
  # ---- Scanning ----------------------------------------------------------
178
174
 
@@ -190,26 +186,6 @@ class MarkdownStore:
190
186
  out.extend(split_into_chunks(text, f))
191
187
  return out
192
188
 
193
- # ---- Chunk map persistence --------------------------------------------
194
-
195
- def _load_chunk_map(self) -> dict[str, str]:
196
- if not self._chunk_map_path.exists():
197
- return {}
198
- try:
199
- data = json.loads(self._chunk_map_path.read_text(encoding="utf-8"))
200
- except (OSError, json.JSONDecodeError):
201
- return {}
202
- return {str(k): str(v) for k, v in data.items() if isinstance(v, str)}
203
-
204
- def _save_chunk_map(self, chunk_map: dict[str, str]) -> None:
205
- self._chunk_map_path.write_text(
206
- json.dumps(chunk_map, ensure_ascii=False, indent=0),
207
- encoding="utf-8",
208
- )
209
-
210
- def get_chunk(self, chunk_id: str) -> str | None:
211
- return self._load_chunk_map().get(chunk_id)
212
-
213
189
  # ---- Reindex -----------------------------------------------------------
214
190
 
215
191
  def reindex(self, store: MemoryStore) -> dict[str, int]:
@@ -221,7 +197,6 @@ class MarkdownStore:
221
197
  """
222
198
  chunks = self.scan()
223
199
  current_ids = {c.id for c in chunks}
224
- previous = self._load_chunk_map()
225
200
 
226
201
  added = 0
227
202
  unchanged = 0
@@ -239,10 +214,9 @@ class MarkdownStore:
239
214
  if inserted is not None:
240
215
  added += 1
241
216
  else:
242
- # Dedup suppressed the insert (exact hash collision or near-dup).
243
217
  unchanged += 1
244
218
 
245
- for old_id in set(previous) - current_ids:
219
+ for old_id in set(store.entries) - current_ids:
246
220
  if old_id in store.entries:
247
221
  store.db.delete_entry(old_id)
248
222
  store.entries.pop(old_id, None)
@@ -252,9 +226,6 @@ class MarkdownStore:
252
226
  if removed:
253
227
  store._rebuild_index()
254
228
 
255
- chunk_map = {c.id: c.content for c in chunks}
256
- self._save_chunk_map(chunk_map)
257
-
258
229
  return {
259
230
  "added": added,
260
231
  "removed": removed,
@@ -170,7 +170,6 @@ def test_expand_prints_chunk(
170
170
  assert rc == 0
171
171
  out = capsys.readouterr().out
172
172
  assert "window seats" in out
173
- assert "session:s" in out
174
173
 
175
174
 
176
175
  def test_expand_missing_chunk_returns_error(
@@ -135,7 +135,7 @@ def test_scan_reads_all_md_files_in_order(tmp_path: Path) -> None:
135
135
  assert [c.heading for c in chunks] == ["A", "B"]
136
136
 
137
137
 
138
- def test_reindex_adds_new_chunks_and_persists_chunk_map(
138
+ def test_reindex_adds_new_chunks(
139
139
  tmp_path: Path, mock_bi_encoder, mock_cross_encoder
140
140
  ) -> None:
141
141
  memory = tmp_path / "memory"
@@ -157,10 +157,7 @@ def test_reindex_adds_new_chunks_and_persists_chunk_map(
157
157
  store.close()
158
158
 
159
159
  assert counts == {"added": 2, "removed": 0, "unchanged": 0, "total": 2}
160
- # chunk map file exists and has both chunk ids
161
- chunk_map = md._load_chunk_map()
162
- assert len(chunk_map) == 2
163
- assert all(len(cid) == 16 for cid in chunk_map)
160
+ assert store.size == 2
164
161
 
165
162
 
166
163
  def test_reindex_is_idempotent(
@@ -251,21 +248,25 @@ def test_get_chunk_returns_full_markdown(
251
248
  )
252
249
  try:
253
250
  md.reindex(store)
251
+ [chunk] = md.scan()
252
+ expanded = store.db.get_content(chunk.id)
253
+ assert expanded is not None
254
+ assert "bullet" in expanded
254
255
  finally:
255
256
  store.close()
256
257
 
257
- [chunk] = md.scan()
258
- expanded = md.get_chunk(chunk.id)
259
- assert expanded is not None
260
- assert "bullet" in expanded
261
- assert parse_anchor(expanded) == {
262
- "session": "s", "turn": "t", "transcript": "/x",
263
- }
264
-
265
258
 
266
- def test_get_chunk_returns_none_for_unknown_id(tmp_path: Path) -> None:
267
- md = MarkdownStore(memory_dir=tmp_path / "memory", index_dir=tmp_path / "index")
268
- assert md.get_chunk("deadbeef" * 2) is None
259
+ def test_get_content_returns_none_for_unknown_id(
260
+ tmp_path: Path, mock_bi_encoder, mock_cross_encoder,
261
+ ) -> None:
262
+ store = MemoryStore(
263
+ tmp_path / "store",
264
+ bi_encoder=mock_bi_encoder,
265
+ cross_encoder=mock_cross_encoder,
266
+ dim=mock_bi_encoder.dim,
267
+ )
268
+ assert store.db.get_content("deadbeef" * 2) is None
269
+ store.close()
269
270
 
270
271
 
271
272
  def test_chunk_anchor_property_pulls_from_content() -> None:
@@ -1,40 +0,0 @@
1
- name: publish
2
-
3
- on:
4
- release:
5
- types: [published]
6
-
7
- jobs:
8
- build:
9
- name: build sdist + wheel
10
- runs-on: ubuntu-latest
11
- steps:
12
- - uses: actions/checkout@v4
13
- - uses: actions/setup-python@v5
14
- with:
15
- python-version: "3.12"
16
- - name: Install build tooling
17
- run: python -m pip install --upgrade build
18
- - name: Build distributions
19
- run: python -m build
20
- - uses: actions/upload-artifact@v4
21
- with:
22
- name: dist
23
- path: dist/
24
- if-no-files-found: error
25
-
26
- publish:
27
- name: upload to PyPI (trusted publishing)
28
- needs: build
29
- runs-on: ubuntu-latest
30
- environment:
31
- name: pypi
32
- url: https://pypi.org/p/lethe-memory
33
- permissions:
34
- id-token: write
35
- steps:
36
- - uses: actions/download-artifact@v4
37
- with:
38
- name: dist
39
- path: dist/
40
- - uses: pypa/gh-action-pypi-publish@release/v1
@@ -1,3 +0,0 @@
1
- {
2
- ".": "0.2.0"
3
- }
@@ -1,21 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: lethe-memory
3
- Version: 0.2.0
4
- Summary: Self-improving memory store for LLM agents: hybrid retrieval, clustered retrieval-induced forgetting, optional LLM enrichment
5
- Requires-Python: >=3.11
6
- Requires-Dist: anthropic>=0.30
7
- Requires-Dist: duckdb>=0.10
8
- Requires-Dist: faiss-cpu>=1.7
9
- Requires-Dist: fastembed>=0.4
10
- Requires-Dist: numpy>=1.24
11
- Requires-Dist: rank-bm25>=0.2
12
- Provides-Extra: benchmarks
13
- Requires-Dist: beir>=2.0; extra == 'benchmarks'
14
- Requires-Dist: datasets>=2.0; extra == 'benchmarks'
15
- Requires-Dist: matplotlib>=3.7; extra == 'benchmarks'
16
- Requires-Dist: sentence-transformers>=2.0; extra == 'benchmarks'
17
- Requires-Dist: tqdm>=4.60; extra == 'benchmarks'
18
- Provides-Extra: dev
19
- Requires-Dist: mypy>=1.0; extra == 'dev'
20
- Requires-Dist: pytest-cov>=4.0; extra == 'dev'
21
- Requires-Dist: pytest>=7.0; extra == 'dev'
@@ -1,288 +0,0 @@
1
- # lethe
2
-
3
- <div align="center">
4
- <img src="./assets/logo.png" width="300" height="300" />
5
- </div>
6
-
7
- > *Λήθη: the ancient Greek personification of forgetfulness, and one of the five rivers of the underworld.*
8
-
9
- A memory store for LLM agents. Hybrid BM25 + dense retrieval, cross-encoder reranking, **clustered retrieval-induced forgetting (RIF)**, and an optional LLM enrichment layer at write time.
10
-
11
- ## Read this before the benchmark numbers
12
-
13
- Most memory-tool READMEs advertise NDCG or recall in the 95 to 99% range. Those numbers come from evaluating on a per-query fresh database of ~50 sessions, at session granularity, with recall@5. That's a roughly 2000× easier task than "find this specific turn among 200,000". Some implementations additionally leak ground-truth annotations into the index.
14
-
15
- lethe's numbers are reported on the full 199,509-turn LongMemEval S corpus, **turn-level retrieval, NDCG@10, no leakage**. Random baseline is 0.005%. Vector-only search gets 0.14. BM25 alone gets 0.24. When other tools are run on *this* setup, they land in the same 0.1 to 0.4 range. The 99% numbers don't translate.
16
-
17
- No 99%. No cherry-picking. These are the honest numbers on a hard benchmark:
18
-
19
- | Stage | NDCG@10 | notes |
20
- |---|---|---|
21
- | Hybrid BM25 + vector (RRF) | 0.217 | basic retrieval (most popular) |
22
- | + cross-encoder reranking | 0.293 | +35% from semantic reranking |
23
- | + clustered+gap RIF (checkpoint 13) | **0.312** | +6.5% from retrieval-induced forgetting |
24
- | + LLM enrichment, on covered queries | **0.473** | +21% on the 75 queries where the answer turn was Haiku-enriched |
25
-
26
- Enrichment only helps where it's been applied; the full-corpus number is diluted by the uncovered 425 queries. Full methodology in [BENCHMARKS.md](BENCHMARKS.md). The research path, including 10 failed approaches before anything worked, is in [RESEARCH_JOURNEY.md](RESEARCH_JOURNEY.md).
27
-
28
-
29
- https://github.com/user-attachments/assets/ea6e9657-7525-4fed-ba40-df7a14e6b9f4
30
-
31
- > Lethe full stack vs basic hybrid RRF on the 199,509-turn LongMemEval corpus. NDCG@10 over 5,000 queries.
32
-
33
- ## What's new here: retrieval-induced forgetting
34
-
35
- RIF is a well-studied phenomenon in cognitive psychology (Anderson, 1994). In plain words:
36
-
37
- > *When you retrieve one thing, related things that could have been retrieved but weren't get quietly suppressed. Ask someone to name a fruit; they say "apple". A few minutes later, "pear" and "orange" are measurably harder for them to recall, even though nothing asked about them. Remembering is also an act of forgetting.*
38
-
39
- lethe brings this mechanism to vector memory:
40
-
41
- - Every retrieval surfaces a candidate pool (BM25 ∪ vector top-k).
42
- - The cross-encoder picks the winners.
43
- - Entries that reached the pool but lost don't disappear. They accumulate a per-cluster **suppression score**.
44
- - On future retrievals in the same query cluster ("travel-like queries", "debugging-like queries"), those entries get their scores penalised *before* the cross-encoder sees them, freeing slots for entries that were previously crowded out.
45
-
46
- The key detail: suppression is **cue-dependent, not global**. An entry suppressed for travel queries stays fully available for food queries. Global suppression was ~5× weaker in our benchmarks. The clustering is what made it work.
47
-
48
- ## Why this matters: a memory that evolves
49
-
50
- Most memory stores are static caches. You put strings in; you retrieve them by similarity. The retrieval function never changes.
51
-
52
- RIF makes retrieval behaviour **adapt over time without retraining any model**:
53
-
54
- - Entries that keep losing to more relevant alternatives get quieter in their own cluster.
55
- - Entries that keep winning get amplified via tier promotion (naive → gc → memory) and exemption from decay.
56
- - The system learns "in the context of *this kind* of query, you usually mean *that* chunk, not *this* one" from its own retrieval history. No fine-tuning. No extra LLM calls. Just O(n_clusters × n_entries) bookkeeping.
57
-
58
- Concrete example: you store both *"I prefer window seats on flights"* and *"my wife needs aisle seats"*. Travel queries keep picking your preference first. The system quietly penalises your wife's preference for travel-sounding queries, not globally, just in that cluster. Ask about your spouse's preferences and both entries remain equally findable.
59
-
60
- Checkpoint 13 (clustered + rank-gap RIF) gives +6.5% NDCG and +9.5% recall@30 on LongMemEval over the baseline hybrid pipeline. As far as I can find, this is the first implementation of clustered RIF in a production retrieval system.
61
-
62
- ## Claude Code plugin
63
-
64
- Lethe ships as a Claude Code plugin that persists memory across sessions per-project:
65
-
66
- ```
67
- /plugin marketplace add teimurjan/lethe
68
- /plugin install lethe
69
- ```
70
-
71
- Hooks write daily markdown to `.lethe/memory/YYYY-MM-DD.md`, summarize each turn with `claude -p --model haiku` (no extra API key), reindex via the `lethe` CLI, and a `memory-recall` skill surfaces prior-session context on demand.
72
-
73
- See [plugins/claude-code/README.md](plugins/claude-code/README.md) for hook behavior, CLI reference, and local-install instructions.
74
-
75
- ## Quick start
76
-
77
- ```python
78
- from lethe import MemoryStore
79
- from sentence_transformers import SentenceTransformer, CrossEncoder
80
-
81
- store = MemoryStore(
82
- "./my_memories",
83
- bi_encoder=SentenceTransformer("all-MiniLM-L6-v2"),
84
- cross_encoder=CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2"),
85
- )
86
-
87
- store.add("I prefer window seats on flights", session_id="trip")
88
- store.add("My wife needs aisle seats", session_id="trip")
89
- store.add("I work at Google as a software engineer", session_id="work")
90
-
91
- results = store.retrieve("What are my travel preferences?", k=5)
92
- for entry_id, content, score in results:
93
- print(f" [{score:.1f}] {content}")
94
-
95
- store.save()
96
- store.close()
97
- ```
98
-
99
- ## Architecture
100
-
101
- ```
102
- Query
103
-
104
- ├── FAISS top-30 (dense vector similarity)
105
- ├── BM25 top-30 (sparse keyword match)
106
-
107
- └── Merge (RRF)
108
-
109
- └── RIF suppression penalty (per-cluster, gap-based)
110
-
111
- └── Cross-encoder rerank → top-k
112
-
113
- └── Update suppression state, affinities, tier
114
- ```
115
-
116
- **Optional write-time LLM enrichment layer** (`src/lethe/enrichment.py`): before indexing, each memory can be processed by an LLM (default `claude-haiku-4-5`) to produce a gist, 3 anticipated queries, entities, and temporal markers. All fields index alongside the original text; cross-encoder still scores against original. Attacks the vocabulary-mismatch failure mode.
117
-
118
- ### RIF: technical details
119
-
120
- The formula that made it work (checkpoint 13 of 17):
121
-
122
- ```
123
- competition_strength = max(0, xenc_rank - initial_rank) / pool_size × sigmoid(-xenc_score)
124
- suppression[eid, cluster] += learning_rate × competition_strength
125
- score_adjusted = base_score - alpha × suppression[eid, cluster]
126
- ```
127
-
128
- The rank-gap term only penalises entries that **dropped in rank** between the initial hybrid retrieval and the cross-encoder rerank AND were actively rejected (negative xenc score). Entries that just lost a close race aren't suppressed. That prevents the system from forgetting legitimately useful context because it happened to rank second once.
129
-
130
- k-means runs once (30 clusters) on the bi-encoder query embedding at retrieval time; the cluster assignment is the "cue" coordinate. Based on Anderson's inhibition theory (1994) and the SAM competitive-sampling model (Raaijmakers & Shiffrin, 1981).
131
-
132
- ### Three storage layers
133
-
134
- | Layer | File | Purpose |
135
- |-------|------|---------|
136
- | DuckDB | `lethe.duckdb` | Entries, suppression state, rescue cache, stats. Many project DBs can be `ATTACH`ed simultaneously for cross-project search. |
137
- | numpy + FAISS | `embeddings.npz`, `faiss.index` | Vector storage |
138
- | BM25 | In-memory, rebuilt on startup | Sparse keyword index |
139
-
140
- ### Entry lifecycle (germinal-center inspired)
141
-
142
- ```
143
- NAIVE → GC → MEMORY
144
-
145
- APOPTOTIC
146
- ```
147
-
148
- - **Naive**: new entries, unproven
149
- - **GC**: retrieved 3+ times, actively evaluated
150
- - **Memory**: high affinity + frequently retrieved, stable, exempt from decay
151
- - **Apoptotic**: low affinity + idle > 1000 steps, excluded from search
152
-
153
- Useful for long-running agents; doesn't directly improve retrieval quality (that's what RIF and enrichment do).
154
-
155
- ### Deduplication (on add)
156
-
157
- 1. **Exact**: SHA-256 content hash (free)
158
- 2. **Near-duplicate**: cosine similarity > 0.95 (keeps the longer entry)
159
-
160
- ## Install
161
-
162
- From PyPI (the distribution name is `lethe-memory`; the import name is still `lethe`):
163
-
164
- ```bash
165
- pip install lethe-memory
166
- # or
167
- uv pip install lethe-memory
168
- ```
169
-
170
- As the `lethe` CLI on PATH without a venv:
171
-
172
- ```bash
173
- uv tool install lethe-memory
174
- lethe --version
175
- ```
176
-
177
- From source, for development:
178
-
179
- ```bash
180
- git clone https://github.com/teimurjan/lethe && cd lethe
181
- uv venv --python 3.12 && uv pip install -e .
182
- ```
183
-
184
- As a Claude Code plugin (see [plugin README](plugins/claude-code/README.md)):
185
-
186
- ```
187
- /plugin marketplace add teimurjan/lethe
188
- /plugin install lethe
189
- ```
190
-
191
- ## Global search across projects
192
-
193
- Every `lethe index` registers the project in `~/.lethe/projects.json`. `lethe search --all` then ATTACHes each registered `lethe.duckdb` as a read-only schema and runs the full hybrid + RIF + cross-encoder pipeline across the union.
194
-
195
- ```bash
196
- # In each project you care about:
197
- lethe index
198
-
199
- # Search across all registered projects:
200
- lethe search "mongodb pool" --all --top-k 5
201
- # [p_site_fix_pentest_8a99] [+4.63] 06e6... - MongoDB connection pool issue at 100% rollout…
202
- # [p_lethe_77be] [+3.41] 4670... - lethe search returning real hybrid results…
203
-
204
- # Restrict to a subset:
205
- lethe search "auth flow" --projects p_site_fix_pentest_8a99,p_api_server_12ab
206
-
207
- # Inspect / manage the registry:
208
- lethe projects list
209
- lethe projects add /path/to/other-project
210
- lethe projects remove <slug-or-path>
211
- lethe projects prune # drop entries whose roots no longer exist
212
- ```
213
-
214
- Opt out of auto-registration in three ways:
215
- - Per invocation: `lethe index --no-register`
216
- - Per project: `lethe config set auto_register false`
217
- - Globally: `export LETHE_DISABLE_GLOBAL_REGISTRY=1`
218
-
219
- The registry only stores absolute project paths, never contents. Cross-project search is read-only; it does not mutate RIF state. Local `lethe search` continues to learn as normal.
220
-
221
- DuckDB has no cap on attached databases, so the 10/125 ceiling in SQLite's `ATTACH DATABASE` doesn't apply. Real-world bottleneck before you'd feel it is the per-project BM25 rebuild, which scales linearly.
222
-
223
- ## Benchmark
224
-
225
- ```bash
226
- # prep LongMemEval
227
- uv run python experiments/data_prep.py --dataset longmemeval
228
-
229
- # retrieval-only baseline + RIF variants
230
- uv run python benchmarks/run_benchmark.py
231
- uv run python benchmarks/run_rif_benchmark.py
232
-
233
- # LLM enrichment layer (needs ANTHROPIC_API_KEY)
234
- export ANTHROPIC_API_KEY=sk-ant-...
235
- uv run python experiments/enrich_longmemeval.py # one-time, ~$16 for 10k entries
236
- uv run python benchmarks/run_rif_enriched.py # 3-arm benchmark
237
- ```
238
-
239
- See [BENCHMARKS.md](BENCHMARKS.md) for results tables and methodology details.
240
-
241
- ## Project structure
242
-
243
- ```
244
- src/lethe/ # Production library (162 tests, ~95% coverage)
245
- ├── memory_store.py
246
- ├── markdown_store.py # Markdown chunker for .lethe/memory/*.md
247
- ├── cli.py # `lethe` console script
248
- ├── union_store.py # Cross-project read-only search via DuckDB ATTACH
249
- ├── db.py # DuckDB persistence
250
- ├── vectors.py # FAISS + BM25 index
251
- ├── reranker.py # Cross-encoder + adaptive depth
252
- ├── rif.py # Retrieval-induced forgetting (clustered + gap)
253
- ├── enrichment.py # Optional LLM write-time enrichment (Anthropic SDK)
254
- ├── dedup.py # Hash + cosine deduplication
255
- └── entry.py # MemoryEntry dataclass + Tier enum
256
-
257
- plugins/claude-code/ # Claude Code plugin (hooks + memory-recall skill)
258
- .claude-plugin/ # Marketplace manifest
259
-
260
- benchmarks/ # Per-checkpoint benchmark scripts
261
- ├── run_*.py
262
- ├── _lib/ # Benchmark-only helpers (metrics, etc.)
263
- └── results/ # Raw per-run output markdowns
264
-
265
- scripts/ # Reproducibility utilities (dataset prep, enrichment runner)
266
-
267
- research/ # Experimental / research code (not production)
268
- ├── gc_mutation/ # Germinal-center mutation thread (checkpoints 1-10)
269
- └── sdm/ # Sparse Distributed Memory prototype (checkpoint 15)
270
-
271
- tests/ # Production unit tests
272
- ```
273
-
274
- ## Research background
275
-
276
- This project started as an experiment porting the immune system's germinal-center mechanism to vector memory. 17 checkpoints so far:
277
-
278
- 1. **Checkpoints 1-10** (GC-mutation approaches): all failed to improve retrieval quality. Useful for lifecycle management, not retrieval.
279
- 2. **Checkpoints 11-13** (RIF): cognitive-science inspired. Clustered + gap-formula variant gives +6.5% NDCG, +9.5% recall@30 on the full 500-query eval. First positive learned-retrieval result.
280
- 3. **Checkpoints 14-15** (exploration/rescue, Sparse Distributed Memory prototype): both negative at full scale.
281
- 4. **Checkpoint 16** (extended behavior metrics for checkpoint 13): RIF's gain is primarily from reducing cross-topic retrieval (−1.6pp wrong_family); sibling confusion and stale-fact rate unchanged.
282
- 5. **Checkpoint 17** (LLM enrichment layer): write-time structured extraction via Haiku. On covered queries: +8.3pp NDCG over checkpoint 13, the largest single-lever gain in the journey. Overall diluted by partial coverage; scaling to full coverage is pending.
283
-
284
- Full journey in [RESEARCH_JOURNEY.md](RESEARCH_JOURNEY.md).
285
-
286
- ## License
287
-
288
- MIT.
File without changes
File without changes
File without changes