lean-memory 0.1.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.
Files changed (87) hide show
  1. lean_memory-0.1.0/.github/workflows/release.yml +44 -0
  2. lean_memory-0.1.0/.github/workflows/test.yml +43 -0
  3. lean_memory-0.1.0/.gitignore +10 -0
  4. lean_memory-0.1.0/ARCHITECTURE.md +208 -0
  5. lean_memory-0.1.0/CHANGELOG.md +57 -0
  6. lean_memory-0.1.0/CLAUDE.md +58 -0
  7. lean_memory-0.1.0/PERFORMANCE.md +3 -0
  8. lean_memory-0.1.0/PKG-INFO +228 -0
  9. lean_memory-0.1.0/README.md +184 -0
  10. lean_memory-0.1.0/bench/bet2_ablation.py +897 -0
  11. lean_memory-0.1.0/bench/bet2_goldset.py +1057 -0
  12. lean_memory-0.1.0/bench/phase2_escalation_probe.py +204 -0
  13. lean_memory-0.1.0/bench/phase2_eval.py +329 -0
  14. lean_memory-0.1.0/bench/phase2_ingest.py +470 -0
  15. lean_memory-0.1.0/bench/phase2_judge.py +232 -0
  16. lean_memory-0.1.0/bench/phase2_reader.py +114 -0
  17. lean_memory-0.1.0/bench/results/calibration/2026-07-bet2-revalidation.txt +59 -0
  18. lean_memory-0.1.0/bench/results/calibration/2026-07-escalation-baseline.json +511 -0
  19. lean_memory-0.1.0/bench/results/calibration/2026-07-escalation-postdrop-p1.json +22 -0
  20. lean_memory-0.1.0/bench/results/calibration/2026-07-escalation-postdrop-p2.json +24 -0
  21. lean_memory-0.1.0/bench/results/calibration/2026-07-escalation-postdrop-p3.json +23 -0
  22. lean_memory-0.1.0/bench/results/calibration/2026-07-escalation-postdrop-p4.json +23 -0
  23. lean_memory-0.1.0/bench/results/calibration/2026-07-escalation-postfix.json +631 -0
  24. lean_memory-0.1.0/bench/results/calibration/2026-07-granularity-sweep.json +95 -0
  25. lean_memory-0.1.0/bench/results/calibration/README.md +200 -0
  26. lean_memory-0.1.0/bench/results/phase2/.gitkeep +0 -0
  27. lean_memory-0.1.0/bench/results/phase2/dataset_pins.json +5 -0
  28. lean_memory-0.1.0/bench/smoke_quality.py +101 -0
  29. lean_memory-0.1.0/docs/assets/quickstart.gif +0 -0
  30. lean_memory-0.1.0/docs/assets/quickstart.tape +25 -0
  31. lean_memory-0.1.0/docs/benchmarks.md +191 -0
  32. lean_memory-0.1.0/docs/competitive-landscape.md +212 -0
  33. lean_memory-0.1.0/docs/launch-fixlist.md +97 -0
  34. lean_memory-0.1.0/docs/phase2-eval-plan.md +317 -0
  35. lean_memory-0.1.0/docs/phase2-learnings.md +130 -0
  36. lean_memory-0.1.0/docs/superpowers/phase2-HANDOFF.md +161 -0
  37. lean_memory-0.1.0/docs/superpowers/plans/2026-06-21-demo-agent.md +745 -0
  38. lean_memory-0.1.0/docs/superpowers/plans/2026-06-21-mcp-server.md +488 -0
  39. lean_memory-0.1.0/docs/superpowers/plans/2026-07-02-phase2-eval-harness.md +2201 -0
  40. lean_memory-0.1.0/docs/superpowers/plans/2026-07-08-launch-quality-gate.md +1062 -0
  41. lean_memory-0.1.0/docs/superpowers/specs/2026-07-02-phase2-eval-harness-design.md +249 -0
  42. lean_memory-0.1.0/docs/superpowers/specs/2026-07-08-strategic-direction-design.md +141 -0
  43. lean_memory-0.1.0/docs/superpowers/workpackets.md +414 -0
  44. lean_memory-0.1.0/examples/chat.py +177 -0
  45. lean_memory-0.1.0/examples/mcp_config.json +11 -0
  46. lean_memory-0.1.0/pyproject.toml +93 -0
  47. lean_memory-0.1.0/src/lean_memory/__init__.py +14 -0
  48. lean_memory-0.1.0/src/lean_memory/embed/__init__.py +0 -0
  49. lean_memory-0.1.0/src/lean_memory/embed/base.py +51 -0
  50. lean_memory-0.1.0/src/lean_memory/embed/fake.py +41 -0
  51. lean_memory-0.1.0/src/lean_memory/embed/sentence_transformer.py +65 -0
  52. lean_memory-0.1.0/src/lean_memory/extract/__init__.py +0 -0
  53. lean_memory-0.1.0/src/lean_memory/extract/contradiction.py +329 -0
  54. lean_memory-0.1.0/src/lean_memory/extract/gliner_extractor.py +483 -0
  55. lean_memory-0.1.0/src/lean_memory/extract/llm_typer.py +444 -0
  56. lean_memory-0.1.0/src/lean_memory/extract/router.py +418 -0
  57. lean_memory-0.1.0/src/lean_memory/extract/rules.py +130 -0
  58. lean_memory-0.1.0/src/lean_memory/extract/salience.py +123 -0
  59. lean_memory-0.1.0/src/lean_memory/extract/taxonomy.py +244 -0
  60. lean_memory-0.1.0/src/lean_memory/mcp_server.py +150 -0
  61. lean_memory-0.1.0/src/lean_memory/memory.py +203 -0
  62. lean_memory-0.1.0/src/lean_memory/retrieve/__init__.py +0 -0
  63. lean_memory-0.1.0/src/lean_memory/retrieve/rerank.py +59 -0
  64. lean_memory-0.1.0/src/lean_memory/retrieve/retriever.py +125 -0
  65. lean_memory-0.1.0/src/lean_memory/store/__init__.py +0 -0
  66. lean_memory-0.1.0/src/lean_memory/store/base.py +91 -0
  67. lean_memory-0.1.0/src/lean_memory/store/schema.py +83 -0
  68. lean_memory-0.1.0/src/lean_memory/store/sqlite_store.py +301 -0
  69. lean_memory-0.1.0/src/lean_memory/types.py +107 -0
  70. lean_memory-0.1.0/tests/fixtures/phase2/lme_oracle_mini.json +26 -0
  71. lean_memory-0.1.0/tests/fixtures/phase2/lme_s_mini.json +36 -0
  72. lean_memory-0.1.0/tests/fixtures/phase2/locomo_mini.json +23 -0
  73. lean_memory-0.1.0/tests/test_asof_sparse.py +46 -0
  74. lean_memory-0.1.0/tests/test_chat.py +225 -0
  75. lean_memory-0.1.0/tests/test_contradiction_extends.py +93 -0
  76. lean_memory-0.1.0/tests/test_embedder_default.py +25 -0
  77. lean_memory-0.1.0/tests/test_escalation_probe.py +64 -0
  78. lean_memory-0.1.0/tests/test_known_entities_cap.py +19 -0
  79. lean_memory-0.1.0/tests/test_mcp_server.py +269 -0
  80. lean_memory-0.1.0/tests/test_phase1_extraction.py +106 -0
  81. lean_memory-0.1.0/tests/test_phase2_eval.py +98 -0
  82. lean_memory-0.1.0/tests/test_phase2_ingest.py +212 -0
  83. lean_memory-0.1.0/tests/test_phase2_judge.py +70 -0
  84. lean_memory-0.1.0/tests/test_phase2_reader.py +41 -0
  85. lean_memory-0.1.0/tests/test_router.py +260 -0
  86. lean_memory-0.1.0/tests/test_search_now.py +39 -0
  87. lean_memory-0.1.0/tests/test_spine.py +98 -0
