lethe-memory 0.2.0__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.
- lethe_memory-0.2.0/.claude-plugin/marketplace.json +21 -0
- lethe_memory-0.2.0/.github/workflows/publish-pypi.yml +40 -0
- lethe_memory-0.2.0/.github/workflows/release-please.yml +41 -0
- lethe_memory-0.2.0/.gitignore +16 -0
- lethe_memory-0.2.0/.release-please-manifest.json +3 -0
- lethe_memory-0.2.0/BENCHMARKS.md +165 -0
- lethe_memory-0.2.0/CHANGELOG.md +50 -0
- lethe_memory-0.2.0/CLAUDE.md +104 -0
- lethe_memory-0.2.0/PKG-INFO +21 -0
- lethe_memory-0.2.0/README.md +288 -0
- lethe_memory-0.2.0/RESEARCH_JOURNEY.md +346 -0
- lethe_memory-0.2.0/assets/demo.gif +0 -0
- lethe_memory-0.2.0/assets/demo.mp4 +0 -0
- lethe_memory-0.2.0/assets/logo.png +0 -0
- lethe_memory-0.2.0/benchmarks/_lib/__init__.py +0 -0
- lethe_memory-0.2.0/benchmarks/_lib/metrics.py +107 -0
- lethe_memory-0.2.0/benchmarks/run_benchmark.py +195 -0
- lethe_memory-0.2.0/benchmarks/run_lifecycle_benchmark.py +252 -0
- lethe_memory-0.2.0/benchmarks/run_rif_benchmark.py +249 -0
- lethe_memory-0.2.0/benchmarks/run_rif_clustered.py +297 -0
- lethe_memory-0.2.0/benchmarks/run_rif_enriched.py +394 -0
- lethe_memory-0.2.0/benchmarks/run_rif_explore.py +389 -0
- lethe_memory-0.2.0/benchmarks/run_rif_explore_smart.py +409 -0
- lethe_memory-0.2.0/benchmarks/run_rif_explore_threshold.py +368 -0
- lethe_memory-0.2.0/benchmarks/run_rif_explore_validation.py +368 -0
- lethe_memory-0.2.0/benchmarks/run_rif_extended_metrics.py +317 -0
- lethe_memory-0.2.0/benchmarks/run_rif_gap.py +308 -0
- lethe_memory-0.2.0/benchmarks/run_rif_sweep.py +264 -0
- lethe_memory-0.2.0/demo/.gitignore +6 -0
- lethe_memory-0.2.0/demo/package.json +26 -0
- lethe_memory-0.2.0/demo/public/logo.png +0 -0
- lethe_memory-0.2.0/demo/remotion.config.ts +4 -0
- lethe_memory-0.2.0/demo/scripts/_pass.py +186 -0
- lethe_memory-0.2.0/demo/scripts/_pass_injected.py +146 -0
- lethe_memory-0.2.0/demo/scripts/ab_test.py +223 -0
- lethe_memory-0.2.0/demo/scripts/collect.py +85 -0
- lethe_memory-0.2.0/demo/src/Composition.tsx +80 -0
- lethe_memory-0.2.0/demo/src/Intro.tsx +71 -0
- lethe_memory-0.2.0/demo/src/LearningCurve.tsx +292 -0
- lethe_memory-0.2.0/demo/src/Outro.tsx +110 -0
- lethe_memory-0.2.0/demo/src/RifSchematic.tsx +314 -0
- lethe_memory-0.2.0/demo/src/Root.tsx +22 -0
- lethe_memory-0.2.0/demo/src/index.ts +4 -0
- lethe_memory-0.2.0/demo/src/theme.ts +16 -0
- lethe_memory-0.2.0/demo/src/types.ts +32 -0
- lethe_memory-0.2.0/demo/tsconfig.json +18 -0
- lethe_memory-0.2.0/plugins/claude-code/.claude-plugin/plugin.json +10 -0
- lethe_memory-0.2.0/plugins/claude-code/README.md +61 -0
- lethe_memory-0.2.0/plugins/claude-code/hooks/common.sh +119 -0
- lethe_memory-0.2.0/plugins/claude-code/hooks/hooks.json +51 -0
- lethe_memory-0.2.0/plugins/claude-code/hooks/parse-transcript.sh +101 -0
- lethe_memory-0.2.0/plugins/claude-code/hooks/session-end.sh +17 -0
- lethe_memory-0.2.0/plugins/claude-code/hooks/session-start.sh +58 -0
- lethe_memory-0.2.0/plugins/claude-code/hooks/stop.sh +96 -0
- lethe_memory-0.2.0/plugins/claude-code/hooks/user-prompt-submit.sh +22 -0
- lethe_memory-0.2.0/plugins/claude-code/scripts/derive-collection.sh +12 -0
- lethe_memory-0.2.0/plugins/claude-code/scripts/transcript.py +112 -0
- lethe_memory-0.2.0/plugins/claude-code/skills/memory-recall/SKILL.md +43 -0
- lethe_memory-0.2.0/pyproject.toml +40 -0
- lethe_memory-0.2.0/release-please-config.json +34 -0
- lethe_memory-0.2.0/research/__init__.py +0 -0
- lethe_memory-0.2.0/research/gc_mutation/README.md +29 -0
- lethe_memory-0.2.0/research/gc_mutation/__init__.py +0 -0
- lethe_memory-0.2.0/research/gc_mutation/analyze.py +202 -0
- lethe_memory-0.2.0/research/gc_mutation/baselines.py +44 -0
- lethe_memory-0.2.0/research/gc_mutation/config.py +49 -0
- lethe_memory-0.2.0/research/gc_mutation/graph.py +107 -0
- lethe_memory-0.2.0/research/gc_mutation/rescue_index.py +103 -0
- lethe_memory-0.2.0/research/gc_mutation/run_experiment.py +293 -0
- lethe_memory-0.2.0/research/gc_mutation/segmentation.py +124 -0
- lethe_memory-0.2.0/research/gc_mutation/store.py +347 -0
- lethe_memory-0.2.0/research/sdm/README.md +35 -0
- lethe_memory-0.2.0/research/sdm/dataset.py +329 -0
- lethe_memory-0.2.0/research/sdm/embedding.py +56 -0
- lethe_memory-0.2.0/research/sdm/eval.py +85 -0
- lethe_memory-0.2.0/research/sdm/main.py +219 -0
- lethe_memory-0.2.0/research/sdm/sdm.py +162 -0
- lethe_memory-0.2.0/scripts/enrich_longmemeval.py +113 -0
- lethe_memory-0.2.0/scripts/prep_longmemeval.py +148 -0
- lethe_memory-0.2.0/src/lethe/__init__.py +8 -0
- lethe_memory-0.2.0/src/lethe/_lock.py +50 -0
- lethe_memory-0.2.0/src/lethe/_registry.py +159 -0
- lethe_memory-0.2.0/src/lethe/cli.py +687 -0
- lethe_memory-0.2.0/src/lethe/db.py +262 -0
- lethe_memory-0.2.0/src/lethe/dedup.py +30 -0
- lethe_memory-0.2.0/src/lethe/encoders.py +112 -0
- lethe_memory-0.2.0/src/lethe/enrichment.py +311 -0
- lethe_memory-0.2.0/src/lethe/entry.py +67 -0
- lethe_memory-0.2.0/src/lethe/markdown_store.py +263 -0
- lethe_memory-0.2.0/src/lethe/memory_store.py +402 -0
- lethe_memory-0.2.0/src/lethe/reranker.py +43 -0
- lethe_memory-0.2.0/src/lethe/rif.py +249 -0
- lethe_memory-0.2.0/src/lethe/union_store.py +220 -0
- lethe_memory-0.2.0/src/lethe/vectors.py +124 -0
- lethe_memory-0.2.0/tests/__init__.py +0 -0
- lethe_memory-0.2.0/tests/conftest.py +152 -0
- lethe_memory-0.2.0/tests/test_cli.py +391 -0
- lethe_memory-0.2.0/tests/test_db.py +168 -0
- lethe_memory-0.2.0/tests/test_dedup.py +60 -0
- lethe_memory-0.2.0/tests/test_enrichment.py +293 -0
- lethe_memory-0.2.0/tests/test_entry.py +75 -0
- lethe_memory-0.2.0/tests/test_markdown_store.py +278 -0
- lethe_memory-0.2.0/tests/test_memory_store.py +505 -0
- lethe_memory-0.2.0/tests/test_metrics.py +84 -0
- lethe_memory-0.2.0/tests/test_reranker.py +59 -0
- lethe_memory-0.2.0/tests/test_rif.py +254 -0
- lethe_memory-0.2.0/tests/test_union_store.py +138 -0
- lethe_memory-0.2.0/tests/test_vectors.py +124 -0
- lethe_memory-0.2.0/uv.lock +3580 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lethe-plugins",
|
|
3
|
+
"owner": { "name": "Teimur Gasanov" },
|
|
4
|
+
"metadata": {
|
|
5
|
+
"description": "Self-improving memory for Claude Code — markdown-first, hybrid retrieval, RIF, optional LLM enrichment",
|
|
6
|
+
"version": "0.1.0"
|
|
7
|
+
},
|
|
8
|
+
"plugins": [
|
|
9
|
+
{
|
|
10
|
+
"name": "lethe",
|
|
11
|
+
"description": "Persistent memory across Claude Code sessions — hybrid BM25+dense retrieval, clustered retrieval-induced forgetting, optional Haiku enrichment.",
|
|
12
|
+
"version": "0.1.0",
|
|
13
|
+
"source": "./plugins/claude-code",
|
|
14
|
+
"author": { "name": "Teimur Gasanov" },
|
|
15
|
+
"homepage": "https://github.com/teimurjan/lethe",
|
|
16
|
+
"repository": "https://github.com/teimurjan/lethe",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"keywords": ["memory", "rif", "hybrid-retrieval", "markdown", "longmemeval"]
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: release-please
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
pull-requests: write
|
|
11
|
+
id-token: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
release-please:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
outputs:
|
|
17
|
+
release_created: ${{ steps.release.outputs.release_created }}
|
|
18
|
+
steps:
|
|
19
|
+
- id: release
|
|
20
|
+
uses: googleapis/release-please-action@v4
|
|
21
|
+
with:
|
|
22
|
+
config-file: release-please-config.json
|
|
23
|
+
manifest-file: .release-please-manifest.json
|
|
24
|
+
|
|
25
|
+
publish:
|
|
26
|
+
needs: release-please
|
|
27
|
+
if: needs.release-please.outputs.release_created == 'true' || github.event_name == 'workflow_dispatch'
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
environment:
|
|
30
|
+
name: pypi
|
|
31
|
+
url: https://pypi.org/p/lethe-memory
|
|
32
|
+
permissions:
|
|
33
|
+
id-token: write
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v4
|
|
36
|
+
- uses: actions/setup-python@v5
|
|
37
|
+
with:
|
|
38
|
+
python-version: "3.12"
|
|
39
|
+
- run: python -m pip install --upgrade build
|
|
40
|
+
- run: python -m build
|
|
41
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Benchmarks
|
|
2
|
+
|
|
3
|
+
Canonical results for `lethe`. For the narrative (why each experiment was run, what failed, how we got here), see [RESEARCH_JOURNEY.md](RESEARCH_JOURNEY.md).
|
|
4
|
+
|
|
5
|
+
## Methodology
|
|
6
|
+
|
|
7
|
+
### Dataset
|
|
8
|
+
- **LongMemEval S variant** via HuggingFace `xiaowu0162/longmemeval-cleaned`.
|
|
9
|
+
- **199,509 conversation turns** (the full "haystack") + **500 evaluation questions** with 1–3 relevant turns each, annotated via qrels.
|
|
10
|
+
|
|
11
|
+
### Retrieval setup
|
|
12
|
+
- Global corpus — all 200k turns indexed together, no per-query pre-filtering.
|
|
13
|
+
- Turn-level retrieval (not session-level).
|
|
14
|
+
- Return top-10 results per query.
|
|
15
|
+
|
|
16
|
+
### Primary metric — NDCG@10
|
|
17
|
+
Position-sensitive; penalizes putting the right turn at rank 8 vs rank 1. Ideal DCG computed from qrels.
|
|
18
|
+
|
|
19
|
+
### Supporting metrics
|
|
20
|
+
- **Recall@k** (k ∈ {5, 10, 30}) — fraction of qrels hits appearing in top-k.
|
|
21
|
+
- **exact_episode** — top-1 ∈ qrels (precision@1 by another name).
|
|
22
|
+
- **sibling_confusion** — top-1 is from an answer-session but not in qrels (right topic, wrong turn).
|
|
23
|
+
- **wrong_family** — top-1 is from a session with no relevant turns (unrelated topic).
|
|
24
|
+
- **stale_fact** (knowledge-update only) — top-1 from answer-session but not in qrels (proxy for older version of the fact).
|
|
25
|
+
- **abstain@T** — fraction where top-1 xenc score < T (would trigger abstention).
|
|
26
|
+
|
|
27
|
+
### Burn-in protocol for RIF results
|
|
28
|
+
RIF is a learned mechanism — needs usage history to accumulate suppression. All RIF numbers below use a **5000-step burn-in** (query schedule: 70% hot queries drawn from a 20% subset, 30% cold queries) before the eval pass.
|
|
29
|
+
|
|
30
|
+
### Integrity checks
|
|
31
|
+
- All systems evaluated on identical queries with identical qrels.
|
|
32
|
+
- All systems return exactly 10 results to the scorer.
|
|
33
|
+
- No hyperparameters tuned on the eval set.
|
|
34
|
+
- RRF vs our hybrid merge produce identical numbers on the baseline — confirmed the 0.3680 number is from the cross-encoder, not from the merge strategy (see checkpoint 10).
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Results
|
|
39
|
+
|
|
40
|
+
### Retrieval baselines
|
|
41
|
+
|
|
42
|
+
200-query eval sample, checkpoint 10 audit:
|
|
43
|
+
|
|
44
|
+
| System | NDCG@10 | Recall@10 | Notes |
|
|
45
|
+
|--------|---------|-----------|-------|
|
|
46
|
+
| Vector only (MiniLM top-10) | 0.1376 | 0.2173 | Bi-encoder baseline |
|
|
47
|
+
| BM25 only (top-10) | 0.2420 | 0.3264 | Sparse keyword baseline |
|
|
48
|
+
| Hybrid BM25+vector RRF (memsearch style) | 0.2171 | 0.3334 | Rank fusion, no reranker |
|
|
49
|
+
| Vector + cross-encoder rerank (k=30) | 0.2425 | 0.2892 | Dense + reranker |
|
|
50
|
+
| **Hybrid + cross-encoder rerank (k=30)** | **0.3680** | **0.4694** | **Best without any learned component** |
|
|
51
|
+
|
|
52
|
+
500-query full eval (the default for all Phase 2+ results):
|
|
53
|
+
|
|
54
|
+
| System | NDCG@10 | Recall@30 |
|
|
55
|
+
|--------|---------|-----------|
|
|
56
|
+
| Baseline (hybrid + xenc, no RIF) | 0.2960 | 0.4103 |
|
|
57
|
+
|
|
58
|
+
### Retrieval-induced forgetting (RIF)
|
|
59
|
+
|
|
60
|
+
Checkpoints 11–13. All on 500-query full eval, 5000-step burn-in.
|
|
61
|
+
|
|
62
|
+
| Config | NDCG@10 | Recall@30 | Notes |
|
|
63
|
+
|--------|---------|-----------|-------|
|
|
64
|
+
| Global RIF (original formula) | 0.2993 (+1.1%) | 0.4142 (+0.9%) | Cue-independent; conservative is optimal |
|
|
65
|
+
| Global RIF (gap formula) | 0.3037 (+2.6%) | 0.4250 (+3.6%) | Gap formula alone is 2.4× better than original |
|
|
66
|
+
| Clustered RIF 30 (original) | 0.3132 (+5.8%) | 0.4381 (+6.8%) | K-means 30 query clusters, per-cluster suppression |
|
|
67
|
+
| **Clustered RIF 30 + gap formula** | **0.3152 (+6.5%)** | **0.4494 (+9.5%)** | **Best retrieval-only configuration** |
|
|
68
|
+
|
|
69
|
+
Default config for the best row: `alpha=0.3`, `suppression_rate=0.1`, `reinforcement_rate=0.05`, `decay_lambda=0.005`, `n_clusters=30`, `use_rank_gap=True`.
|
|
70
|
+
|
|
71
|
+
**Mechanism**: entries that make the candidate pool but lose to the cross-encoder accumulate a per-cluster suppression score. On subsequent retrievals in the same query cluster, suppression penalizes RRF scores before the cross-encoder sees them.
|
|
72
|
+
- **Clustered**: `{entry_id → {cluster_id → float}}` instead of `{entry_id → float}`. An entry suppressed for "travel" queries stays available for "food" queries.
|
|
73
|
+
- **Gap formula**: `max(0, xenc_rank − initial_rank) / pool × sigmoid(−xenc)`. Only penalizes entries that actually dropped in rank AND were actively rejected.
|
|
74
|
+
|
|
75
|
+
### Extended behavior metrics (checkpoint 16)
|
|
76
|
+
|
|
77
|
+
Decomposes the +6.5% NDCG gain into behavior-level changes. 500-query eval.
|
|
78
|
+
|
|
79
|
+
| Metric | baseline | clustered+gap RIF | Δ |
|
|
80
|
+
|--------|----------|-------------------|---|
|
|
81
|
+
| exact_episode | 0.208 | 0.222 | +0.014 |
|
|
82
|
+
| ndcg@10 | 0.293 | 0.316 | +0.023 |
|
|
83
|
+
| **wrong_family** | **0.688** | **0.672** | **−0.016** |
|
|
84
|
+
| sibling_confusion | 0.104 | 0.106 | +0.002 |
|
|
85
|
+
| stale_fact (knowledge-update only) | 0.205 | 0.205 | 0.000 |
|
|
86
|
+
| abstain@0 | 0.180 | 0.166 | −0.014 |
|
|
87
|
+
| abstain@2 | 0.324 | 0.310 | −0.014 |
|
|
88
|
+
| abstain@4 | 0.512 | 0.506 | −0.006 |
|
|
89
|
+
|
|
90
|
+
**Read**: the NDCG gain comes primarily from **reducing cross-topic retrieval** (−1.6pp `wrong_family`). RIF has no effect on within-session discrimination (`sibling_confusion`) or temporal awareness (`stale_fact`).
|
|
91
|
+
|
|
92
|
+
### LLM enrichment layer (checkpoint 17)
|
|
93
|
+
|
|
94
|
+
Write-time structured extraction via `claude-haiku-4-5`: gist, 3 anticipated queries, entities, temporal markers. Indexed alongside original text in BM25/vector. Cross-encoder still scores original text.
|
|
95
|
+
|
|
96
|
+
Coverage: 975 entries enriched (15% of queries have ≥1 enriched qrels entry). Partial-coverage result — overall numbers are diluted by the 85% of uncovered queries.
|
|
97
|
+
|
|
98
|
+
**Overall (500 queries):**
|
|
99
|
+
|
|
100
|
+
| Metric | baseline | RIF | RIF + enrichment | Δ(enr−RIF) |
|
|
101
|
+
|--------|----------|-----|------------------|-----------|
|
|
102
|
+
| exact_episode | 0.208 | 0.218 | 0.220 | +0.002 |
|
|
103
|
+
| ndcg@10 | 0.293 | 0.312 | 0.324 | +0.012 |
|
|
104
|
+
| wrong_family | 0.688 | 0.676 | 0.674 | −0.002 |
|
|
105
|
+
|
|
106
|
+
**Covered bucket (75 queries — where the mechanism applies):**
|
|
107
|
+
|
|
108
|
+
| Metric | baseline | RIF | RIF + enrichment | Δ(enr−RIF) |
|
|
109
|
+
|--------|----------|-----|------------------|-----------|
|
|
110
|
+
| exact_episode | 0.267 | 0.280 | **0.333** | **+0.053 (+19% rel)** |
|
|
111
|
+
| ndcg@10 | 0.350 | 0.390 | **0.473** | **+0.083 (+21% rel)** |
|
|
112
|
+
| wrong_family | 0.720 | 0.707 | **0.653** | **−0.053** |
|
|
113
|
+
| sibling_confusion | 0.013 | 0.013 | 0.013 | 0 |
|
|
114
|
+
| abstain@4 | 0.280 | 0.253 | 0.227 | −0.027 (more confident) |
|
|
115
|
+
|
|
116
|
+
The covered-bucket +8.3pp NDCG gain is **3.6× larger than RIF's own contribution** — the largest single-lever improvement in the project. Mechanism: anticipated_queries reduces vocabulary mismatch, so correct sessions reach the candidate pool more often.
|
|
117
|
+
|
|
118
|
+
**Cost**: $1.60 for 1000 entries on Haiku; system prompt is below the 4096-token cache minimum, so no prompt caching. Projected ~$16 for full coverage of the ~10k answer-relevant entries.
|
|
119
|
+
|
|
120
|
+
### What doesn't work (negative results)
|
|
121
|
+
|
|
122
|
+
Summarized here; full numbers in [RESEARCH_JOURNEY.md](RESEARCH_JOURNEY.md).
|
|
123
|
+
|
|
124
|
+
| Approach | Phase | Result |
|
|
125
|
+
|----------|-------|--------|
|
|
126
|
+
| Embedding mutation (direct Gaussian, adapter, MLP) | 1 | No gain or negative on retrieval quality |
|
|
127
|
+
| Text segmentation | 1 | −46% NDCG |
|
|
128
|
+
| Co-relevance graph | 1 | −2.7% on LongMemEval (too sparse) |
|
|
129
|
+
| GC routing index | 1 | −11% |
|
|
130
|
+
| Exploration + rescue list (top-K injection) | 2 | −2.6pp at scale (looked positive on small eval, variance) |
|
|
131
|
+
| Sparse Distributed Memory (binary LSH + cleanup) | 3 | FAISS wins outright on synthetic episodic data |
|
|
132
|
+
| Iterative cleanup on SDM | 3 | Drifts to sibling prototypes — ≈3× precision drop |
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Benchmark methodology note (comparison with other tools)
|
|
137
|
+
|
|
138
|
+
All numbers on this page measure **full-corpus NDCG@10 with 200k-turn needle-in-haystack retrieval**. This is deliberately close to production — in a real deployed memory, you don't know upfront which sessions are relevant to a query.
|
|
139
|
+
|
|
140
|
+
Other memory-tool benchmarks commonly report **recall@5 over per-query fresh DBs of ~50 sessions at session granularity** — a ~2000× easier task (random baseline 10% vs 0.005%). Some implementations additionally leak ground-truth signal at indexing time (e.g. filtering assistant turns on `has_answer=true`, which is a dataset annotation). Published numbers in the 95-99% range on that methodology are state-of-the-art *for that methodology*; they are not comparable to numbers here.
|
|
141
|
+
|
|
142
|
+
Running either side under a shared methodology is a separate experiment that hasn't been done.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## How to reproduce
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Prep data
|
|
150
|
+
uv run python experiments/data_prep.py --dataset longmemeval
|
|
151
|
+
|
|
152
|
+
# Retrieval-only
|
|
153
|
+
uv run python benchmarks/run_benchmark.py # checkpoint 10 baselines
|
|
154
|
+
uv run python benchmarks/run_rif_benchmark.py # checkpoint 11 (global RIF)
|
|
155
|
+
uv run python benchmarks/run_rif_clustered.py # checkpoint 12
|
|
156
|
+
uv run python benchmarks/run_rif_gap.py # checkpoint 13 (best retrieval-only)
|
|
157
|
+
uv run python benchmarks/run_rif_extended_metrics.py # checkpoint 16
|
|
158
|
+
|
|
159
|
+
# LLM enrichment (needs ANTHROPIC_API_KEY)
|
|
160
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
161
|
+
uv run python experiments/enrich_longmemeval.py # ~$16 for 10k entries
|
|
162
|
+
uv run python benchmarks/run_rif_enriched.py # checkpoint 17 (3-arm)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Raw per-run outputs from each benchmark script are checked into `benchmarks/results/` for auditability.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.0](https://github.com/teimurjan/lethe/compare/lethe-memory-v0.1.0...lethe-memory-v0.2.0) (2026-04-17)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* Add analysis script with plots and summary table ([b1e140b](https://github.com/teimurjan/lethe/commit/b1e140b93a4df4ebd32e483c779b7a8ea093fde5))
|
|
9
|
+
* Add BM25 hybrid retrieval, +73% over static on LongMemEval ([7dd3320](https://github.com/teimurjan/lethe/commit/7dd33206804039d6076481e2b8aaff259a314caf))
|
|
10
|
+
* Add data preparation script for NFCorpus ([a47ab4b](https://github.com/teimurjan/lethe/commit/a47ab4b793d62b613ef702e1e59207d70998d711))
|
|
11
|
+
* Add health metrics (diversity, anchor drift, NDCG, recall) ([e0d13eb](https://github.com/teimurjan/lethe/commit/e0d13eb6a734a31b8760b937e4c621efc09076bd))
|
|
12
|
+
* Add MemoryEntry dataclass and Tier enum ([c021bfb](https://github.com/teimurjan/lethe/commit/c021bfb7c6f6c42473c1cb80c7a5f4cf957fc942))
|
|
13
|
+
* Add Static and RandomMutation baseline stores ([f20803f](https://github.com/teimurjan/lethe/commit/f20803f83598648db13e3799f0beeba34b3df421))
|
|
14
|
+
* Clustered RIF, +5.8% NDCG — cue-dependent suppression 5x better than global ([cbfe8c3](https://github.com/teimurjan/lethe/commit/cbfe8c3e32b099eda3589656c638f09e66f37db1))
|
|
15
|
+
* Enrichment layer validated on covered queries — +8.3pp NDCG over RIF ([eb4e551](https://github.com/teimurjan/lethe/commit/eb4e5513f6460aca8309b35ff94ff86ca908d86d))
|
|
16
|
+
* Exploration + rescue list with smart top-K injection ([a828402](https://github.com/teimurjan/lethe/commit/a828402744f63e0bad8a64f56f261586873bef6d))
|
|
17
|
+
* Implement experiment runner with circuit breakers ([4b6c4dd](https://github.com/teimurjan/lethe/commit/4b6c4dd289deb0832d8c803fde3c3df6b2e6f2cb))
|
|
18
|
+
* Implement GCMemoryStore with FAISS retrieval and GC loop ([9fc0544](https://github.com/teimurjan/lethe/commit/9fc05440033d1a8729d6704aa95fba0695d82442))
|
|
19
|
+
* Implement mutation and selection with unit tests ([a731564](https://github.com/teimurjan/lethe/commit/a731564f506adb761543fb0f922e65d1762f210f))
|
|
20
|
+
* Lifecycle benchmark confirms GC tiers don't help retrieval ([2bc0df3](https://github.com/teimurjan/lethe/commit/2bc0df3c5845ec2638daa3914f59055464ff9bab))
|
|
21
|
+
* LLM enrichment layer for checkpoint 13 (pipeline, not yet run) ([d9676ca](https://github.com/teimurjan/lethe/commit/d9676ca36193dc1fdfbd63e31ad1e1d4fe09d35a))
|
|
22
|
+
* MLP with per-step delta cap shows first positive result ([fbd70fb](https://github.com/teimurjan/lethe/commit/fbd70fb9089e91eddc682bcbf222afccaa3208ae))
|
|
23
|
+
* Production MemoryStore API with SQLite + FAISS + BM25 ([82f7076](https://github.com/teimurjan/lethe/commit/82f7076f23cc80e364f50def38340614a0ea90d3))
|
|
24
|
+
* Rank-gap competition formula, clustered+gap hits +6.5% NDCG / +9.5% recall ([784bd59](https://github.com/teimurjan/lethe/commit/784bd597c81cbb936ad2e9c0b46c751d55bacca3))
|
|
25
|
+
* Rescue index gives +12.9% hot NDCG on LongMemEval ([231e06c](https://github.com/teimurjan/lethe/commit/231e06c9eed988d6807452133450e79a82306941))
|
|
26
|
+
* Retrieval-induced forgetting, first positive learned mechanism (+2.0% NDCG) ([3bb484f](https://github.com/teimurjan/lethe/commit/3bb484f8240bd8f356e79009c4e7385eb20af776))
|
|
27
|
+
* Scaffold repo with pyproject.toml, config, and README ([f278435](https://github.com/teimurjan/lethe/commit/f27843593e8a6bcbd322da74a3ad1dd6fcf4aef2))
|
|
28
|
+
* Ship lethe as a Claude Code plugin ([87f1a40](https://github.com/teimurjan/lethe/commit/87f1a403e10d2549d1380e9cef83c138a9eeb4bf))
|
|
29
|
+
* Sparse Distributed Memory (SDM) research prototype ([7056b6c](https://github.com/teimurjan/lethe/commit/7056b6c2cb050fd590803e3f766b97bfc69c403a))
|
|
30
|
+
* Stabilize MLP with lower promotion thresholds ([2798a4a](https://github.com/teimurjan/lethe/commit/2798a4afa044e98e566eac1d2bab41af18fca3ca))
|
|
31
|
+
* Switch benchmark from NFCorpus/BEIR to LongMemEval ([b4a7194](https://github.com/teimurjan/lethe/commit/b4a7194e6cb924e6e6e82863097d7f5462fedd43))
|
|
32
|
+
* V3 cross-encoder guided adapter mutation ([019bb1f](https://github.com/teimurjan/lethe/commit/019bb1f55ea1c6c62c0c8692b014c591d015c187))
|
|
33
|
+
* V4 learned MLP adapter + segmentation mutation ([ef9a9c6](https://github.com/teimurjan/lethe/commit/ef9a9c649d8cf3a738ab06c1c093542adf34719f))
|
|
34
|
+
* V4 results on both datasets + MLP selection fix ([aa4f333](https://github.com/teimurjan/lethe/commit/aa4f33377c60b526ad90de74c915821ec80e99ac))
|
|
35
|
+
* Wire clustered RIF into production, add video demo ([d2b728b](https://github.com/teimurjan/lethe/commit/d2b728b6b90bbf27326e2e705cca0c14ce49d687))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
### Bug Fixes
|
|
39
|
+
|
|
40
|
+
* Average NDCG@10 over all eval queries, not single sample ([413943e](https://github.com/teimurjan/lethe/commit/413943e7b9a567a765edc6729c8626d4ad35012d))
|
|
41
|
+
* Enrichment rate-limiting — default concurrency 20→5, max_retries 4→8 ([2c04558](https://github.com/teimurjan/lethe/commit/2c045582ffe17c97d567d74fc12008d9353bfc00))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
### Documentation
|
|
45
|
+
|
|
46
|
+
* Checkpoint 14 negative result — retrieval-only ceiling reached ([ca6fb06](https://github.com/teimurjan/lethe/commit/ca6fb063d8b4fdcc0d6385c84bc478b9a77510a4))
|
|
47
|
+
* Checkpoint 15 — SDM prototype, negative result ([d657de4](https://github.com/teimurjan/lethe/commit/d657de4ee8a93e7c4be55698d2698171c207b7f8))
|
|
48
|
+
* Checkpoints 16-17 + benchmark-methodology disclaimer ([082b0c6](https://github.com/teimurjan/lethe/commit/082b0c6380f872ac0e2a60399f58b817a41f96db))
|
|
49
|
+
* Restructure — move result snapshots into benchmarks/results, rewrite BENCHMARKS and RESEARCH_JOURNEY ([1373264](https://github.com/teimurjan/lethe/commit/13732649ae27a8f42673d9c8573665f1002c3aa2))
|
|
50
|
+
* Rewrite README for checkpoints 13+17 and methodology note ([7637ce2](https://github.com/teimurjan/lethe/commit/7637ce2a00c59d6fd1cbd883c154976877ea46bc))
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# lethe
|
|
2
|
+
|
|
3
|
+
Self-improving memory store for LLM agents. BM25 + dense vector hybrid retrieval with cross-encoder reranking, clustered retrieval-induced forgetting, and optional LLM write-time enrichment.
|
|
4
|
+
|
|
5
|
+
Ships as a **Claude Code plugin** (`plugins/claude-code/`) that drops a `.lethe/` directory into each project, writes daily markdown memory via hooks, and surfaces prior-session context through a `memory-recall` skill.
|
|
6
|
+
|
|
7
|
+
## stack
|
|
8
|
+
|
|
9
|
+
- Python 3.12, managed with `uv`
|
|
10
|
+
- FAISS (faiss-cpu) for dense vector index
|
|
11
|
+
- rank_bm25 for sparse keyword index
|
|
12
|
+
- sentence-transformers: all-MiniLM-L6-v2 (bi-encoder), ms-marco-MiniLM-L-6-v2 (cross-encoder)
|
|
13
|
+
- SQLite for metadata persistence
|
|
14
|
+
- numpy for vectors, matplotlib for plots, pytest for tests
|
|
15
|
+
- LongMemEval (S variant) + NFCorpus for benchmarks
|
|
16
|
+
|
|
17
|
+
## layout
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
src/lethe/ # Production library (162 tests, ~95% coverage)
|
|
21
|
+
├── memory_store.py # Main API: MemoryStore
|
|
22
|
+
├── markdown_store.py # Markdown chunker (##/### headings, content hashes)
|
|
23
|
+
├── cli.py # `lethe` CLI: index/search/expand/status/config/reset/enrich/projects
|
|
24
|
+
├── union_store.py # Read-only cross-project search via DuckDB ATTACH
|
|
25
|
+
├── db.py # DuckDB persistence (lethe.duckdb, unlimited ATTACH)
|
|
26
|
+
├── vectors.py # FAISS + BM25 index management
|
|
27
|
+
├── reranker.py # Cross-encoder + adaptive depth
|
|
28
|
+
├── rif.py # Retrieval-Induced Forgetting (clustered + gap)
|
|
29
|
+
├── enrichment.py # Optional LLM write-time enrichment (Anthropic SDK)
|
|
30
|
+
├── dedup.py # Hash + cosine deduplication
|
|
31
|
+
├── encoders.py # ONNX bi-encoder + cross-encoder adapters (fastembed)
|
|
32
|
+
├── _lock.py # fcntl-based per-project lock for concurrent CLI calls
|
|
33
|
+
├── _registry.py # ~/.lethe/projects.json for cross-project search
|
|
34
|
+
└── entry.py # MemoryEntry dataclass + Tier enum
|
|
35
|
+
|
|
36
|
+
plugins/claude-code/ # Claude Code plugin
|
|
37
|
+
├── hooks/ # SessionStart / UserPromptSubmit / Stop / SessionEnd
|
|
38
|
+
├── scripts/ # transcript.py + derive-collection.sh helpers
|
|
39
|
+
└── skills/memory-recall/
|
|
40
|
+
|
|
41
|
+
.claude-plugin/ # Marketplace manifest (for `/plugin marketplace add`)
|
|
42
|
+
|
|
43
|
+
benchmarks/ # Per-checkpoint benchmark scripts
|
|
44
|
+
├── run_*.py
|
|
45
|
+
├── _lib/ # Benchmark-only helpers (metrics, NDCG/recall)
|
|
46
|
+
└── results/ # Raw per-run output markdowns
|
|
47
|
+
|
|
48
|
+
scripts/ # Reproducibility utilities (dataset prep, enrichment runner)
|
|
49
|
+
|
|
50
|
+
research/ # Experimental / non-production code
|
|
51
|
+
├── gc_mutation/ # Germinal-center mutation thread (checkpoints 1-10)
|
|
52
|
+
└── sdm/ # Sparse Distributed Memory prototype (checkpoint 15)
|
|
53
|
+
|
|
54
|
+
tests/ # Production unit tests
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## commands
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
uv venv --python 3.12 && uv pip install -e .
|
|
61
|
+
uv run pytest tests/ -v
|
|
62
|
+
uv run python experiments/data_prep.py --dataset longmemeval
|
|
63
|
+
uv run python benchmarks/run_benchmark.py
|
|
64
|
+
|
|
65
|
+
# CLI (exposed as console script `lethe`)
|
|
66
|
+
lethe index # reindex .lethe/memory (auto-registers project)
|
|
67
|
+
lethe search "query" --top-k 5 # single-project
|
|
68
|
+
lethe search "query" --all --top-k 5 # all registered projects (DuckDB ATTACH)
|
|
69
|
+
lethe projects list|add|remove|prune # manage ~/.lethe/projects.json
|
|
70
|
+
lethe expand <chunk-id>
|
|
71
|
+
lethe status
|
|
72
|
+
|
|
73
|
+
# Run CLI without local install
|
|
74
|
+
uvx --from git+https://github.com/teimurjan/lethe lethe --version
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## key architecture decisions
|
|
78
|
+
|
|
79
|
+
- BM25 + FAISS hybrid retrieval (BM25 is the strongest single signal on conversation data)
|
|
80
|
+
- Cross-encoder reranking on the merged candidate pool
|
|
81
|
+
- Adaptive search depth: shallow k=30 for confident queries, deep k=200 when unsure
|
|
82
|
+
- Cosine 0.95 dedup on add (removes 4.6% of LongMemEval, +6.5% NDCG)
|
|
83
|
+
- Tier lifecycle: naive → gc → memory (with decay and apoptosis)
|
|
84
|
+
- RIF: retrieval-induced forgetting suppresses chronic false positives at candidate selection stage
|
|
85
|
+
- SQLite + .npz + FAISS for persistence (no external services)
|
|
86
|
+
|
|
87
|
+
## benchmark results
|
|
88
|
+
|
|
89
|
+
LongMemEval S (200k turns, 500 questions, 200-query eval sample):
|
|
90
|
+
|
|
91
|
+
| System | NDCG@10 |
|
|
92
|
+
|--------|---------|
|
|
93
|
+
| Vector only | 0.1376 |
|
|
94
|
+
| BM25 only | 0.2420 |
|
|
95
|
+
| Hybrid RRF (memsearch style) | 0.2171 |
|
|
96
|
+
| **Hybrid + cross-encoder rerank** | **0.3680** |
|
|
97
|
+
|
|
98
|
+
The 0.3680 is from BM25+vector+cross-encoder reranking (standard IR). The GC mechanism does not improve this number. Verified with integrity checks.
|
|
99
|
+
|
|
100
|
+
## research status
|
|
101
|
+
|
|
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
|
+
|
|
104
|
+
Full research journey: [RESEARCH_JOURNEY.md](RESEARCH_JOURNEY.md)
|
|
@@ -0,0 +1,21 @@
|
|
|
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'
|