sqlite-graph-memory 0.3.0__tar.gz → 0.3.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 (37) hide show
  1. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/CHANGELOG.md +64 -0
  2. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/PKG-INFO +111 -19
  3. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/README.md +107 -17
  4. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/eval/run_eval.py +31 -7
  5. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/examples/README.md +15 -6
  6. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/pyproject.toml +16 -2
  7. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/src/sqlite_graph_memory/__init__.py +1 -1
  8. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/src/sqlite_graph_memory/brain_ask.py +118 -5
  9. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/src/sqlite_graph_memory/mcp_server.py +40 -9
  10. sqlite_graph_memory-0.3.2/src/sqlite_graph_memory/ppr.py +234 -0
  11. sqlite_graph_memory-0.3.2/src/sqlite_graph_memory/vault_stats.py +217 -0
  12. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/tests/test_brain_ask.py +26 -0
  13. sqlite_graph_memory-0.3.2/tests/test_llms_txt.py +39 -0
  14. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/tests/test_mcp_server.py +118 -1
  15. sqlite_graph_memory-0.3.2/tests/test_ppr.py +203 -0
  16. sqlite_graph_memory-0.3.2/tests/test_star_prompt.py +118 -0
  17. sqlite_graph_memory-0.3.2/tests/test_turnstate_hook.py +126 -0
  18. sqlite_graph_memory-0.3.2/tests/test_vault_stats.py +229 -0
  19. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/.gitignore +0 -0
  20. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/CITATION.cff +0 -0
  21. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/LICENSE +0 -0
  22. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/bench/README.md +0 -0
  23. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/bench/requirements.txt +0 -0
  24. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/docs/RELEASING.md +0 -0
  25. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/docs/bitemporal.md +0 -0
  26. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/eval/build_gold.py +0 -0
  27. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/examples/claude-code-stop-hook.json +0 -0
  28. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/examples/mcp-config.json +0 -0
  29. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/requirements.txt +0 -0
  30. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/src/sqlite_graph_memory/index_notes.py +0 -0
  31. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/src/sqlite_graph_memory/schema.sql +0 -0
  32. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/src/sqlite_graph_memory/turnstate_hook.py +0 -0
  33. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/src/sqlite_graph_memory/turnstate_show.py +0 -0
  34. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/tests/test_bench.py +0 -0
  35. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/tests/test_eval.py +0 -0
  36. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/tests/test_index_notes.py +0 -0
  37. {sqlite_graph_memory-0.3.0 → sqlite_graph_memory-0.3.2}/tests/test_optional_extra.py +0 -0
@@ -4,6 +4,68 @@ What shipped, in plain words. Entries below v0.1.3 were written on 2026-09-05 fr
4
4
  release notes that already existed — the file itself did not exist until then, so those three are
5
5
  recorded after the fact rather than backdated to look contemporaneous.
6
6
 