@@ -0,0 +1,44 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ build:
9
+ name: Build sdist + wheel
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.13"
17
+
18
+ - name: Build distributions
19
+ run: |
20
+ pip install build
21
+ python -m build
22
+
23
+ - name: Upload dist artifact
24
+ uses: actions/upload-artifact@v4
25
+ with:
26
+ name: dist
27
+ path: dist/
28
+
29
+ publish:
30
+ name: Publish to PyPI (Trusted Publishing)
31
+ needs: build
32
+ runs-on: ubuntu-latest
33
+ environment: pypi
34
+ permissions:
35
+ id-token: write
36
+ steps:
37
+ - name: Download dist artifact
38
+ uses: actions/download-artifact@v4
39
+ with:
40
+ name: dist
41
+ path: dist/
42
+
43
+ - name: Publish
44
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,43 @@
1
+ name: test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ # Cancel superseded runs on the same ref.
9
+ concurrency:
10
+ group: test-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ test:
15
+ name: ${{ matrix.os }} · py${{ matrix.python-version }}
16
+ runs-on: ${{ matrix.os }}
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ os: [ubuntu-latest, macos-latest]
21
+ python-version: ["3.10", "3.13"]
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ # Provision Python via uv (python-build-standalone). Unlike
26
+ # actions/setup-python, these builds are compiled with loadable-sqlite
27
+ # extensions enabled, which sqlite-vec needs to load — the macOS
28
+ # setup-python builds are not, so extension loading fails there.
29
+ # One code path for both OSes.
30
+ - name: Set up uv + Python ${{ matrix.python-version }}
31
+ uses: astral-sh/setup-uv@v5
32
+ with:
33
+ python-version: ${{ matrix.python-version }}
34
+ enable-cache: true
35
+
36
+ # setup-uv with a python-version input already provisions and activates a
37
+ # venv (it exports VIRTUAL_ENV), so no explicit `uv venv` step is needed —
38
+ # the install below targets that activated environment.
39
+ - name: Install (editable, dev + mcp extras)
40
+ run: uv pip install -e '.[dev,mcp]'
41
+
42
+ - name: Run offline test suite
43
+ run: uv run pytest tests/ -q
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.pyo
4
+ .venv/
5
+ venv/
6
+ *.egg-info/
7
+ dist/
8
+ build/
9
+ .superpowers/
10
+ bench/.phase2_cache/
@@ -0,0 +1,208 @@
1
+ # lean-memory — Architecture & Status
2
+
3
+ Implementation status, design decisions, benchmark results, and known limitations.
4
+
5
+ ---
6
+
7
+ ## Implementation Status
8
+
9
+ ### Phase 0 — Storage & Retrieval
10
+
11
+ | Component | Status | Notes |
12
+ |---|---|---|
13
+ | `Store` interface | ✅ | single abstraction; `SqliteStore` is the only impl |
14
+ | `SqliteStore` (vec0 + FTS5) | ✅ | one file per namespace (per-tenant isolation) |
15
+ | Monotemporal spine | ✅ | `valid_at`/`valid_to` + `is_latest` + ADD-only `superseded_by`; nothing is ever deleted |
16
+ | Two-stage Matryoshka dense | ✅ | coarse 256-dim KNN → exact 768-dim re-score |
17
+ | BM25 sparse arm | ✅ | FTS5 `bm25()` |
18
+ | RRF fusion (k=10) | ✅ | `Σ 1/(10 + rank)` |
19
+ | Mandatory rerank | ✅ | `IdentityReranker` (offline default) / `CrossEncoderReranker` (Ettin-32M) |
20
+ | Salience-decay re-score | ✅ | `0.6·rel + 0.2·recency + 0.2·importance`, `recency = exp(-λ·age)` |
21
+ | as-of temporal query | ✅ | world-time interval predicate |
22
+ | Pluggable embedder | ✅ | `FakeEmbedder` (offline) / `SentenceTransformerEmbedder` (Qwen3-0.6B) |
23
+
24
+ ### Phase 1 — Hybrid Extraction
25
+
26
+ | Component | Status | Notes |
27
+ |---|---|---|
28
+ | Relation taxonomy | ✅ | `asserts`/`supersedes`/`extends`/`derives`; single shared `Candidate` contract |
29
+ | Pass 2 — candidate generation | ✅ | `StubCandidateGenerator` (offline) / `Gliner2Generator` (GLiNER2, `[extract]` extra) |
30
+ | Pass 3 — recall-biased router | ✅ | escalates low-conf / endpoint-scoped coref / possible-`derives` (2026-07 re-freeze: `prior_entity` trigger dropped — see "Escalation recalibration") |
31
+ | Pass 4 — LLM constrained typing | ✅ | `StubTyper` (offline) / `OllamaTyper` (local model, `[llm]` extra) |
32
+ | Contradiction → supersession | ✅ | cheap-then-escalate: slot → cosine → subsumption → LLM |
33
+ | Salience at write | ✅ | deterministic heuristic, rated once + cached on the `Fact` |
34
+ | BET-2 ablation harness | ✅ | `bench/bet2_ablation.py` — **BET-2 PASS**; re-frozen 2026-07 at `(typing=0.4, conf=0.4)` (−0.4pp delta, 7.6% escalation), superseding the 2026-06-21 freeze — see "Escalation recalibration (2026-07)" below |
35
+
36
+ ### Phase 1 — Integrations
37
+
38
+ | Component | Status | Notes |
39
+ |---|---|---|
40
+ | MCP server | ✅ | `memory_add` / `memory_search` / `memory_clear` via FastMCP (stdio transport) |
41
+ | Terminal demo agent | ✅ | `examples/chat.py` — full add→retrieve→supersede loop with Claude |
42
+
43
+ ### Phase 2 — Next
44
+
45
+ | Item | Status |
46
+ |---|---|
47
+ | Public benchmarks (LongMemEval / LoCoMo + frozen judge) | ⬜ deferred (post-launch) — harness complete, see `docs/superpowers/specs/2026-07-08-strategic-direction-design.md` |
48
+ | int8 vector storage | ⬜ blocked — sqlite-vec 0.1.9 insert path is broken upstream |
49
+ | `LanceStore` scale tier | ⬜ |
50
+
51
+ ---
52
+
53
+ ## Measured Performance
54
+
55
+ ### Retrieval Quality (BET-1 evidence)
56
+
57
+ | Configuration | Top-1 | What it proves |
58
+ |---|---|---|
59
+ | Offline stubs (FakeEmbedder + IdentityReranker) | **1/5** | plumbing routes facts end-to-end (random vectors → chance result) |
60
+ | Real (Qwen3-Embedding-0.6B + Ettin-32M), 5-fact set | **4/5** | real models lift quality with zero code changes |
61
+ | Real, clean 3-fact set | **3/3** @ ~0.69 | the one toy-set miss was a small-corpus vocabulary artifact |
62
+
63
+ The pluggable-backend architecture is validated. These are sanity checks on small hand-built sets — a publishable quality claim needs LongMemEval/LoCoMo + a frozen judge (Phase 2).
64
+
65
+ Note: `google/embeddinggemma-300m` is a gated HF repo requiring license-accept. `Qwen3-Embedding-0.6B` is ungated and the stronger retrieval model (MTEB-R 64.65 vs 62.49) — use it instead.
66
+
67
+ ### Extraction Quality — BET-2 (2026-06-21, n=97)
68
+
69
+ > **Superseded by the 2026-07 re-freeze** at operating point
70
+ > `(typing=0.4, conf=0.4)` — see "Escalation recalibration (2026-07)" below.
71
+ > The 2026-06-21 run below stays as the original PASS record; the frozen
72
+ > constants and escalation rate quoted here (`conf 0.5`-era, 10.1%) are historical.
73
+
74
+ **Metric A — Typer (`asserts` vs `derives`):**
75
+
76
+ | Arm | macro-F1 | derives-recall |
77
+ |---|---|---|
78
+ | 100%-LLM (qwen2.5:3b) | 0.474 [0.27, 0.68] | 0.20 |
79
+ | hybrid (StubTyper on direct bucket) | 0.474 [0.27, 0.68] | 0.20 |
80
+
81
+ **Metric B — Resolver (`asserts`/`extends`/`supersedes`) with real embeddings:**
82
+
83
+ | Arm | macro-F1 |
84
+ |---|---|
85
+ | both arms | **0.897** [0.73, 1.00] |
86
+
87
+ **Gate results:**
88
+
89
+ | Gate | Target | Result | Verdict |
90
+ |---|---|---|---|
91
+ | 1 — F1 delta (hybrid vs LLM, direct bucket) | ≤ 3pp | 0.0pp [0, 0] | ✅ PASS |
92
+ | 2 — escalation rate (Wilson upper) | < 20% | 10.1% [5.2%, 18.7%] | ✅ PASS |
93
+ | 3 — hybrid derives-recall not worse | ≥ LLM − 10pp | 0.20 ≥ 0.10 | ✅ PASS |
94
+
95
+ **BET-2 = PASS** (goldset sha256 `350b18b51a97fe57`).
96
+
97
+ ### Escalation recalibration (2026-07)
98
+
99
+ The Phase 2 ingest surfaced that BET-2's <20% escalation gate held only on clean
100
+ goldset sentences: on **real LongMemEval conversational turns** the router routed
101
+ **95.9%** of candidates to the LLM (baseline probe, `bench/results/calibration/2026-07-escalation-baseline.json`;
102
+ best point typing=conf=0.3, 971/1012). Two confidence-independent router floors
103
+ drove it — `coreference` 65.6% and `prior_entity` 54.8% — so no threshold pair
104
+ could reach the target. Both were retired at the router:
105
+
106
+ - **Endpoint-scoped coref/ellipsis** — the pronoun/demonstrative scan narrowed
107
+ from the whole `fact_text` to the predicate endpoints. On real turns
108
+ `coreference` collapsed **664 → 1** (`2026-07-granularity-sweep.json`).
109
+ - **`prior_entity` trigger dropped** (two user-approved amendments: subject-only,
110
+ then removed entirely) — subject re-mention of a known entity is normal
111
+ discourse, not a hard case, and it stayed at 52.8% of real candidates even
112
+ scoped to the subject (`2026-07-escalation-postfix.json`). Entity linking is
113
+ deterministic by name; ambiguous refs still escalate via `coreference`,
114
+ inferential edges via `derives`.
115
+
116
+ Post-drop probe on real turns (8 namespaces / 192 turns / 704 candidates,
117
+ `2026-07-escalation-postdrop-p{1..4}.json`) selected the operating point:
118
+
119
+ | typing | conf | escalated/seen | rate | by_reason |
120
+ |---:|---:|---:|---:|---|
121
+ | **0.40** | **0.40** | **103/704** | **14.6%** | derives 102, coref 1 |
122
+ | 0.50 | 0.50 | 316/704 | 44.9% | pre_flagged 247, low_conf 247, derives 102, coref 1 |
123
+
124
+ Only `(0.4, 0.4)` clears the margin (raising either threshold to 0.5 pulls in the
125
+ 247 candidates with model confidence in [0.4, 0.5)); the residual is
126
+ derives-dominated (the irreducible inferential edges the LLM must own).
127
+
128
+ **Granularity** (`2026-07-granularity-sweep.json`): GLiNER `DEFAULT_THRESHOLD`
129
+ 0.1 → 0.4 cut over-generation **8.43 → 3.67 facts/turn** (decision rule: smallest
130
+ swept threshold meeting facts/turn ≤ 4). The `median_fact_len ≤ 160` target was
131
+ waived (user-approved) as threshold-insensitive — it sits at 171–187 chars across
132
+ the entire sweep, so no GLiNER threshold moves it.
133
+
134
+ **Re-frozen constants:** `DEFAULT_TYPING_THRESHOLD = 0.4`, router
135
+ `conf_threshold = 0.4`, `FROZEN_CONF_THRESHOLD = FROZEN_TYPING_THRESHOLD = 0.4`
136
+ (`bench/bet2_goldset.py`). **BET-2 three-gate revalidation at the frozen point
137
+ (`bench/bet2_ablation.py --real`, full output `2026-07-bet2-revalidation.txt`):**
138
+
139
+ | Gate | Target | Result | Verdict |
140
+ |---|---|---|---|
141
+ | 1 — direct-bucket paired F1 delta | upper ≤ 3.0pp | −0.4pp [−1.2, +0.0]pp | ✅ PASS |
142
+ | 2 — escalation rate (goldset, Wilson upper) | < 20% | 7.6% [3.5%, 15.6%] | ✅ PASS |
143
+ | 3 — hybrid derives-recall not worse | ≥ LLM − 10pp | 0.20 ≥ 0.10 | ✅ PASS |
144
+
145
+ **BET-2 re-freeze = PASS** (all three gates jointly). Metric B resolver macro-F1
146
+ unchanged at **0.897** (the router change touches only the typer/direct path).
147
+
148
+ ---
149
+
150
+ ## Bugs Found by the Benchmark
151
+
152
+ The measurement process found 4 real engine bugs before they shipped.
153
+
154
+ | # | Bug | Fix |
155
+ |---|---|---|
156
+ | 1 | **`extends` unreachable** — every non-identical object mapped to `supersedes` | Additive signal routing: additive cues ("also", multi-valued predicates) → `extends`; functional slots still supersede |
157
+ | 2 | **Resolver thresholds miscalibrated** — `HIGH=0.82`/`LOW=0.55` put Qwen3 refinements in the wrong band | Recalibrated to `HIGH=0.80`/`LOW=0.45`; verified 4/4 on real Qwen3 |
158
+ | 3 | **`OllamaTyper` label-set mismatch** — offering all 4 relations to a typer with no prior-slot context caused qwen2.5:3b to anchor on `extends` for everything | Constrained to `{asserts, derives}` |
159
+ | 4 | **Router `prior_entity` over-escalation** — "user" always in `known_entities` after turn 1, causing 73.7% false escalation | `self_entity` exemption in `_references_prior_entity()`; expanded `_KNOWN_PREDICATES`; gold set rebalanced 37→97 cases — **superseded 2026-07:** the `prior_entity` trigger was dropped entirely after real-turn data showed it still fired on 52.8% of candidates (see "Escalation recalibration (2026-07)") |
160
+
161
+ ---
162
+
163
+ ## Design Decisions
164
+
165
+ ### ADD-only writes / supersession
166
+
167
+ Facts are never deleted or updated in place. A contradicting fact marks the old one `is_latest=False` via a `superseded_by` pointer and inserts the new one. This makes the full history auditable and enables point-in-time queries (`as_of=<timestamp_ms>`).
168
+
169
+ ### Offline-first with pluggable backends
170
+
171
+ Every component has an offline stub that needs zero downloads and produces deterministic output. Real models are opt-in extras. This means the full offline test suite runs in seconds with no network access, and the architecture is validated independently of model availability.
172
+
173
+ ### Cheap-then-escalate contradiction detection
174
+
175
+ The resolver tries slot match → cosine similarity → token subsumption before calling the LLM. Each step is ~100× cheaper than the next. Only genuinely ambiguous cases (high cosine, disjoint tokens) reach the LLM. The BET-2 gate 2 verifies the escalation rate stays under 20%.
176
+
177
+ ### Per-namespace SQLite files
178
+
179
+ Each namespace (e.g. per-user) gets its own SQLite file rather than a shared database with a namespace column. This gives hard write isolation, trivial backup/export (copy one file), and clean multi-tenant semantics with no cross-tenant query risk.
180
+
181
+ ---
182
+
183
+ ## Known Limitations
184
+
185
+ - **No benchmark-grade quality claim.** All numbers are on small hand-built sets. A real claim needs LongMemEval/LoCoMo + a frozen judge (Phase 2, deferred post-launch).
186
+ - **Recency on historical corpora — addressed (2026-07).** `exp(-λ·age)` collapses to ~0 for every fact when historical data is read years later, so the 0.2 recency term was dead weight on such corpora. `Memory.search(now=...)` now forwards an explicit search-time anchor so callers evaluating historical data can restore the recency signal; the wall-clock default is unchanged for live use.
187
+ - **Vectors stored float32, not int8.** sqlite-vec 0.1.9's int8 insert path is broken upstream. Flip when fixed (~0.2pt quality cost per BET-1, no correctness impact).
188
+ - **Inference typing is second-class without `[llm]`.** On the default `[mcp,models,extract]` path there is no Ollama typer, so the ~15% of candidates that escalate — almost entirely inferential (`derives`) facts — fall through to the deterministic `StubTyper` rather than a model. Assertional facts are unaffected; inference-type facts are second-class until you add the `[llm]` extra, which routes that escalated tier through real constrained typing.
189
+ - **Small-model ceiling.** qwen2.5:3b caps Typer accuracy. A larger local model would likely lift derives-recall. Untested.
190
+ - **Single machine, single run** for all real numbers. Reproducible, but not multi-seed/multi-judge as the spec's BET-5 demands.
191
+
192
+ ---
193
+
194
+ ## Reproduce
195
+
196
+ ```bash
197
+ # Offline (full suite, no downloads)
198
+ pip install -e '.[dev]' && pytest -q
199
+
200
+ # Real retrieval quality
201
+ pip install -e '.[models]'
202
+ python bench/smoke_quality.py --real --embedder Qwen/Qwen3-Embedding-0.6B
203
+
204
+ # Real BET-2 ablation
205
+ pip install -e '.[models,extract,llm]'
206
+ ollama serve & ollama pull qwen2.5:3b
207
+ python bench/bet2_ablation.py --real --decodes 3
208
+ ```
@@ -0,0 +1,57 @@
1
+ # Changelog
2
+
3
+ All notable changes to lean-memory are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.1.0] - 2026-07-12
8
+
9
+ First public release. lean-memory is an embedded, local-first agent-memory
10
+ engine: one SQLite file per namespace, hybrid dense+sparse retrieval with
11
+ rerank, and ADD-only supersession queryable at any past point in time
12
+ (`as_of`). No server, no daemon, no mandatory cloud key.
13
+
14
+ ### Added
15
+
16
+ - **MCP server** exposing memory as three tools (`memory_add`, `memory_search`,
17
+ `memory_clear`) for Claude Code, Claude Desktop, and other MCP clients.
18
+ Canonical install `pip install 'lean-memory[mcp,models,extract]'`
19
+ opportunistically upgrades each backend whose extra is present (real embedder
20
+ + reranker via `[models]`, GLiNER2 extraction via `[extract]`) and otherwise
21
+ falls back to deterministic offline stubs. Two-minute quickstart with
22
+ copy-paste Claude Code / Claude Desktop config and a demo GIF.
23
+ - **`Memory.search(now=...)`** — recency decay now anchors to a caller-supplied
24
+ timestamp, so the 0.2 recency term is no longer dead on historical corpora.
25
+ - **Point-in-time queries** via `as_of` (epoch ms) with `is_latest_only=False`.
26
+ - **CI + release workflows** (GitHub Actions): offline test matrix on
27
+ ubuntu/macOS × Python 3.10/3.13, plus build-and-publish to PyPI on `v*` tag
28
+ via Trusted Publishing.
29
+ - PyPI metadata: keywords, classifiers, and project URLs.
30
+
31
+ ### Changed
32
+
33
+ - **Default embedder is now the ungated Qwen3-Embedding-0.6B** (was a gated
34
+ Gemma model that broke the `[models]` first run). Reranker default is
35
+ Ettin-32M; both are pinned ungated and covered by regression tests.
36
+ - **Escalation engine recalibrated on real conversational turns.** Endpoint-
37
+ scoped coreference/ellipsis detection replaces the whole-text pronoun scan
38
+ (coreference escalations dropped from 65.6% to effectively nil on real
39
+ turns), and the `prior_entity` trigger was retired (subject re-mention is
40
+ normal discourse, measured at 52.8% of candidates). At the re-frozen
41
+ `(typing_threshold=0.4, conf_threshold=0.4)` operating point, escalation on
42
+ the real LongMemEval probe is **14.6%** (was ~96% pre-fix), with the residual
43
+ being irreducible inferential-edge (`derives`) escalations. BET-2 three-gate
44
+ revalidation PASSes at this operating point.
45
+ - **Extraction granularity calibrated** — GLiNER candidate threshold set to
46
+ 0.4, cutting the extractor from ~8 facts/turn to ~3.7 so `fact_text` reads as
47
+ facts rather than whole utterances.
48
+ - MCP server loads models lazily (first tool call rather than import) so a
49
+ cold-cache spawn answers the MCP handshake immediately instead of blocking on
50
+ a model download; search output is deduplicated.
51
+
52
+ ### Fixed
53
+
54
+ - Sparse BM25 retrieval arm now honors the `as_of` interval predicate.
55
+ - Known-entities handed to the router/typer are capped at the 100 most recent.
56
+
57
+ [0.1.0]: https://github.com/Wuesteon/lean-memory/releases/tag/v0.1.0
@@ -0,0 +1,58 @@
1
+ # CLAUDE.md
2
+
3
+ ## ⚠️ START HERE: strategy is quality-gate → MCP launch (2026-07-08)
4
+
5
+ Approved direction: **close a small, hard quality gate on the first-run
6
+ experience, then launch across MCP channels** (Pure A). Read before any work:
7
+
8
+ 1. **`docs/superpowers/specs/2026-07-08-strategic-direction-design.md`** — the
9
+ approved strategy (positioning, the six-item quality gate, launch plan, the
10
+ six-week post-launch demand read). Non-negotiable context; do not re-derive.
11
+ 2. **`docs/superpowers/plans/2026-07-08-launch-quality-gate.md`** — the ordered
12
+ implementation plan that executes the gate.
13
+
14
+ State: the former "Phase 2 suspended, fix engine then re-run benchmarks" framing
15
+ is **superseded**. Engine-fix backlog items 1–3 are **FIXED** on `launch-gate`
16
+ (numbers are the source of truth in `bench/results/calibration/README.md`):
17
+
18
+ - **Escalation recalibration** — endpoint-scoped coref + `prior_entity` trigger
19
+ dropped (two user-approved amendments); real-turn escalation **95.9% → 14.6%**
20
+ at the frozen `(typing=0.4, conf=0.4)` operating point; goldset **10.1% → 7.6%**.
21
+ BET-2 revalidation PASSES all three gates.
22
+ - **Extraction granularity** — GLiNER `DEFAULT_THRESHOLD` 0.1 → 0.4;
23
+ **8.43 → 3.67 facts/turn**.
24
+ - **Recency anchoring** — `Memory.search(now=...)` forwards search-time now
25
+ (wall-clock default unchanged).
26
+
27
+ **Benchmark runs (LongMemEval/LoCoMo) are DEFERRED past the MCP launch** per the
28
+ spec — they are a post-launch credibility layer, not the critical path. The
29
+ harness (`bench/phase2_*.py`) is complete and needs no changes for the eventual
30
+ re-run.
31
+
32
+ **Remaining next steps = launch execution** per spec §3 (MCP Registry listing,
33
+ `awesome-mcp-servers` PR, Claude Code plugin marketplace, PyPI polish, Show HN,
34
+ subreddit posts); security housekeeping (rotate the OpenRouter key + HF token,
35
+ merge the harness branch) is gate item 5.
36
+
37
+ Historical context (dated, do not re-derive): `docs/phase2-learnings.md`
38
+ (assumptions vs. reality postmortem) and `docs/superpowers/phase2-HANDOFF.md`
39
+ (operational runbook + the now-fixed engine-fix backlog).
40
+
41
+ ## Project
42
+
43
+ lean-memory: embedded, local-first agent-memory engine (SQLite vec0 + FTS5,
44
+ hybrid retrieval + rerank, ADD-only supersession with a monotemporal spine).
45
+ See `ARCHITECTURE.md` for the phase roadmap and BET results; `README.md` for
46
+ the user-facing quickstart.
47
+
48
+ - Python ≥3.10; dev venv at `.venv` (3.13). Run tests:
49
+ `.venv/bin/python -m pytest tests/ -q` (offline by default — all model
50
+ backends have deterministic stubs).
51
+ - Real model extras are opt-in: `[models]` (embedder+reranker), `[extract]`
52
+ (GLiNER2), `[llm]` (Ollama typer), `[bench]` (OpenRouter client).
53
+ - Benchmarks live in `bench/` (BET-2: `bet2_*.py`; Phase 2: `phase2_*.py`).
54
+ Frozen-config discipline: any number without a pinned config hash, judge
55
+ model, judge prompt, and backbone is not publishable.
56
+ - Roadmap work is clustered into worktree-sized packets with dependencies,
57
+ gates, and file-conflict lanes: `docs/superpowers/workpackets.md`. Claim a
58
+ packet there before starting it; one worktree per packet.
@@ -0,0 +1,3 @@
1
+ # lean-memory — Performance
2
+
3
+ Benchmark results, implementation status, and design decisions have moved to [ARCHITECTURE.md](ARCHITECTURE.md).
@@ -0,0 +1,228 @@
1
+ Metadata-Version: 2.4
2
+ Name: lean-memory
3
+ Version: 0.1.0
4
+ Summary: Embedded, local-first agent memory with hybrid retrieval and ADD-only supersession.
5
+ Project-URL: Homepage, https://github.com/Wuesteon/lean-memory
6
+ Project-URL: Repository, https://github.com/Wuesteon/lean-memory
7
+ Author: lean-memory
8
+ License: Apache-2.0
9
+ Keywords: agent-memory,embeddings,llm,local-first,mcp,memory,rag,retrieval,sqlite,vector-search
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Database :: Database Engines/Servers
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: bm25s>=0.2.0
24
+ Requires-Dist: numpy>=1.24
25
+ Requires-Dist: python-dateutil>=2.8
26
+ Requires-Dist: sqlite-vec>=0.1.6
27
+ Provides-Extra: bench
28
+ Requires-Dist: openai>=1.40; extra == 'bench'
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=8.0; extra == 'dev'
31
+ Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'dev'
32
+ Provides-Extra: examples
33
+ Requires-Dist: anthropic>=0.25; extra == 'examples'
34
+ Provides-Extra: extract
35
+ Requires-Dist: gliner2>=1.3.1; extra == 'extract'
36
+ Provides-Extra: llm
37
+ Requires-Dist: ollama>=0.6.0; extra == 'llm'
38
+ Provides-Extra: mcp
39
+ Requires-Dist: mcp>=1.0; extra == 'mcp'
40
+ Provides-Extra: models
41
+ Requires-Dist: sentence-transformers>=3.0; extra == 'models'
42
+ Requires-Dist: torch>=2.2; extra == 'models'
43
+ Description-Content-Type: text/markdown
44
+
45
+ # lean-memory
46
+
47
+ [![test](https://github.com/Wuesteon/lean-memory/actions/workflows/test.yml/badge.svg)](https://github.com/Wuesteon/lean-memory/actions/workflows/test.yml)
48
+
49
+ Embedded, local-first agent memory. No server, no daemon, no mandatory cloud key.
50
+
51
+ > **Status (2026-07):** working toward the first public launch (MCP-first).
52
+ > Roadmap and rationale: `docs/superpowers/specs/2026-07-08-strategic-direction-design.md`.
53
+ > Public benchmark runs (LongMemEval/LoCoMo) are deferred until after launch;
54
+ > the harness is complete (`bench/phase2_*.py`) and the engine flaws it exposed
55
+ > are fixed on this branch — see `docs/phase2-learnings.md`.
56
+
57
+ ```python
58
+ from lean_memory import Memory
59
+
60
+ mem = Memory(root="./data")
61
+
62
+ mem.add("user-42", "I work at Acme Corp.")
63
+ mem.add("user-42", "I now work at Globex.") # supersedes Acme automatically
64
+
65
+ mem.search("user-42", "where does the user work?") # → "I now work at Globex."
66
+ ```
67
+
68
+ ![lean-memory quickstart](docs/assets/quickstart.gif)
69
+
70
+ Facts are extracted from natural language, stored in a per-namespace SQLite file, and retrieved with hybrid dense+sparse search. Old facts are never deleted — they're superseded and queryable at any past point in time.
71
+
72
+ ## Install
73
+
74
+ ```bash
75
+ pip install lean-memory
76
+ ```
77
+
78
+ Runs fully offline out of the box. Optional extras unlock real model quality:
79
+
80
+ | Extra | What it adds |
81
+ |---|---|
82
+ | `lean-memory[models]` | Real embedder + reranker (Qwen3-0.6B + Ettin-32M) |
83
+ | `lean-memory[extract]` | GLiNER2 candidate generation for richer extraction |
84
+ | `lean-memory[llm]` | Ollama-backed LLM typing pass |
85
+ | `lean-memory[mcp]` | MCP server bridge for Claude Desktop / Claude Code |
86
+ | `lean-memory[examples]` | Terminal demo agent (requires `anthropic` SDK) |
87
+
88
+ ## Quickstart
89
+
90
+ ```python
91
+ from lean_memory import Memory
92
+
93
+ mem = Memory(root="./data") # one SQLite file per namespace, stored under ./data/
94
+
95
+ # Store facts in natural language
96
+ mem.add("alice", "I work at Stripe.")
97
+ mem.add("alice", "I now work at Vercel.") # supersedes Stripe automatically
98
+
99
+ # Retrieve — the superseded Stripe fact drops out; only the current one is returned
100
+ results = mem.search("alice", "what does Alice do for work?", k=3)
101
+ for hit in results:
102
+ print(hit.fact.fact_text, hit.final_score)
103
+ # → I now work at Vercel. 0.89
104
+
105
+ # Point-in-time query — what was true at a specific moment?
106
+ mem.search("alice", "employer", as_of=1_700_000_000_000, is_latest_only=False) # epoch ms
107
+
108
+ # Always close when done (flushes WAL)
109
+ mem.close()
110
+ ```
111
+
112
+ ## Demo Agent
113
+
114
+ A terminal chatbot showing the full memory loop — add, retrieve, supersede, restart:
115
+
116
+ ```bash
117
+ pip install 'lean-memory[examples]'
118
+ export ANTHROPIC_API_KEY=sk-ant-...
119
+ python examples/chat.py # uses offline stubs by default
120
+ python examples/chat.py --namespace bob # separate memory tenant, persists across restarts
121
+ ```
122
+
123
+ No API key? The demo still runs — it echoes the retrieved memory context instead of calling Claude, so you can watch the engine work offline.
124
+
125
+ ## MCP Server — memory for Claude Code / Claude Desktop
126
+
127
+ Give any MCP agent persistent local memory: three tools (`memory_add`,
128
+ `memory_search`, `memory_clear`), one SQLite file per namespace, nothing
129
+ leaves your machine.
130
+
131
+ ```bash
132
+ pip install 'lean-memory[mcp,models,extract]'
133
+ ```
134
+
135
+ > First run downloads three open models (~2.0 GB total: Qwen3-Embedding-0.6B
136
+ > + Ettin-32M reranker for retrieval, plus GLiNER2-base (~0.8 GB) for real
137
+ > extraction — all ungated). Pre-warm once so your MCP client never waits on
138
+ > a download:
139
+ >
140
+ > ```bash
141
+ > python -c "from lean_memory.embed.sentence_transformer import SentenceTransformerEmbedder; \
142
+ > from lean_memory.retrieve.rerank import CrossEncoderReranker; \
143
+ > SentenceTransformerEmbedder().embed_one('warm'); CrossEncoderReranker().score('warm', ['up']); \
144
+ > from lean_memory.extract.gliner_extractor import Gliner2Generator; from lean_memory.types import Episode; \
145
+ > Gliner2Generator().generate(Episode(namespace='w', raw='I work at Acme.', t_ref=0, source='user'))"
146
+ > ```
147
+
148
+ **Claude Code:**
149
+
150
+ ```bash
151
+ claude mcp add lean-memory -- lean-memory-mcp
152
+ ```
153
+
154
+ **Claude Desktop** — add to `mcpServers` (or copy `examples/mcp_config.json`):
155
+
156
+ ```json
157
+ { "lean-memory": { "command": "lean-memory-mcp", "env": { "LM_DATA_ROOT": "~/.lean_memory" } } }
158
+ ```
159
+
160
+ Data root: `LM_DATA_ROOT` (default `~/.lean_memory`). Works offline-only too —
161
+ the server opportunistically upgrades each backend that its extra is installed
162
+ for (`[models]` → real embedder + reranker, `[extract]` → GLiNER2 extraction)
163
+ and otherwise falls back to deterministic stub backends (fine for CI,
164
+ semantically meaningless for real use — install `[mcp,models,extract]`).
165
+
166
+ > **What the optional `[llm]` extra buys.** The canonical `[mcp,models,extract]`
167
+ > install has no LLM typing pass, so the ~15% of candidates that escalate —
168
+ > almost all of them inferential (`derives`) facts — are typed by a
169
+ > deterministic stub instead of a model. Assertional facts are unaffected;
170
+ > inference-type facts are effectively second-class on the default path. Adding
171
+ > `[llm]` (a local Ollama model) upgrades that escalated tier to real
172
+ > constrained typing. See ARCHITECTURE.md → Known Limitations.
173
+
174
+ ## Real Model Quality
175
+
176
+ The default backends are offline stubs — deterministic and dependency-free, but semantically meaningless. Swap in real models for production-quality retrieval:
177
+
178
+ ```bash
179
+ pip install 'lean-memory[models]'
180
+ ```
181
+
182
+ With `Qwen3-Embedding-0.6B` + `Ettin-32M` reranker, retrieval jumps from 1/5 to 4/5 on the internal benchmark with zero code changes.
183
+
184
+ > For benchmark results, architecture decisions, and implementation status see [ARCHITECTURE.md](ARCHITECTURE.md).
185
+
186
+ ## How It Works
187
+
188
+ Each `mem.add()` call runs a 4-pass hybrid extraction pipeline:
189
+
190
+ 1. **Rules** — regex + dateparser for common predicates (`works_at`, `lives_in`, …)
191
+ 2. **GLiNER2** — open-vocabulary NER candidate generation (offline stub by default)
192
+ 3. **Router** — recall-biased escalation: low-confidence, coreference, and inferential (`derives`) facts escalate to the LLM pass
193
+ 4. **LLM typing** — constrained relation typing via a local Ollama model (stub by default)
194
+
195
+ Contradiction detection runs cheap-first (slot match → cosine → token subsumption → LLM). Conflicting facts are superseded, not deleted — the old fact stays with `is_latest=False` and a `superseded_by` pointer.
196
+
197
+ Retrieval fuses two-stage Matryoshka dense search (256-dim coarse KNN → 768-dim re-score) with BM25 sparse, applies RRF fusion, reranks with a cross-encoder, and scores with salience-decay (`0.6·relevance + 0.2·recency + 0.2·importance`).
198
+
199
+ ## Develop
200
+
201
+ ```bash
202
+ git clone https://github.com/Wuesteon/lean-memory
203
+ cd lean-memory
204
+ python -m venv .venv && source .venv/bin/activate
205
+ pip install -e '.[dev]'
206
+ pytest -q # full offline suite, no downloads
207
+ ```
208
+
209
+ ## Project Layout
210
+
211
+ ```
212
+ src/lean_memory/
213
+ memory.py Memory facade — the public API
214
+ types.py Episode / Fact / RetrievedFact types
215
+ store/ Store interface + SqliteStore (vec0 + FTS5)
216
+ embed/ Embedder interface, FakeEmbedder, SentenceTransformer
217
+ extract/ 4-pass extraction pipeline
218
+ retrieve/ Reranker interface, retrieval pipeline
219
+ examples/
220
+ chat.py Terminal demo agent
221
+ mcp_config.json Drop-in MCP client config
222
+ tests/ offline test suite
223
+ bench/ Retrieval quality + BET-2 ablation harnesses
224
+ ```
225
+
226
+ ## License
227
+
228
+ Apache-2.0