7
+ ## v0.3.2 — 2026-10-24
8
+
9
+ **The PageRank lane the README credits is now in the package** (#51). `ppr.py` runs
10
+ personalized PageRank over the `[[wikilink]]` graph, seeded by the dense hits, and fuses it with
11
+ the reranker by RRF while the reranker's top 5 stay where they are. Off by default (`SGM_PPR=1`
12
+ or `sgm-ask --ppr`); `eval/run_eval.py --ppr` scores it as its own mode. Any failure returns the
13
+ reranker's order. `tests/test_ppr.py` builds a hub note that pushes the reranker's rank-1 hit to
14
+ position 11 when the guard is removed, and goes red if it is.
15
+
16
+ **`sgm-stats <notes_dir>`: four health numbers for the link graph.** Orphan rate, links per
17
+ page, share of pages in the main connected component, and share of concept pages whose
18
+ `updated` date is older than 90 days, each with an ok/warn/fail verdict and a `--threshold-*`
19
+ flag. Exit code 0/1/2 for cron. Reuses the indexer's note filter and the recall wikilink parser,
20
+ so it measures the graph recall walks. On `bench/vault`: 26.2% orphans (fail), 4.65 links per
21
+ page, 100% in one component. `tests/test_vault_stats.py` crosses every threshold on a synthetic
22
+ vault and goes red when inbound links stop being counted.
23
+
24
+ **A spec you can paste.** `BITEMPORAL.md` turns the close-don't-delete rule from
25
+ `docs/bitemporal.md` into a block for any agent's `CLAUDE.md` or `AGENTS.md`: three fields
26
+ (`valid_from`, `valid_to`, `superseded_by`), five rules, and a one-line attribution as the
27
+ whole licence.
28
+
29
+ **`llms.txt` for agents that read before they install.** One page: what this is, the three
30
+ commands, MCP config for JSON clients, Claude Code and Codex, the bench numbers as they stand
31
+ on the leaderboard, the eval commands, every environment variable.
32
+
33
+ **The README says what it is for in its first line**: persistent memory for Claude Code,
34
+ Cursor and Codex over your own markdown notes.
35
+
36
+ **One star request, earned and once.** After a successful `sgm-ask --ab` in a terminal, one
37
+ stderr line asks for a GitHub star. It is printed once per machine (a marker file in
38
+ `~/.config/sqlite-graph-memory`, or `SGM_CONFIG_DIR`, created exclusively so two first runs
39
+ at once still print one line), never under `CI`, never when stdout or stderr is piped, never
40
+ when `BRAIN_AB_JSON` is set, and never with `SGM_NO_STAR_PROMPT=1`. If the line cannot be
41
+ written, the marker is removed so the next run asks again. `tests/test_star_prompt.py` covers
42
+ each guard and the call from `main()`, and goes red when any of them is removed.
43
+
44
+ ## v0.3.1 — 2026-10-19
45
+
46
+ **Credit where it is due.** The MCP server that ships as `sgm-mcp` was written by Vardhman Gupta
47
+ ([@Kaap10](https://github.com/Kaap10)) in PR #8: the server, its tests and its docs. His
48
+ follow-up fixes are the shared answer-file race (commit 3ce7061), the stdout fallback (PR #10)
49
+ and the swallowed read errors (PR #14).
50
+ He is now listed in the `authors` field of `pyproject.toml`, which is the attribution he asked for.
51
+
52
+ **Indexing works on Intel Macs again.** A fresh `pip install 'sqlite-graph-memory[embeddings]'`
53
+ on an x86_64 Mac with Python 3.12 crashed `sgm-index` with `NameError: name 'nn' is not
54
+ defined`. Torch stops at 2.2 there, transformers 5.x needs 2.6 and quietly disables torch,
55
+ and sentence-transformers 6.x requires transformers 5.x. The embeddings extra now caps
56
+ transformers below 4.50 and sentence-transformers below 5 on that one platform. Measured
57
+ both ways on the same machine: 0.3.0 from PyPI crashes, this build indexes the 202-note
58
+ bench vault cleanly (204 chunks).
59
+
60
+ **Listed in the official MCP Registry.** `server.json` describes the server as
61
+ `io.github.tonydzi/sqlite-graph-memory`, the README carries the `mcp-name` line the registry
62
+ checks, and the publish workflow lists the release after PyPI has it. The package also
63
+ installs the MCP server under its own name, `sqlite-graph-memory`, so registry clients can
64
+ start it with `uvx`.
65
+
66
+ **The README opens with three commands and their real output** on the bench vault, including
67
+ the honest caveat that vector-only ranks ahead of graph mode on that vault overall.
68
+
7
69
  ## bench-v0.1 — 2026-10-09
8
70
 
9
71
  **A benchmark other people can run.** Until now every retrieval number in this repo came from
@@ -24,6 +86,8 @@ On this vault, vector-only ranks ahead of graph mode. All of it is written out i
24
86
 
25
87
  ## v0.3.0 — 2026-10-07
26
88
 
89
+ *Release record, added 2026-10-16: the package went to PyPI on 2026-10-15 and this GitHub release object was cut a day later, so the tag points at a commit slightly after the upload (it adds only the workflow's `skip-existing` flag). Also in 0.3.0 as uploaded: the embeddings extra pins `numpy<2` on Intel Macs, where torch stops at 2.2.*
90
+
27
91
  **You can install it now.** For three months this was a repository you had to clone, and for
28
92
  an engineer deciding whether to try something, a clone is a much bigger ask than a command.
29
93
  There is now a `pyproject.toml`, the modules live in `src/sqlite_graph_memory/`, and
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sqlite-graph-memory
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Associative memory for LLM agents: Graph RAG over a folder of markdown notes, in one SQLite file, with no server to run.
5
5
  Project-URL: Homepage, https://github.com/tonydzi/sqlite-graph-memory
6
6
  Project-URL: Repository, https://github.com/tonydzi/sqlite-graph-memory
7
7
  Project-URL: Changelog, https://github.com/tonydzi/sqlite-graph-memory/blob/main/CHANGELOG.md
8
8
  Project-URL: Issues, https://github.com/tonydzi/sqlite-graph-memory/issues
9
- Author: Anton Dziatkovskii
9
+ Author: Anton Dziatkovskii, Vardhman Gupta
10
10
  License: MIT License
11
11
 
12
12
  Copyright (c) 2026 Palo Alto AI Research Lab
@@ -44,22 +44,81 @@ Requires-Python: >=3.9
44
44
  Requires-Dist: numpy>=1.24
45
45
  Provides-Extra: embeddings
46
46
  Requires-Dist: numpy<2; (sys_platform == 'darwin' and platform_machine == 'x86_64') and extra == 'embeddings'
47
+ Requires-Dist: sentence-transformers<5; (sys_platform == 'darwin' and platform_machine == 'x86_64') and extra == 'embeddings'
47
48
  Requires-Dist: sentence-transformers>=2.6; extra == 'embeddings'
49
+ Requires-Dist: transformers<4.50; (sys_platform == 'darwin' and platform_machine == 'x86_64') and extra == 'embeddings'
48
50
  Description-Content-Type: text/markdown
49
51
 
50
- # sqlite-graph-memory
52
+ # sqlite-graph-memory: persistent memory for Claude Code, Cursor and Codex over your own markdown notes
53
+
54
+ <!-- mcp-name: io.github.tonydzi/sqlite-graph-memory -->
55
+
56
+ [![PyPI](https://img.shields.io/pypi/v/sqlite-graph-memory)](https://pypi.org/project/sqlite-graph-memory/) [![tests](https://img.shields.io/github/actions/workflow/status/tonydzi/sqlite-graph-memory/tests.yml?label=tests)](https://github.com/tonydzi/sqlite-graph-memory/actions/workflows/tests.yml) [![MCP Registry](https://img.shields.io/badge/MCP%20Registry-io.github.tonydzi%2Fsqlite--graph--memory-blue)](https://registry.modelcontextprotocol.io/v0/servers?search=sqlite-graph-memory) [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
57
+
58
+ Long-term memory for coding agents that lives in the notes you already keep: an MCP server
59
+ and a CLI over any folder of markdown, Obsidian vaults included. `pip install`, point it at
60
+ the folder, and Claude Code, Cursor, Codex or any other MCP client can recall from it.
51
61
 
52
62
  **Your agent forgets everything between sessions, and plain vector search gives it back
53
- flat, unrelated chunks. This gives it associative recall over your own markdown notes —
54
- one SQLite file, no graph database, no ETL pass, no server.**
63
+ flat, unrelated chunks. This gives it associative recall over your own markdown notes:
64
+ vector search finds the entry points, the `[[wikilinks]]` you already wrote are the graph.
65
+ One SQLite file, no graph database, no ETL pass, no server.**
66
+
67
+ ## Three commands
55
68
 
56
69
  ```bash
57
- pip install sqlite-graph-memory
70
+ pip install 'sqlite-graph-memory[embeddings]'
71
+ sgm-index ./notes
72
+ sgm-ask --ab "In which programming language is the tool behind Project Ouzel written?"
58
73
  ```
59
74
 
60
- > The first PyPI release has not landed yet (the publish workflow is in place).
61
- > Until it does: `pip install git+https://github.com/tonydzi/sqlite-graph-memory`
62
- > — same package, same five commands.
75
+ `./notes` is any folder of markdown notes. To get exactly the output below, use the
76
+ 202-note synthetic vault from this repo (it is not inside the pip package):
77
+
78
+ ```bash
79
+ git clone --depth 1 https://github.com/tonydzi/sqlite-graph-memory && cp -R sqlite-graph-memory/bench/vault ./notes
80
+ ```
81
+
82
+ Real output, top 5 of 12:
83
+
84
+ ```
85
+ --- DIRECT memory (vector) ---
86
+ 1. [ 1.66] project-ouzel
87
+ 2. [ -1.33] project-avocet
88
+ 3. [ -1.79] tool-cobbleset
89
+ 4. [ -2.44] meeting-2025-10-21-lighthouse-sync
90
+ 5. [ -2.70] person-severin-ibarrondo
91
+
92
+ --- ASSOCIATIVE memory (vector+graph) ---
93
+ 1. [ 1.66] project-ouzel
94
+ 2. [ 1.14] tool-lanternfish <== +graph
95
+ 3. [ -1.33] project-avocet
96
+ 4. [ -1.79] tool-cobbleset
97
+ 5. [ -2.44] meeting-2025-10-21-lighthouse-sync
98
+ ```
99
+
100
+ The answer lives in `tool-lanternfish` ("Lanternfish is written in Elixir"). The question
101
+ never names Lanternfish, so the vector pass does not find it in the top 12 at all.
102
+ `project-ouzel` links to it, so one graph hop puts it second.
103
+
104
+ If that hop found something your vector search missed, a
105
+ [star](https://github.com/tonydzi/sqlite-graph-memory) is how other people with a notes
106
+ folder find this repo. `sgm-ask --ab` asks the same once per machine, in a terminal only;
107
+ `SGM_NO_STAR_PROMPT=1` turns it off.
108
+
109
+ That is the idea in one query, and it is not a blanket win. On this same vault vector-only
110
+ ranks ahead of graph mode overall; the numbers are in [`bench/`](bench/README.md). `--ab`
111
+ exists so you can measure which one wins on your own notes.
112
+
113
+ **From any MCP client** (Claude Desktop, Cursor, Zed, Windsurf): `sgm-mcp` after the install
114
+ above, or `uvx --with 'sqlite-graph-memory[embeddings]' sqlite-graph-memory`. Set
115
+ `BRAIN_INDEX_DIR` to the `index` folder `sgm-index` wrote. Config:
116
+ [`examples/mcp-config.json`](examples/mcp-config.json). Claude Code:
117
+ `claude mcp add sqlite-graph-memory -e BRAIN_INDEX_DIR=/path/to/index -- sgm-mcp`.
118
+ Codex: a `[mcp_servers.sqlite-graph-memory]` table in `~/.codex/config.toml` with
119
+ `command = "sgm-mcp"`. Agents that read files first: [`llms.txt`](llms.txt).
120
+
121
+ ## What it is
63
122
 
64
123
  Graph RAG on SQLite for AI agents — a working pilot, not a framework. It is the extracted
65
124
  memory layer of a personal "second brain" agent setup: vector search finds the entry
@@ -144,12 +203,13 @@ an edge table only when hop depth or corpus size demands it.
144
203
 
145
204
  - No entity lane. The production setup has an extra retrieval lane over people/project
146
205
  cards; it is too entangled with personal data to publish.
147
- - No incremental indexing and no packaging. There IS an eval suite now — see
206
+ - No incremental indexing (tracked in the issues). Packaging exists since v0.3.0 (`pip install sqlite-graph-memory`). There IS an eval suite now — see
148
207
  [Measure before you swap](#measure-before-you-swap--eval) — but it scores retrieval on
149
208
  wikilink-derived gold, which is weak supervision and says so out loud. Test coverage is uneven:
150
- `index_notes.py`, `brain_ask.py` and `mcp_server.py` have unit tests, `turnstate_hook.py`
151
- has none. Run `pytest -q` for the current count rather than trusting a number written in
152
- prose here — that is how this file ended up claiming "no tests" while seven were passing.
209
+ `index_notes.py`, `brain_ask.py` and `mcp_server.py` have unit tests, and
210
+ `turnstate_hook.py` has write-path coverage. Run `pytest -q` for the current count rather
211
+ than trusting a number written in prose here — that is how this file ended up claiming
212
+ "no tests" while seven were passing.
153
213
  The status line above says **pilot** on purpose.
154
214
 
155
215
  ## Quickstart
@@ -236,6 +296,26 @@ board with one adapter file and a pull request: [bench/CONTRIBUTING.md](bench/CO
236
296
  CI recomputes every submitted number from the per-question outputs, so a typed-in number
237
297
  cannot get onto the board.
238
298
 
299
+ ## Health check — `sgm-stats`
300
+
301
+ Four numbers about the link graph itself, no model and no index needed. It reads the notes with
302
+ the same filter and the same `[[wikilink]]` parser as recall, so it measures the graph `--graph`
303
+ actually walks. Thresholds follow [second-brain-os metrics](https://github.com/undefined-ui/second-brain-os/blob/main/docs/05-graphs/metrics.md);
304
+ each is a `--threshold-*` flag. Exit code 0 ok, 1 warn, 2 fail, so it can sit in cron.
305
+
306
+ ```text
307
+ $ sgm-stats bench/vault --today 2026-10-20
308
+ 202 pages in bench/vault
309
+
310
+ FAIL orphan_rate 26.2% 53 of 202 pages have no inbound link
311
+ OK avg_degree 4.65 940 resolved links over 202 pages
312
+ OK main_component 100.0% 202 of 202 pages; 1 component
313
+ WARN stale_concepts 100.0% 12 of 12 concept pages older than 90 days (as of 2026-10-20)
314
+ ```
315
+
316
+ The synthetic bench vault fails the orphan check for a plain reason: its 40 meeting and 12 decision notes link out and nothing
317
+ links back to them. `--json` gives the same report with the first orphan and stale page names.
318
+
239
319
  ## Measure before you swap — `eval/`
240
320
 
241
321
  Every "obvious" upgrade to a RAG pipeline — a newer embedder, a stronger reranker, hybrid
@@ -297,6 +377,17 @@ of guessing — 97% of the correct `bridge` answers were *already in the candida
297
377
  cross-encoder was dropping them, because the relation is structural, not textual. Widening
298
378
  retrieval, the obvious fix, would have done nothing at all.
299
379
 
380
+ That lane ships in the package as `src/sqlite_graph_memory/ppr.py`, **off by default**: turn it
381
+ on with `SGM_PPR=1` or `sgm-ask --ppr`, and score it with `python eval/run_eval.py --gold ...
382
+ --ppr`, which adds a third mode next to `vector` and `graph`. The numbers above were measured on
383
+ the graph pool, so the matching command is `sgm-ask --graph --ppr`; `--ppr` alone reorders the
384
+ vector pool, and `--ab` leaves the lane out so its A/B log stays vector against graph. Any failure in it falls back to
385
+ the reranker's order, like the graph hop. Be clear about what that reproduces: the bridge
386
+ numbers above came from the maintainer's home vault and home pipeline on 2026-09-16, and they
387
+ have not been re-run from the published package. The code is the same algorithm with the same
388
+ constants (alpha 0.85, 10 PPR files, top 5 protected), but your vault will give your numbers,
389
+ and `bench/` does not score this lane yet.
390
+
300
391
  Seven other changes were rejected by the same ruler, under one rule fixed in advance:
301
392
  **a regression worse than 0.01 nDCG@12 on any class reverts the change.**
302
393
 
@@ -306,10 +397,10 @@ Seven other changes were rejected by the same ruler, under one rule fixed in adv
306
397
  | Qwen3-Embedding-0.6B | body Recall 0.767 -> 0.683; reindex 2863 s vs 333 s -> **rejected** |
307
398
  | bge-reranker-v2-m3 | −0.011…−0.018 nDCG on every class except English (+0.041) -> **rejected** |
308
399
  | Qwen3-Reranker-0.6B | title −0.111 nDCG at 12x the cost -> **rejected** |
309
- | BM25 + RRF hybrid | temporal −0.035 -> **merged switched off** |
400
+ | BM25 + RRF hybrid | temporal −0.035 -> **merged switched off** (in the home pipeline, not in this package) |
310
401
  | heading-path prefix in every chunk | body −0.023, temporal −0.035 -> **reverted** |
311
402
  | `sqlite-vec` (vec0) | full scan 11.99 s vs 0.14 s for a plain SQLite table, and its ANN missed 1.2 hits of 60 -> vectors live in SQLite, **the extension does not** |
312
- | bi-temporal demotion of superseded notes | temporal +0.130 nDCG on a 41-question temporal set, but title −0.012 -> **merged behind a flag, default off** |
403
+ | bi-temporal demotion of superseded notes | temporal +0.130 nDCG on a 41-question temporal set, but title −0.012 -> **merged behind a flag, default off** (in the home pipeline, not in this package) |
313
404
 
314
405
  None of those verdicts is a claim about the models. They are claims about *this corpus with
315
406
  this ruler*, which is the only kind of claim a swap decision actually needs — and the reason
@@ -331,7 +422,7 @@ while the metric is broken never tested the metric.
331
422
 
332
423
  ## Roadmap
333
424
 
334
- **Now — [v0.1.1](https://github.com/tonydzi/sqlite-graph-memory/releases).**
425
+ **Now — [latest release](https://github.com/tonydzi/sqlite-graph-memory/releases/latest)** (v0.3.1 on PyPI and in the MCP Registry as of 2026-10-19).
335
426
  The pilot as it runs daily: retrieval pipeline, per-turn ledger and A/B telemetry, all in
336
427
  `brain_ask.py` and `turnstate_hook.py`, plus the bi-temporal design note. Known defects live in
337
428
  the [tracker](https://github.com/tonydzi/sqlite-graph-memory/issues) rather than in footnotes
@@ -369,9 +460,10 @@ Both are set as constants (`E5_MODEL`, `RERANK_MODEL`) in `brain_ask.py` and sma
369
460
  to run on a laptop GPU; swap freely, since nothing in the pipeline depends on these
370
461
  specific models.
371
462
 
372
- ## Cite this work
463
+ ## Maintainers
373
464
 
374
- If this repo shows up in your research, cite it via [CITATION.cff](CITATION.cff) (GitHub's "Cite this repository" button). Academic identity: Anton Dzyatkovsky publishes as **Anton Dziatkovskii** ([ORCID 0000-0001-7408-3054](https://orcid.org/0000-0001-7408-3054)).
465
+ - **Anton Dziatkovskii** ([@tonydzi](https://github.com/tonydzi)): retrieval pipeline, ledger, bench, releases.
466
+ - **Vardhman Gupta** ([@Kaap10](https://github.com/Kaap10)): the MCP server (`sgm-mcp`, PRs #8, #10, #14) and its tests; reviews on `mcp_server.py` route to him via CODEOWNERS.
375
467
 
376
468
  ## AI contributors
377
469
 
@@ -383,7 +475,7 @@ content** — no decorative credits. Lab-wide policy, one source for every repo:
383
475
 
384
476
  ## Cite this work
385
477
 
386
- The paper about this system — retrieval design, pilot telemetry, and the query-class-stratified evaluation protocol — is published: [DOI 10.5281/zenodo.22639718](https://doi.org/10.5281/zenodo.22639718) (preprint, CC BY 4.0). Machine-readable citation: [CITATION.cff](CITATION.cff).
478
+ The paper about this system — retrieval design, pilot telemetry, and the query-class-stratified evaluation protocol — is published: [DOI 10.5281/zenodo.22639718](https://doi.org/10.5281/zenodo.22639718) (preprint, CC BY 4.0). Machine-readable citation: [CITATION.cff](CITATION.cff) (GitHub's "Cite this repository" button). Academic identity: Anton Dzyatkovsky publishes as **Anton Dziatkovskii** ([ORCID 0000-0001-7408-3054](https://orcid.org/0000-0001-7408-3054)).
387
479
 
388
480
  ## License
389
481
 
@@ -1,16 +1,73 @@
1
- # sqlite-graph-memory
1
+ # sqlite-graph-memory: persistent memory for Claude Code, Cursor and Codex over your own markdown notes
2
+
3
+ <!-- mcp-name: io.github.tonydzi/sqlite-graph-memory -->
4
+
5
+ [![PyPI](https://img.shields.io/pypi/v/sqlite-graph-memory)](https://pypi.org/project/sqlite-graph-memory/) [![tests](https://img.shields.io/github/actions/workflow/status/tonydzi/sqlite-graph-memory/tests.yml?label=tests)](https://github.com/tonydzi/sqlite-graph-memory/actions/workflows/tests.yml) [![MCP Registry](https://img.shields.io/badge/MCP%20Registry-io.github.tonydzi%2Fsqlite--graph--memory-blue)](https://registry.modelcontextprotocol.io/v0/servers?search=sqlite-graph-memory) [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
6
+
7
+ Long-term memory for coding agents that lives in the notes you already keep: an MCP server
8
+ and a CLI over any folder of markdown, Obsidian vaults included. `pip install`, point it at
9
+ the folder, and Claude Code, Cursor, Codex or any other MCP client can recall from it.
2
10
 
3
11
  **Your agent forgets everything between sessions, and plain vector search gives it back
4
- flat, unrelated chunks. This gives it associative recall over your own markdown notes —
5
- one SQLite file, no graph database, no ETL pass, no server.**
12
+ flat, unrelated chunks. This gives it associative recall over your own markdown notes:
13
+ vector search finds the entry points, the `[[wikilinks]]` you already wrote are the graph.
14
+ One SQLite file, no graph database, no ETL pass, no server.**
15
+
16
+ ## Three commands
6
17
 
7
18
  ```bash
8
- pip install sqlite-graph-memory
19
+ pip install 'sqlite-graph-memory[embeddings]'
20
+ sgm-index ./notes
21
+ sgm-ask --ab "In which programming language is the tool behind Project Ouzel written?"
9
22
  ```
10
23
 
11
- > The first PyPI release has not landed yet (the publish workflow is in place).
12
- > Until it does: `pip install git+https://github.com/tonydzi/sqlite-graph-memory`
13
- > — same package, same five commands.
24
+ `./notes` is any folder of markdown notes. To get exactly the output below, use the
25
+ 202-note synthetic vault from this repo (it is not inside the pip package):
26
+
27
+ ```bash
28
+ git clone --depth 1 https://github.com/tonydzi/sqlite-graph-memory && cp -R sqlite-graph-memory/bench/vault ./notes
29
+ ```
30
+
31
+ Real output, top 5 of 12:
32
+
33
+ ```
34
+ --- DIRECT memory (vector) ---
35
+ 1. [ 1.66] project-ouzel
36
+ 2. [ -1.33] project-avocet
37
+ 3. [ -1.79] tool-cobbleset
38
+ 4. [ -2.44] meeting-2025-10-21-lighthouse-sync
39
+ 5. [ -2.70] person-severin-ibarrondo
40
+
41
+ --- ASSOCIATIVE memory (vector+graph) ---
42
+ 1. [ 1.66] project-ouzel
43
+ 2. [ 1.14] tool-lanternfish <== +graph
44
+ 3. [ -1.33] project-avocet
45
+ 4. [ -1.79] tool-cobbleset
46
+ 5. [ -2.44] meeting-2025-10-21-lighthouse-sync
47
+ ```
48
+
49
+ The answer lives in `tool-lanternfish` ("Lanternfish is written in Elixir"). The question
50
+ never names Lanternfish, so the vector pass does not find it in the top 12 at all.
51
+ `project-ouzel` links to it, so one graph hop puts it second.
52
+
53
+ If that hop found something your vector search missed, a
54
+ [star](https://github.com/tonydzi/sqlite-graph-memory) is how other people with a notes
55
+ folder find this repo. `sgm-ask --ab` asks the same once per machine, in a terminal only;
56
+ `SGM_NO_STAR_PROMPT=1` turns it off.
57
+
58
+ That is the idea in one query, and it is not a blanket win. On this same vault vector-only
59
+ ranks ahead of graph mode overall; the numbers are in [`bench/`](bench/README.md). `--ab`
60
+ exists so you can measure which one wins on your own notes.
61
+
62
+ **From any MCP client** (Claude Desktop, Cursor, Zed, Windsurf): `sgm-mcp` after the install
63
+ above, or `uvx --with 'sqlite-graph-memory[embeddings]' sqlite-graph-memory`. Set
64
+ `BRAIN_INDEX_DIR` to the `index` folder `sgm-index` wrote. Config:
65
+ [`examples/mcp-config.json`](examples/mcp-config.json). Claude Code:
66
+ `claude mcp add sqlite-graph-memory -e BRAIN_INDEX_DIR=/path/to/index -- sgm-mcp`.
67
+ Codex: a `[mcp_servers.sqlite-graph-memory]` table in `~/.codex/config.toml` with
68
+ `command = "sgm-mcp"`. Agents that read files first: [`llms.txt`](llms.txt).
69
+
70
+ ## What it is
14
71
 
15
72
  Graph RAG on SQLite for AI agents — a working pilot, not a framework. It is the extracted
16
73
  memory layer of a personal "second brain" agent setup: vector search finds the entry
@@ -95,12 +152,13 @@ an edge table only when hop depth or corpus size demands it.
95
152
 
96
153
  - No entity lane. The production setup has an extra retrieval lane over people/project
97
154
  cards; it is too entangled with personal data to publish.
98
- - No incremental indexing and no packaging. There IS an eval suite now — see
155
+ - No incremental indexing (tracked in the issues). Packaging exists since v0.3.0 (`pip install sqlite-graph-memory`). There IS an eval suite now — see
99
156
  [Measure before you swap](#measure-before-you-swap--eval) — but it scores retrieval on
100
157
  wikilink-derived gold, which is weak supervision and says so out loud. Test coverage is uneven:
101
- `index_notes.py`, `brain_ask.py` and `mcp_server.py` have unit tests, `turnstate_hook.py`
102
- has none. Run `pytest -q` for the current count rather than trusting a number written in
103
- prose here — that is how this file ended up claiming "no tests" while seven were passing.
158
+ `index_notes.py`, `brain_ask.py` and `mcp_server.py` have unit tests, and
159
+ `turnstate_hook.py` has write-path coverage. Run `pytest -q` for the current count rather
160
+ than trusting a number written in prose here — that is how this file ended up claiming
161
+ "no tests" while seven were passing.
104
162
  The status line above says **pilot** on purpose.
105
163
 
106
164
  ## Quickstart
@@ -187,6 +245,26 @@ board with one adapter file and a pull request: [bench/CONTRIBUTING.md](bench/CO
187
245
  CI recomputes every submitted number from the per-question outputs, so a typed-in number
188
246
  cannot get onto the board.
189
247
 
248
+ ## Health check — `sgm-stats`
249
+
250
+ Four numbers about the link graph itself, no model and no index needed. It reads the notes with
251
+ the same filter and the same `[[wikilink]]` parser as recall, so it measures the graph `--graph`
252
+ actually walks. Thresholds follow [second-brain-os metrics](https://github.com/undefined-ui/second-brain-os/blob/main/docs/05-graphs/metrics.md);
253
+ each is a `--threshold-*` flag. Exit code 0 ok, 1 warn, 2 fail, so it can sit in cron.
254
+
255
+ ```text
256
+ $ sgm-stats bench/vault --today 2026-10-20
257
+ 202 pages in bench/vault
258
+
259
+ FAIL orphan_rate 26.2% 53 of 202 pages have no inbound link
260
+ OK avg_degree 4.65 940 resolved links over 202 pages
261
+ OK main_component 100.0% 202 of 202 pages; 1 component
262
+ WARN stale_concepts 100.0% 12 of 12 concept pages older than 90 days (as of 2026-10-20)
263
+ ```
264
+
265
+ The synthetic bench vault fails the orphan check for a plain reason: its 40 meeting and 12 decision notes link out and nothing
266
+ links back to them. `--json` gives the same report with the first orphan and stale page names.
267
+
190
268
  ## Measure before you swap — `eval/`
191
269
 
192
270
  Every "obvious" upgrade to a RAG pipeline — a newer embedder, a stronger reranker, hybrid
@@ -248,6 +326,17 @@ of guessing — 97% of the correct `bridge` answers were *already in the candida
248
326
  cross-encoder was dropping them, because the relation is structural, not textual. Widening
249
327
  retrieval, the obvious fix, would have done nothing at all.
250
328
 
329
+ That lane ships in the package as `src/sqlite_graph_memory/ppr.py`, **off by default**: turn it
330
+ on with `SGM_PPR=1` or `sgm-ask --ppr`, and score it with `python eval/run_eval.py --gold ...
331
+ --ppr`, which adds a third mode next to `vector` and `graph`. The numbers above were measured on
332
+ the graph pool, so the matching command is `sgm-ask --graph --ppr`; `--ppr` alone reorders the
333
+ vector pool, and `--ab` leaves the lane out so its A/B log stays vector against graph. Any failure in it falls back to
334
+ the reranker's order, like the graph hop. Be clear about what that reproduces: the bridge
335
+ numbers above came from the maintainer's home vault and home pipeline on 2026-09-16, and they
336
+ have not been re-run from the published package. The code is the same algorithm with the same
337
+ constants (alpha 0.85, 10 PPR files, top 5 protected), but your vault will give your numbers,
338
+ and `bench/` does not score this lane yet.
339
+
251
340
  Seven other changes were rejected by the same ruler, under one rule fixed in advance:
252
341
  **a regression worse than 0.01 nDCG@12 on any class reverts the change.**
253
342
 
@@ -257,10 +346,10 @@ Seven other changes were rejected by the same ruler, under one rule fixed in adv
257
346
  | Qwen3-Embedding-0.6B | body Recall 0.767 -> 0.683; reindex 2863 s vs 333 s -> **rejected** |
258
347
  | bge-reranker-v2-m3 | −0.011…−0.018 nDCG on every class except English (+0.041) -> **rejected** |
259
348
  | Qwen3-Reranker-0.6B | title −0.111 nDCG at 12x the cost -> **rejected** |
260
- | BM25 + RRF hybrid | temporal −0.035 -> **merged switched off** |
349
+ | BM25 + RRF hybrid | temporal −0.035 -> **merged switched off** (in the home pipeline, not in this package) |
261
350
  | heading-path prefix in every chunk | body −0.023, temporal −0.035 -> **reverted** |
262
351
  | `sqlite-vec` (vec0) | full scan 11.99 s vs 0.14 s for a plain SQLite table, and its ANN missed 1.2 hits of 60 -> vectors live in SQLite, **the extension does not** |
263
- | bi-temporal demotion of superseded notes | temporal +0.130 nDCG on a 41-question temporal set, but title −0.012 -> **merged behind a flag, default off** |
352
+ | bi-temporal demotion of superseded notes | temporal +0.130 nDCG on a 41-question temporal set, but title −0.012 -> **merged behind a flag, default off** (in the home pipeline, not in this package) |
264
353
 
265
354
  None of those verdicts is a claim about the models. They are claims about *this corpus with
266
355
  this ruler*, which is the only kind of claim a swap decision actually needs — and the reason
@@ -282,7 +371,7 @@ while the metric is broken never tested the metric.
282
371
 
283
372
  ## Roadmap
284
373
 
285
- **Now — [v0.1.1](https://github.com/tonydzi/sqlite-graph-memory/releases).**
374
+ **Now — [latest release](https://github.com/tonydzi/sqlite-graph-memory/releases/latest)** (v0.3.1 on PyPI and in the MCP Registry as of 2026-10-19).
286
375
  The pilot as it runs daily: retrieval pipeline, per-turn ledger and A/B telemetry, all in
287
376
  `brain_ask.py` and `turnstate_hook.py`, plus the bi-temporal design note. Known defects live in
288
377
  the [tracker](https://github.com/tonydzi/sqlite-graph-memory/issues) rather than in footnotes
@@ -320,9 +409,10 @@ Both are set as constants (`E5_MODEL`, `RERANK_MODEL`) in `brain_ask.py` and sma
320
409
  to run on a laptop GPU; swap freely, since nothing in the pipeline depends on these
321
410
  specific models.
322
411
 
323
- ## Cite this work
412
+ ## Maintainers
324
413
 
325
- If this repo shows up in your research, cite it via [CITATION.cff](CITATION.cff) (GitHub's "Cite this repository" button). Academic identity: Anton Dzyatkovsky publishes as **Anton Dziatkovskii** ([ORCID 0000-0001-7408-3054](https://orcid.org/0000-0001-7408-3054)).
414
+ - **Anton Dziatkovskii** ([@tonydzi](https://github.com/tonydzi)): retrieval pipeline, ledger, bench, releases.
415
+ - **Vardhman Gupta** ([@Kaap10](https://github.com/Kaap10)): the MCP server (`sgm-mcp`, PRs #8, #10, #14) and its tests; reviews on `mcp_server.py` route to him via CODEOWNERS.
326
416
 
327
417
  ## AI contributors
328
418
 
@@ -334,7 +424,7 @@ content** — no decorative credits. Lab-wide policy, one source for every repo:
334
424
 
335
425
  ## Cite this work
336
426
 
337
- The paper about this system — retrieval design, pilot telemetry, and the query-class-stratified evaluation protocol — is published: [DOI 10.5281/zenodo.22639718](https://doi.org/10.5281/zenodo.22639718) (preprint, CC BY 4.0). Machine-readable citation: [CITATION.cff](CITATION.cff).
427
+ The paper about this system — retrieval design, pilot telemetry, and the query-class-stratified evaluation protocol — is published: [DOI 10.5281/zenodo.22639718](https://doi.org/10.5281/zenodo.22639718) (preprint, CC BY 4.0). Machine-readable citation: [CITATION.cff](CITATION.cff) (GitHub's "Cite this repository" button). Academic identity: Anton Dzyatkovsky publishes as **Anton Dziatkovskii** ([ORCID 0000-0001-7408-3054](https://orcid.org/0000-0001-7408-3054)).
338
428
 
339
429
  ## License
340
430
 
@@ -9,6 +9,8 @@ Every question is scored twice, in both retrieval modes, on the same candidates
9
9
 
10
10
  vector dense retrieve -> rerank (what you get without the graph)
11
11
  graph dense -> 1-hop [[wikilinks]] -> rerank
12
+ ppr graph, then personalized PageRank fused with the reranker by RRF, top 5
13
+ protected (only with --ppr; see src/sqlite_graph_memory/ppr.py)
12
14
 
13
15
  The difference between those two columns is the only honest answer to "does the graph
14
16
  actually help?" — and on our own vault the answer differed by class (see README).
@@ -16,6 +18,7 @@ actually help?" — and on our own vault the answer differed by class (see READM
16
18
  USAGE
17
19
  python eval/run_eval.py --gold eval/gold-2026-09-16.jsonl
18
20
  python eval/run_eval.py --gold ... --limit 20 # smoke run
21
+ python eval/run_eval.py --gold ... --ppr # add the PageRank lane as a 3rd mode
19
22
  python eval/run_eval.py --selftest # metrics only, no model, no index
20
23
 
21
24
  Config (env): BRAIN_INDEX_DIR (index to score), TURNSTATE_DB (where --sqlite appends a
@@ -119,8 +122,8 @@ def selftest():
119
122
 
120
123
 
121
124
  # ------------------------------------------------------------------ runner
122
- def score_gold(gold, progress=True):
123
- """Run every question through both modes. -> (rows, config dict)."""
125
+ def score_gold(gold, progress=True, ppr=False):
126
+ """Run every question through both modes (three with ppr=True). -> (rows, config dict)."""
124
127
  from sqlite_graph_memory import brain_ask as ba
125
128
 
126
129
  dev = ba.pick_device()
@@ -135,15 +138,26 @@ def score_gold(gold, progress=True):
135
138
  by_base = ba.index_by_basename(meta)
136
139
  stem_of = lambda i: Path(meta[i]['path']).stem.lower()
137
140
 
138
- rows, t0 = [], time.time()
141
+ rows, t0, ppr_ran = [], time.time(), False
139
142
  for k, g in enumerate(gold):
140
143
  tq = time.time()
141
144
  sims = ba.query_sims(enc, emb, g['query'])
142
145
  base = ba.retrieve_candidates(sims, meta)
143
146
  added = ba.expand_1hop(sims, meta, base, by_base)
147
+ lists = {'vector': ba.rerank_candidates(ce, g['query'], base, meta)}
148
+ if ppr:
149
+ # One rerank of the graph pool serves both modes: `graph` is its top-12, exactly
150
+ # what rerank_candidates(topn=12) returns, and `ppr` fuses the FULL list.
151
+ full = ba.rerank_candidates(ce, g['query'], base + added, meta, topn=None)
152
+ lists['graph'] = full[:TOPN]
153
+ lane = ba.ppr_lane(meta, sims, base)
154
+ ppr_ran = ppr_ran or bool(lane)
155
+ lists['ppr'] = ba.fuse_ppr(full, meta, lane, TOPN)
156
+ else:
157
+ lists['graph'] = ba.rerank_candidates(ce, g['query'], base + added, meta)
144
158
  res = {}
145
- for mode, cand in (('vector', base), ('graph', base + added)):
146
- ranked = [stem_of(i) for i, _ in ba.rerank_candidates(ce, g['query'], cand, meta)]
159
+ for mode, scored in lists.items():
160
+ ranked = [stem_of(i) for i, _ in scored]
147
161
  res[mode] = {'ranked': ranked,
148
162
  'recall': recall_at(ranked, g['gold']),
149
163
  'mrr': mrr_at(ranked, g['gold']),
@@ -163,6 +177,11 @@ def score_gold(gold, progress=True):
163
177
  sys.stderr.write('!! the graph added no neighbours to ANY question — link resolution or '
164
178
  'the index is broken, not the graph. The graph rows below are the '
165
179
  'vector rows.\n')
180
+ # Same silence, PPR lane: an empty lane falls back to the reranker order, so the ppr rows
181
+ # would equal the graph rows and read as "PPR adds nothing" when it never ran.
182
+ if ppr and rows and not ppr_ran:
183
+ sys.stderr.write('!! the PPR lane returned no files for ANY question (no edges, or it '
184
+ 'failed). The ppr rows below are the graph rows.\n')
166
185
  return rows, cfg
167
186
 
168
187
 
@@ -173,7 +192,7 @@ def summarize(rows):
173
192
  groups += [('lang:' + lg, [r for r in rows if r['lang'] == lg]) for lg in ('ru', 'en')]
174
193
  for name, rs in groups:
175
194
  if not rs: continue
176
- for mode in ('vector', 'graph'):
195
+ for mode in [m for m in ('vector', 'graph', 'ppr') if m in rs[0]['res']]:
177
196
  out[(name, mode)] = {'n': len(rs),
178
197
  'recall': sum(r['res'][mode]['recall'] for r in rs) / len(rs),
179
198
  'mrr': sum(r['res'][mode]['mrr'] for r in rs) / len(rs),
@@ -197,6 +216,9 @@ def print_table(summary, cfg, gold_file):
197
216
  if (name, 'graph') in summary:
198
217
  d = summary[(name, 'graph')]['ndcg'] - summary[(name, 'vector')]['ndcg']
199
218
  print('graph effect on %-9s nDCG@12 %+.3f' % (name, d))
219
+ if (name, 'ppr') in summary:
220
+ d = summary[(name, 'ppr')]['ndcg'] - summary[(name, 'graph')]['ndcg']
221
+ print('ppr effect on %-9s nDCG@12 %+.3f (vs graph)' % (name, d))
200
222
 
201
223
 
202
224
  def write_sqlite(summary, cfg, gold_file, scores_file):
@@ -232,6 +254,8 @@ def main():
232
254
  ap.add_argument('--out', help='scores .jsonl (default: eval/scores-<timestamp>.jsonl)')
233
255
  ap.add_argument('--sqlite', action='store_true', help='also append to gold_eval in TURNSTATE_DB')
234
256
  ap.add_argument('--no-persist', action='store_true', help='print the table, write nothing')
257
+ ap.add_argument('--ppr', action='store_true',
258
+ help='also score the PageRank lane (graph + PPR/RRF, reranker top 5 protected)')
235
259
  ap.add_argument('--selftest', action='store_true', help='check the metrics and exit')
236
260
  a = ap.parse_args()
237
261
 
@@ -256,7 +280,7 @@ def main():
256
280
  if a.limit: gold = gold[:a.limit]
257
281
  if not gold: sys.exit('gold file %s is empty' % gold_path)
258
282
 
259
- rows, cfg = score_gold(gold)
283
+ rows, cfg = score_gold(gold, ppr=a.ppr)
260
284
  summary = summarize(rows)
261
285
  print_table(summary, cfg, a.gold)
262
286
  if os.environ.get('EVAL_MUTANT') == '1':
@@ -47,19 +47,28 @@ Add the server to your MCP client config (e.g. `claude_desktop_config.json` or C
47
47
  {
48
48
  "mcpServers": {
49
49
  "sqlite-graph-memory": {
50
- "command": "python",
51
- "args": [
52
- "/absolute/path/to/sqlite-graph-memory/sgm-mcp"
53
- ],
50
+ "command": "sgm-mcp",
54
51
  "env": {
55
- "BRAIN_INDEX_DIR": "/absolute/path/to/sqlite-graph-memory/index",
56
- "TURNSTATE_DB": "/absolute/path/to/sqlite-graph-memory/turnstate.db"
52
+ "BRAIN_INDEX_DIR": "/path/to/your/index",
53
+ "TURNSTATE_DB": "/path/to/your/turnstate.db"
57
54
  }
58
55
  }
59
56
  }
60
57
  }
61
58
  ```
62
59
 
60
+ The same file is checked in as [`mcp-config.json`](mcp-config.json). `sgm-mcp` is on PATH after `pip install 'sqlite-graph-memory[embeddings]'`; if your client does not inherit that venv, use the absolute path to its `bin/sgm-mcp`.
61
+
62
+ Two copy-paste doors that need no JSON editing:
63
+
64
+ ```bash
65
+ # Claude Code
66
+ claude mcp add sqlite-graph-memory -e BRAIN_INDEX_DIR=/path/to/your/index -e TURNSTATE_DB=/path/to/your/turnstate.db -- sgm-mcp
67
+
68
+ # any client that speaks uvx (registry name io.github.tonydzi/sqlite-graph-memory)
69
+ uvx --with 'sqlite-graph-memory[embeddings]' sqlite-graph-memory
70
+ ```
71
+
63
72
  ### Direct CLI Verification
64
73
 
65
74
  You can verify the MCP server directly from the command line without launching an MCP client: