memkernel 0.3.0__tar.gz → 0.4.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 (41) hide show
  1. {memkernel-0.3.0 → memkernel-0.4.0}/PKG-INFO +65 -13
  2. {memkernel-0.3.0 → memkernel-0.4.0}/README.md +64 -12
  3. {memkernel-0.3.0 → memkernel-0.4.0}/pyproject.toml +1 -1
  4. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/__init__.py +1 -1
  5. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/api/routes/admin.py +16 -1
  6. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/config.py +8 -0
  7. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/core/models.py +73 -0
  8. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/core/ports.py +9 -3
  9. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/core/services.py +474 -44
  10. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/lifecycle.py +34 -2
  11. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/mcp/server.py +41 -8
  12. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/mcp/tools/memory.py +10 -1
  13. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/storage/chroma.py +43 -16
  14. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/wiki/sync.py +2 -3
  15. {memkernel-0.3.0 → memkernel-0.4.0}/LICENSE +0 -0
  16. {memkernel-0.3.0 → memkernel-0.4.0}/NOTICE +0 -0
  17. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/adapters/__init__.py +0 -0
  18. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/adapters/rlmstudio.py +0 -0
  19. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/api/__init__.py +0 -0
  20. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/api/app.py +0 -0
  21. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/api/routes/__init__.py +0 -0
  22. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/core/__init__.py +0 -0
  23. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/factory.py +0 -0
  24. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/mcp/__init__.py +0 -0
  25. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/mcp/tools/__init__.py +0 -0
  26. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/mcp/tools/wiki.py +0 -0
  27. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/memory/__init__.py +0 -0
  28. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/memory/dedup.py +0 -0
  29. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/memory/lifecycle.py +0 -0
  30. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/memory/types.py +0 -0
  31. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/py.typed +0 -0
  32. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/retrieval/__init__.py +0 -0
  33. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/retrieval/chunking.py +0 -0
  34. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/retrieval/embeddings.py +0 -0
  35. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/retrieval/reasoner.py +0 -0
  36. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/retrieval/search.py +0 -0
  37. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/retrieval/sparse.py +0 -0
  38. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/storage/__init__.py +0 -0
  39. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/storage/documents.py +0 -0
  40. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/wiki/__init__.py +0 -0
  41. {memkernel-0.3.0 → memkernel-0.4.0}/src/memkernel/wiki/pages.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memkernel
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Code-aware memory server for AI coding copilots
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -27,7 +27,10 @@ Project-URL: Homepage, https://memkernel.dev
27
27
  Project-URL: Repository, https://github.com/gosha70/memkernel
28
28
  Description-Content-Type: text/markdown
29
29
 
30
- # MemKernel
30
+ <h1>
31
+ <img src="https://raw.githubusercontent.com/gosha70/memkernel/main/docs/assets/memkernel-logo.png" alt="MemKernel logo: a glowing memory core on three stacked tiers" width="110" align="absmiddle">
32
+ MemKernel
33
+ </h1>
31
34
 
32
35
  [![CI](https://github.com/gosha70/memkernel/actions/workflows/ci.yml/badge.svg)](https://github.com/gosha70/memkernel/actions/workflows/ci.yml)
33
36
  [![PyPI](https://img.shields.io/pypi/v/memkernel)](https://pypi.org/project/memkernel/)
@@ -48,23 +51,33 @@ MemKernel gives Claude Code (and any MCP-compatible copilot) persistent memory a
48
51
 
49
52
  ## Requirements
50
53
 
51
- - Python 3.11 – 3.13
54
+ - Python 3.11 – 3.13 (the package declares `Requires-Python >=3.11,<3.14`; pip reports "from versions: none" when the installing interpreter is outside that range)
52
55
  - [Poetry](https://python-poetry.org) (for development installs)
53
56
 
54
57
  ---
55
58
 
56
59
  ## Install
57
60
 
61
+ MemKernel is a Python package on PyPI. Its `memkernel` command is the MCP server. There is no npm package: an `npx` install someone suggests is a hallucination.
62
+
63
+ **Recommended: an isolated install with [pipx](https://pipx.pypa.io)**, which puts the `memkernel` command on your PATH without touching any other Python environment:
64
+
58
65
  ```bash
59
- pip install memkernel
66
+ pipx install memkernel
67
+ # if pipx's default interpreter is outside 3.11–3.13:
68
+ pipx install --python python3.11 memkernel
60
69
  ```
61
70
 
62
- Or with Poetry:
71
+ **As a library inside a project**, install it into that project's virtual environment:
63
72
 
64
73
  ```bash
65
- poetry add memkernel
74
+ pip install memkernel # or: poetry add memkernel
66
75
  ```
67
76
 
77
+ Avoid a plain `pip install memkernel` into a shared or system interpreter (Homebrew's `python3`, for example). MemKernel pins `chromadb` 1.x and recent `cffi`, so pip will replace older copies and can break unrelated tools that need them.
78
+
79
+ Verify the install with `pipx list` or `which memkernel`. Running `memkernel` with no arguments starts the MCP server on stdio and waits for a client; `memkernel --version` and `memkernel --help` print and exit.
80
+
68
81
  ---
69
82
 
70
83
  ## Add to Claude Code
@@ -77,17 +90,29 @@ Add MemKernel to `.claude/settings.json` in your project:
77
90
  {
78
91
  "mcpServers": {
79
92
  "memkernel": {
80
- "command": "memkernel",
93
+ "command": "/Users/you/.local/bin/memkernel",
81
94
  "args": [],
82
95
  "env": {
83
- "MEMKERNEL_PROJECT_ID": "my-project"
96
+ "MEMKERNEL_PROJECT_ID": "my-project",
97
+ "MEMKERNEL_CHROMA_PERSIST_PATH": "/Users/you/.memkernel/my-project"
84
98
  }
85
99
  }
86
100
  }
87
101
  }
88
102
  ```
89
103
 
90
- Replace `my-project` with a short identifier for the project — memories are isolated per project.
104
+ Replace `my-project` with a short identifier for the project — memories are isolated per project. Use the absolute path of the command (`which memkernel` prints it) because Claude Code does not necessarily see the PATH changes your shell profile makes, and an absolute `MEMKERNEL_CHROMA_PERSIST_PATH` keeps the store outside the repository. On first server initialization, MemKernel downloads the embedding model (about 90 MB) if it is not already cached: `build_service()` loads the sentence-transformers model when the MCP server starts, before any tool call, so the first connection can take a minute. Later starts and every tool call are fast.
105
+
106
+ <details>
107
+ <summary>Troubleshooting the install</summary>
108
+
109
+ - **`pipx install memkernel` says "No matching distribution found"** — pipx used an interpreter outside 3.11–3.13. Pass one explicitly: `pipx install --python /opt/homebrew/opt/python@3.11/bin/python3.11 memkernel`.
110
+ - **`bash: /some/old/path/memkernel: No such file or directory`** right after reinstalling — the shell cached the old location. Run `hash -r` (bash) or `rehash` (zsh), then `which memkernel`.
111
+ - **`memkernel --help` seems to hang** on versions before 0.4.0 — the command had no flags and started the server; press Ctrl-C. From 0.4.0 on, `--help` and `--version` print and exit.
112
+ - **The first MCP connection is slow or times out** — the server is downloading the embedding model at startup (see above). Wait for it once, or pre-warm it with `python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('all-MiniLM-L6-v2')"` in the same environment.
113
+ - **A global pip install broke another tool** (for example `langchain-chroma` wanting `chromadb < 0.6`) — uninstall memkernel from that interpreter, reinstall the versions the other tool needs, and install memkernel with pipx instead.
114
+
115
+ </details>
91
116
 
92
117
  ### 2. Add memory instructions to CLAUDE.md
93
118
 
@@ -107,16 +132,30 @@ See the Prerequisites section inside each script for the Python environment requ
107
132
 
108
133
  ---
109
134
 
135
+ ## Add to Kiro
136
+
137
+ Kiro (and the Kiro CLI, formerly Amazon Q Developer CLI) connects to MemKernel as a stdio MCP server. [`templates/kiro/`](templates/kiro/) holds the three files to copy into a workspace:
138
+
139
+ | Template | Destination | Role |
140
+ |----------|-------------|------|
141
+ | `settings/mcp.json` | `.kiro/settings/mcp.json` | Server entry with `env` and `autoApprove` |
142
+ | `steering/memory.md` | `.kiro/steering/memory.md` | Memory instructions, always included |
143
+ | `hooks/memkernel.json` | `.kiro/hooks/memkernel.json` | `SessionStart` recall, `Stop` retain, manual checkpoint |
144
+
145
+ [`templates/kiro/README.md`](templates/kiro/README.md) has the setup steps, the Kiro CLI custom-agent variant and the caveats (absolute persist path, hook format by Kiro version).
146
+
147
+ ---
148
+
110
149
  ## MCP Tools
111
150
 
112
151
  | Tool | What it does |
113
152
  |------|-------------|
114
153
  | `retain(content, type, session_id?, checkpoint?, source?, importance?, tags?, supersedes?)` | Store a memory. Returns `ref_id`. Idempotent — identical content + type returns the same `ref_id`. `supersedes` hides the memory it replaces. |
115
- | `recall(query, type?, tier?, session_id?, checkpoint_only?, top_k?)` | Search by natural language or identifier. Returns lightweight `ArtifactRef` list ranked by relevance + recency + importance. Records an access (reinforces retention). |
154
+ | `recall(query, type?, tier?, session_id?, checkpoint_only?, top_k?, include_needs_review?)` | Search by natural language or identifier. Returns lightweight `ArtifactRef` list ranked by relevance + recency + importance. Records an access (reinforces retention). Code memories flagged `needs_review` are hidden unless `include_needs_review=true`. |
116
155
  | `get(ref_id)` | Fetch full content of a specific memory. Records an access. |
117
156
  | `forget(ref_id)` | Delete a memory from all indexes. |
118
157
  | `promote(ref_id, type, content?, importance?, tags?)` | Turn a short-term/working memory into a long-term one (you supply the distilled text). Keeps provenance via `promoted_from`. |
119
- | `consolidate(session_id?, limit?, synthesize?)` | Purge expired short-term memories and list the ones most worth promoting (ranked by importance × how often they were recalled). `synthesize=true` also asks an external reasoner to draft long-term statements (`suggested`). |
158
+ | `consolidate(session_id?, limit?, synthesize?, revalidate?)` | Purge expired short-term memories and list the ones most worth promoting (ranked by importance × how often they were recalled). `synthesize=true` also asks an external reasoner to draft long-term statements (`suggested`); `revalidate=true` first checks every code memory against its source file (`revalidated`). The report always carries a `health` summary. |
120
159
  | `ask(ref_id, question, mode?)` | Answer a question about one memory without loading it — for a `document` the full body is consulted. `mode`: `direct` \| `rag` \| `rlm` \| `auto`, passed to the reasoner unchanged; the built-in reasoner answers in `rag` mode with the best-matching chunks. Returns `answer`, `mode`, `citations`, `usage`. |
121
160
  | `wiki_export(wiki_dir?)` | Compile long-term memories without a wiki page into draft markdown pages (`index.md`, `log.md` updated). |
122
161
  | `wiki_import(wiki_dir?)` | Index curated wiki pages as long-term memories (idempotent; changed pages supersede). |
@@ -129,7 +168,7 @@ See the Prerequisites section inside each script for the Python environment requ
129
168
  | `convention` | long-term | Naming rules, code style, project patterns | 0.9 | Never |
130
169
  | `fact` | long-term | Durable project facts: build commands, environments, contracts | 0.7 | Never |
131
170
  | `procedure` | long-term | How-to that worked: workflows, recipes, verified skills | 0.8 | Never |
132
- | `code` | long-term | Function/class snapshots, API contracts | 0.6 | Never |
171
+ | `code` | long-term | Function/class snapshots, API contracts; identified by `(source, content)`, a changed file supersedes the old snapshot, a drifted source flags `needs_review` | 0.6 | Never (superseded on re-retain) |
133
172
  | `document` | long-term | Large artefacts (specs, transcripts, files): the body is stored and chunk-indexed, `get` returns a summary, `ask` answers from the indexed chunks (local reasoner) or the body (RLM Studio) | 0.6 | Never |
134
173
  | `episode` | short-term | Session events, compaction checkpoints | 0.4 | 30 days, extended by every recall |
135
174
  | `observation` | short-term | Findings, tool outputs, things noticed | 0.3 | 7 days, extended by every recall |
@@ -144,6 +183,7 @@ Retention: `ttl = base_ttl × (1 + access_boost × access_count)`, measured from
144
183
  3. **Before compaction** — `retain(type="episode", checkpoint=true)` (the `pre-compact.py` hook) and `consolidate()`.
145
184
  4. **After compaction / end of phase** — `promote(ref_id, type, content)` for the candidates worth keeping.
146
185
  5. **Large artefacts** — `retain(type="document", content=<body>, source=<name>)` then `ask(ref_id, question)`; never `get` a document to read it.
186
+ 7. **Code snapshots** — `retain(type="code", content=<file>, source=<path>)`; retain the file again after editing it (the new snapshot supersedes the old one). The session-start hook revalidates snapshots against their files and lists the ones that need re-retaining.
147
187
  6. **Curate** — `wiki_export` → review the drafts → `wiki_import`.
148
188
 
149
189
  ### Quick examples
@@ -199,6 +239,10 @@ All settings are read from environment variables with the `MEMKERNEL_` prefix, o
199
239
  | `MEMKERNEL_DOCUMENT_SUMMARY_CHARS` | `1000` | Characters of the body kept in a `document` memory's summary record |
200
240
  | `MEMKERNEL_REASONER` | `local` | `ReasonerPort` behind `ask`: `local` (hybrid search over chunks, no LLM) or `rlmstudio` (falls back to `local` when `rlmstudio` is not importable) |
201
241
  | `MEMKERNEL_ASK_TOP_K` | `5` | Chunks the local reasoner returns per `ask` |
242
+ | `MEMKERNEL_PROJECT_ROOT` | `.` | Directory that relative `source` paths of `code` memories resolve under; nothing outside it is ever read. Set it whenever the server or a hook is not started from the project directory |
243
+ | `MEMKERNEL_HEALTH_DUPLICATE_THRESHOLD` | `0.95` | Cosine similarity in [0, 1] from which two long-term memories count as near-duplicates in the health pass |
244
+ | `MEMKERNEL_HEALTH_DUPLICATE_LIMIT` | `500` | Most recent long-term memories the near-duplicate check covers |
245
+ | `MEMKERNEL_HEALTH_REPORT_LIMIT` | `20` | Items listed per category in a health report |
202
246
  | `MEMKERNEL_EPISODE_TTL_DAYS` | `30` | Base TTL for `episode` memories (short-term) |
203
247
  | `MEMKERNEL_OBSERVATION_TTL_DAYS` | `7` | Base TTL for `observation` memories (short-term) |
204
248
  | `MEMKERNEL_TASK_TTL_HOURS` | `24` | Base TTL for `task` memories (working) |
@@ -289,6 +333,12 @@ Long-term memory has a human-readable twin: a markdown wiki in the [LLM Wiki](ht
289
333
 
290
334
  Set `MEMKERNEL_WIKI_DIR` (for example `knowledge/wiki`) or pass `wiki_dir` to the tools. Rationale and the proposed standalone `ai-wiki` repo: [`docs/adr/ADR-008-llm-wiki-as-semantic-layer.md`](docs/adr/ADR-008-llm-wiki-as-semantic-layer.md).
291
335
 
336
+ ### Revalidation and store health
337
+
338
+ A `code` memory is a snapshot of a file. `MemoryService.revalidate()` hashes each live snapshot's whole source file and compares it with the stored content hash: a changed file flags the memory `needs_review` (`changed`), a vanished file flags it `missing`, a matching file clears the flag. Flagged memories leave default `recall` until the file is retained again (which supersedes the stale snapshot) or they are asked for with `include_needs_review=true`. Revalidation runs from `consolidate(revalidate=true)`, at every session start through `MemoryLifecycle.on_session_start()` (a `### Needs review` section lists what to re-retain), and from `POST /revalidate`. Sources resolve under `MEMKERNEL_PROJECT_ROOT` (default: the current directory); set it whenever the server or a hook is not started from the project directory. Nothing outside that root is ever read. 0.4 hashes whole files; per-chunk invalidation is a follow-up.
339
+
340
+ `MemoryService.health()` audits the store without changing it: orphaned chunk and document records, dangling `supersedes` / `superseded_by` / `promoted_from` / `parent_ref` links, supersessions from 0.2/0.3 that lack the durable marker, `code` memories whose source file is gone, memories flagged for review, expired short-term memories not yet purged and, in full mode only, near-duplicate pairs among the most recent long-term memories (cosine ≥ `MEMKERNEL_HEALTH_DUPLICATE_THRESHOLD`). The quick report (counts only, no embeddings, no content loads) is part of every `consolidate()` report and of the `memory://status` resource; the full report is `GET /health/store`. The pass never fixes anything: a fix mode is a later feature.
341
+
292
342
  ## Admin API
293
343
 
294
344
  A FastAPI admin server provides health and stats endpoints (independent of MCP):
@@ -302,6 +352,8 @@ uvicorn memkernel.api.app:app --host 127.0.0.1 --port 8766
302
352
  | `GET /health` | Server health and memory count |
303
353
  | `GET /stats` | Total memory count, per-type and per-tier breakdown, growth rate, last recall latency |
304
354
  | `GET /collections` | List ChromaDB collections |
355
+ | `GET /health/store` | Full store health report (orphans, dangling links, stale sources, near-duplicates, …) |
356
+ | `POST /revalidate` | Check every code memory against its source file; flag changed or missing ones |
305
357
 
306
358
  ---
307
359
 
@@ -324,7 +376,7 @@ Releases are published to PyPI by [`.github/workflows/release.yml`](.github/work
324
376
 
325
377
  1. Set `version` in `pyproject.toml` and move the `[Unreleased]` entries in [`CHANGELOG.md`](CHANGELOG.md) under the new version.
326
378
  2. Merge to `main` with CI green.
327
- 3. Tag and push: `git tag -a v0.3.0 -m "memkernel 0.3.0" && git push origin v0.3.0`.
379
+ 3. Tag and push: `git tag -a v0.4.0 -m "memkernel 0.4.0" && git push origin v0.4.0`.
328
380
 
329
381
  The workflow refuses a tag that does not match the `pyproject.toml` version, builds the sdist and wheel, smoke-installs the wheel, and uploads through the `pypi` environment.
330
382
 
@@ -1,4 +1,7 @@
1
- # MemKernel
1
+ <h1>
2
+ <img src="https://raw.githubusercontent.com/gosha70/memkernel/main/docs/assets/memkernel-logo.png" alt="MemKernel logo: a glowing memory core on three stacked tiers" width="110" align="absmiddle">
3
+ MemKernel
4
+ </h1>
2
5
 
3
6
  [![CI](https://github.com/gosha70/memkernel/actions/workflows/ci.yml/badge.svg)](https://github.com/gosha70/memkernel/actions/workflows/ci.yml)
4
7
  [![PyPI](https://img.shields.io/pypi/v/memkernel)](https://pypi.org/project/memkernel/)
@@ -19,23 +22,33 @@ MemKernel gives Claude Code (and any MCP-compatible copilot) persistent memory a
19
22
 
20
23
  ## Requirements
21
24
 
22
- - Python 3.11 – 3.13
25
+ - Python 3.11 – 3.13 (the package declares `Requires-Python >=3.11,<3.14`; pip reports "from versions: none" when the installing interpreter is outside that range)
23
26
  - [Poetry](https://python-poetry.org) (for development installs)
24
27
 
25
28
  ---
26
29
 
27
30
  ## Install
28
31
 
32
+ MemKernel is a Python package on PyPI. Its `memkernel` command is the MCP server. There is no npm package: an `npx` install someone suggests is a hallucination.
33
+
34
+ **Recommended: an isolated install with [pipx](https://pipx.pypa.io)**, which puts the `memkernel` command on your PATH without touching any other Python environment:
35
+
29
36
  ```bash
30
- pip install memkernel
37
+ pipx install memkernel
38
+ # if pipx's default interpreter is outside 3.11–3.13:
39
+ pipx install --python python3.11 memkernel
31
40
  ```
32
41
 
33
- Or with Poetry:
42
+ **As a library inside a project**, install it into that project's virtual environment:
34
43
 
35
44
  ```bash
36
- poetry add memkernel
45
+ pip install memkernel # or: poetry add memkernel
37
46
  ```
38
47
 
48
+ Avoid a plain `pip install memkernel` into a shared or system interpreter (Homebrew's `python3`, for example). MemKernel pins `chromadb` 1.x and recent `cffi`, so pip will replace older copies and can break unrelated tools that need them.
49
+
50
+ Verify the install with `pipx list` or `which memkernel`. Running `memkernel` with no arguments starts the MCP server on stdio and waits for a client; `memkernel --version` and `memkernel --help` print and exit.
51
+
39
52
  ---
40
53
 
41
54
  ## Add to Claude Code
@@ -48,17 +61,29 @@ Add MemKernel to `.claude/settings.json` in your project:
48
61
  {
49
62
  "mcpServers": {
50
63
  "memkernel": {
51
- "command": "memkernel",
64
+ "command": "/Users/you/.local/bin/memkernel",
52
65
  "args": [],
53
66
  "env": {
54
- "MEMKERNEL_PROJECT_ID": "my-project"
67
+ "MEMKERNEL_PROJECT_ID": "my-project",
68
+ "MEMKERNEL_CHROMA_PERSIST_PATH": "/Users/you/.memkernel/my-project"
55
69
  }
56
70
  }
57
71
  }
58
72
  }
59
73
  ```
60
74
 
61
- Replace `my-project` with a short identifier for the project — memories are isolated per project.
75
+ Replace `my-project` with a short identifier for the project — memories are isolated per project. Use the absolute path of the command (`which memkernel` prints it) because Claude Code does not necessarily see the PATH changes your shell profile makes, and an absolute `MEMKERNEL_CHROMA_PERSIST_PATH` keeps the store outside the repository. On first server initialization, MemKernel downloads the embedding model (about 90 MB) if it is not already cached: `build_service()` loads the sentence-transformers model when the MCP server starts, before any tool call, so the first connection can take a minute. Later starts and every tool call are fast.
76
+
77
+ <details>
78
+ <summary>Troubleshooting the install</summary>
79
+
80
+ - **`pipx install memkernel` says "No matching distribution found"** — pipx used an interpreter outside 3.11–3.13. Pass one explicitly: `pipx install --python /opt/homebrew/opt/python@3.11/bin/python3.11 memkernel`.
81
+ - **`bash: /some/old/path/memkernel: No such file or directory`** right after reinstalling — the shell cached the old location. Run `hash -r` (bash) or `rehash` (zsh), then `which memkernel`.
82
+ - **`memkernel --help` seems to hang** on versions before 0.4.0 — the command had no flags and started the server; press Ctrl-C. From 0.4.0 on, `--help` and `--version` print and exit.
83
+ - **The first MCP connection is slow or times out** — the server is downloading the embedding model at startup (see above). Wait for it once, or pre-warm it with `python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('all-MiniLM-L6-v2')"` in the same environment.
84
+ - **A global pip install broke another tool** (for example `langchain-chroma` wanting `chromadb < 0.6`) — uninstall memkernel from that interpreter, reinstall the versions the other tool needs, and install memkernel with pipx instead.
85
+
86
+ </details>
62
87
 
63
88
  ### 2. Add memory instructions to CLAUDE.md
64
89
 
@@ -78,16 +103,30 @@ See the Prerequisites section inside each script for the Python environment requ
78
103
 
79
104
  ---
80
105
 
106
+ ## Add to Kiro
107
+
108
+ Kiro (and the Kiro CLI, formerly Amazon Q Developer CLI) connects to MemKernel as a stdio MCP server. [`templates/kiro/`](templates/kiro/) holds the three files to copy into a workspace:
109
+
110
+ | Template | Destination | Role |
111
+ |----------|-------------|------|
112
+ | `settings/mcp.json` | `.kiro/settings/mcp.json` | Server entry with `env` and `autoApprove` |
113
+ | `steering/memory.md` | `.kiro/steering/memory.md` | Memory instructions, always included |
114
+ | `hooks/memkernel.json` | `.kiro/hooks/memkernel.json` | `SessionStart` recall, `Stop` retain, manual checkpoint |
115
+
116
+ [`templates/kiro/README.md`](templates/kiro/README.md) has the setup steps, the Kiro CLI custom-agent variant and the caveats (absolute persist path, hook format by Kiro version).
117
+
118
+ ---
119
+
81
120
  ## MCP Tools
82
121
 
83
122
  | Tool | What it does |
84
123
  |------|-------------|
85
124
  | `retain(content, type, session_id?, checkpoint?, source?, importance?, tags?, supersedes?)` | Store a memory. Returns `ref_id`. Idempotent — identical content + type returns the same `ref_id`. `supersedes` hides the memory it replaces. |
86
- | `recall(query, type?, tier?, session_id?, checkpoint_only?, top_k?)` | Search by natural language or identifier. Returns lightweight `ArtifactRef` list ranked by relevance + recency + importance. Records an access (reinforces retention). |
125
+ | `recall(query, type?, tier?, session_id?, checkpoint_only?, top_k?, include_needs_review?)` | Search by natural language or identifier. Returns lightweight `ArtifactRef` list ranked by relevance + recency + importance. Records an access (reinforces retention). Code memories flagged `needs_review` are hidden unless `include_needs_review=true`. |
87
126
  | `get(ref_id)` | Fetch full content of a specific memory. Records an access. |
88
127
  | `forget(ref_id)` | Delete a memory from all indexes. |
89
128
  | `promote(ref_id, type, content?, importance?, tags?)` | Turn a short-term/working memory into a long-term one (you supply the distilled text). Keeps provenance via `promoted_from`. |
90
- | `consolidate(session_id?, limit?, synthesize?)` | Purge expired short-term memories and list the ones most worth promoting (ranked by importance × how often they were recalled). `synthesize=true` also asks an external reasoner to draft long-term statements (`suggested`). |
129
+ | `consolidate(session_id?, limit?, synthesize?, revalidate?)` | Purge expired short-term memories and list the ones most worth promoting (ranked by importance × how often they were recalled). `synthesize=true` also asks an external reasoner to draft long-term statements (`suggested`); `revalidate=true` first checks every code memory against its source file (`revalidated`). The report always carries a `health` summary. |
91
130
  | `ask(ref_id, question, mode?)` | Answer a question about one memory without loading it — for a `document` the full body is consulted. `mode`: `direct` \| `rag` \| `rlm` \| `auto`, passed to the reasoner unchanged; the built-in reasoner answers in `rag` mode with the best-matching chunks. Returns `answer`, `mode`, `citations`, `usage`. |
92
131
  | `wiki_export(wiki_dir?)` | Compile long-term memories without a wiki page into draft markdown pages (`index.md`, `log.md` updated). |
93
132
  | `wiki_import(wiki_dir?)` | Index curated wiki pages as long-term memories (idempotent; changed pages supersede). |
@@ -100,7 +139,7 @@ See the Prerequisites section inside each script for the Python environment requ
100
139
  | `convention` | long-term | Naming rules, code style, project patterns | 0.9 | Never |
101
140
  | `fact` | long-term | Durable project facts: build commands, environments, contracts | 0.7 | Never |
102
141
  | `procedure` | long-term | How-to that worked: workflows, recipes, verified skills | 0.8 | Never |
103
- | `code` | long-term | Function/class snapshots, API contracts | 0.6 | Never |
142
+ | `code` | long-term | Function/class snapshots, API contracts; identified by `(source, content)`, a changed file supersedes the old snapshot, a drifted source flags `needs_review` | 0.6 | Never (superseded on re-retain) |
104
143
  | `document` | long-term | Large artefacts (specs, transcripts, files): the body is stored and chunk-indexed, `get` returns a summary, `ask` answers from the indexed chunks (local reasoner) or the body (RLM Studio) | 0.6 | Never |
105
144
  | `episode` | short-term | Session events, compaction checkpoints | 0.4 | 30 days, extended by every recall |
106
145
  | `observation` | short-term | Findings, tool outputs, things noticed | 0.3 | 7 days, extended by every recall |
@@ -115,6 +154,7 @@ Retention: `ttl = base_ttl × (1 + access_boost × access_count)`, measured from
115
154
  3. **Before compaction** — `retain(type="episode", checkpoint=true)` (the `pre-compact.py` hook) and `consolidate()`.
116
155
  4. **After compaction / end of phase** — `promote(ref_id, type, content)` for the candidates worth keeping.
117
156
  5. **Large artefacts** — `retain(type="document", content=<body>, source=<name>)` then `ask(ref_id, question)`; never `get` a document to read it.
157
+ 7. **Code snapshots** — `retain(type="code", content=<file>, source=<path>)`; retain the file again after editing it (the new snapshot supersedes the old one). The session-start hook revalidates snapshots against their files and lists the ones that need re-retaining.
118
158
  6. **Curate** — `wiki_export` → review the drafts → `wiki_import`.
119
159
 
120
160
  ### Quick examples
@@ -170,6 +210,10 @@ All settings are read from environment variables with the `MEMKERNEL_` prefix, o
170
210
  | `MEMKERNEL_DOCUMENT_SUMMARY_CHARS` | `1000` | Characters of the body kept in a `document` memory's summary record |
171
211
  | `MEMKERNEL_REASONER` | `local` | `ReasonerPort` behind `ask`: `local` (hybrid search over chunks, no LLM) or `rlmstudio` (falls back to `local` when `rlmstudio` is not importable) |
172
212
  | `MEMKERNEL_ASK_TOP_K` | `5` | Chunks the local reasoner returns per `ask` |
213
+ | `MEMKERNEL_PROJECT_ROOT` | `.` | Directory that relative `source` paths of `code` memories resolve under; nothing outside it is ever read. Set it whenever the server or a hook is not started from the project directory |
214
+ | `MEMKERNEL_HEALTH_DUPLICATE_THRESHOLD` | `0.95` | Cosine similarity in [0, 1] from which two long-term memories count as near-duplicates in the health pass |
215
+ | `MEMKERNEL_HEALTH_DUPLICATE_LIMIT` | `500` | Most recent long-term memories the near-duplicate check covers |
216
+ | `MEMKERNEL_HEALTH_REPORT_LIMIT` | `20` | Items listed per category in a health report |
173
217
  | `MEMKERNEL_EPISODE_TTL_DAYS` | `30` | Base TTL for `episode` memories (short-term) |
174
218
  | `MEMKERNEL_OBSERVATION_TTL_DAYS` | `7` | Base TTL for `observation` memories (short-term) |
175
219
  | `MEMKERNEL_TASK_TTL_HOURS` | `24` | Base TTL for `task` memories (working) |
@@ -260,6 +304,12 @@ Long-term memory has a human-readable twin: a markdown wiki in the [LLM Wiki](ht
260
304
 
261
305
  Set `MEMKERNEL_WIKI_DIR` (for example `knowledge/wiki`) or pass `wiki_dir` to the tools. Rationale and the proposed standalone `ai-wiki` repo: [`docs/adr/ADR-008-llm-wiki-as-semantic-layer.md`](docs/adr/ADR-008-llm-wiki-as-semantic-layer.md).
262
306
 
307
+ ### Revalidation and store health
308
+
309
+ A `code` memory is a snapshot of a file. `MemoryService.revalidate()` hashes each live snapshot's whole source file and compares it with the stored content hash: a changed file flags the memory `needs_review` (`changed`), a vanished file flags it `missing`, a matching file clears the flag. Flagged memories leave default `recall` until the file is retained again (which supersedes the stale snapshot) or they are asked for with `include_needs_review=true`. Revalidation runs from `consolidate(revalidate=true)`, at every session start through `MemoryLifecycle.on_session_start()` (a `### Needs review` section lists what to re-retain), and from `POST /revalidate`. Sources resolve under `MEMKERNEL_PROJECT_ROOT` (default: the current directory); set it whenever the server or a hook is not started from the project directory. Nothing outside that root is ever read. 0.4 hashes whole files; per-chunk invalidation is a follow-up.
310
+
311
+ `MemoryService.health()` audits the store without changing it: orphaned chunk and document records, dangling `supersedes` / `superseded_by` / `promoted_from` / `parent_ref` links, supersessions from 0.2/0.3 that lack the durable marker, `code` memories whose source file is gone, memories flagged for review, expired short-term memories not yet purged and, in full mode only, near-duplicate pairs among the most recent long-term memories (cosine ≥ `MEMKERNEL_HEALTH_DUPLICATE_THRESHOLD`). The quick report (counts only, no embeddings, no content loads) is part of every `consolidate()` report and of the `memory://status` resource; the full report is `GET /health/store`. The pass never fixes anything: a fix mode is a later feature.
312
+
263
313
  ## Admin API
264
314
 
265
315
  A FastAPI admin server provides health and stats endpoints (independent of MCP):
@@ -273,6 +323,8 @@ uvicorn memkernel.api.app:app --host 127.0.0.1 --port 8766
273
323
  | `GET /health` | Server health and memory count |
274
324
  | `GET /stats` | Total memory count, per-type and per-tier breakdown, growth rate, last recall latency |
275
325
  | `GET /collections` | List ChromaDB collections |
326
+ | `GET /health/store` | Full store health report (orphans, dangling links, stale sources, near-duplicates, …) |
327
+ | `POST /revalidate` | Check every code memory against its source file; flag changed or missing ones |
276
328
 
277
329
  ---
278
330
 
@@ -295,7 +347,7 @@ Releases are published to PyPI by [`.github/workflows/release.yml`](.github/work
295
347
 
296
348
  1. Set `version` in `pyproject.toml` and move the `[Unreleased]` entries in [`CHANGELOG.md`](CHANGELOG.md) under the new version.
297
349
  2. Merge to `main` with CI green.
298
- 3. Tag and push: `git tag -a v0.3.0 -m "memkernel 0.3.0" && git push origin v0.3.0`.
350
+ 3. Tag and push: `git tag -a v0.4.0 -m "memkernel 0.4.0" && git push origin v0.4.0`.
299
351
 
300
352
  The workflow refuses a tag that does not match the `pyproject.toml` version, builds the sdist and wheel, smoke-installs the wheel, and uploads through the `pypi` environment.
301
353
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "memkernel"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "Code-aware memory server for AI coding copilots"
5
5
  authors = ["EGOGE"]
6
6
  license = "Apache-2.0"
@@ -2,4 +2,4 @@
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  """MemKernel — Code-aware memory server for AI coding copilots."""
4
4
 
5
- __version__ = "0.3.0"
5
+ __version__ = "0.4.0"
@@ -1,6 +1,6 @@
1
1
  # Copyright (c) 2026 EGOGE
2
2
  # SPDX-License-Identifier: Apache-2.0
3
- """Admin API route handlers: /health, /stats, /collections."""
3
+ """Admin API route handlers: /health, /health/store, /stats, /collections, /revalidate."""
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -30,6 +30,14 @@ async def health(request: Request) -> dict[str, Any]:
30
30
  }
31
31
 
32
32
 
33
+ @router.get("/health/store")
34
+ async def health_store(request: Request) -> dict[str, Any]:
35
+ """Read-only store health report: orphans, dangling links, stale sources, near-duplicates and more."""
36
+ svc = _svc(request)
37
+ report = svc.health()
38
+ return {**report.model_dump(mode="json"), "summary": report.summary}
39
+
40
+
33
41
  @router.get("/stats")
34
42
  async def stats(request: Request) -> dict[str, Any]:
35
43
  """Memory metrics: per-type breakdown, growth by time window, last recall latency."""
@@ -42,3 +50,10 @@ async def collections(request: Request) -> list[dict[str, Any]]:
42
50
  """List all project collections with record counts."""
43
51
  svc = _svc(request)
44
52
  return svc.list_collections()
53
+
54
+
55
+ @router.post("/revalidate")
56
+ async def revalidate(request: Request) -> dict[str, Any]:
57
+ """Check every code memory against its source file; flag changed or missing ones (needs_review)."""
58
+ svc = _svc(request)
59
+ return svc.revalidate()
@@ -40,6 +40,14 @@ class MemKernelSettings(BaseSettings):
40
40
  reasoner: Literal["local", "rlmstudio"] = "local" # ReasonerPort behind ask(); falls back to local
41
41
  ask_top_k: int = 5 # chunks the local reasoner returns per ask()
42
42
 
43
+ # Sources, revalidation and store health (0.4)
44
+ project_root: str = "." # relative `source` paths resolve under this directory; nothing outside it is read
45
+ health_duplicate_threshold: float = (
46
+ 0.95 # cosine similarity in [0, 1] from which two long-term memories are near-duplicates
47
+ )
48
+ health_duplicate_limit: int = 500 # most recent long-term memories checked for near-duplicates
49
+ health_report_limit: int = 20 # items listed per health category
50
+
43
51
  # Lifecycle / decay (see docs/architecture/memory-tiers.md)
44
52
  episode_ttl_days: int = 30 # base TTL for short-term `episode` memories
45
53
  observation_ttl_days: int = 7 # base TTL for short-term `observation` memories
@@ -95,12 +95,48 @@ DOCUMENT_PART_KEY = "document_part"
95
95
  ASK_MODES: tuple[str, ...] = ("direct", "rag", "rlm", "auto")
96
96
  DEFAULT_ASK_MODE = "auto"
97
97
 
98
+ # Chunk records of a `code` memory (0.4): owner-specific index entries, never
99
+ # deduplicated by content, tagged and linked to their canonical record.
100
+ CODE_CHUNK_TAG = "code_chunk"
101
+ CODE_CHUNK_KEY = "code_chunk" # boolean metadata so adapters can skip chunk records
102
+ PARENT_REF_KEY = "parent_ref"
103
+ # Durable liveness marker (0.4): set on a record that another record replaced.
104
+ SUPERSEDED_BY_KEY = "superseded_by"
105
+ # Revalidation flags (0.4).
106
+ NEEDS_REVIEW_KEY = "needs_review"
107
+ REVIEW_REASON_KEY = "review_reason"
108
+ REVIEW_REASON_CHANGED = "changed"
109
+ REVIEW_REASON_MISSING = "missing"
110
+
98
111
 
99
112
  def tier_of(memory_type: MemoryType) -> MemoryTier:
100
113
  """Return the tier a memory type belongs to."""
101
114
  return TYPE_TIER[memory_type]
102
115
 
103
116
 
117
+ # Type values of the long-term tier: the health pass filters neighbours on
118
+ # these, never on the `tier` key, which records written before 0.2 lack.
119
+ LONG_TERM_TYPE_VALUES: tuple[str, ...] = tuple(t.value for t, tier in TYPE_TIER.items() if tier == MemoryTier.LONG_TERM)
120
+
121
+ # Health report categories (0.4), in report order.
122
+ HEALTH_ORPHANS = "orphans"
123
+ HEALTH_DANGLING_LINKS = "dangling_links"
124
+ HEALTH_UNMARKED_SUPERSESSIONS = "unmarked_supersessions"
125
+ HEALTH_STALE_SOURCES = "stale_sources"
126
+ HEALTH_NEEDS_REVIEW = "needs_review"
127
+ HEALTH_EXPIRED_PRESENT = "expired_present"
128
+ HEALTH_NEAR_DUPLICATES = "near_duplicates"
129
+ HEALTH_CATEGORIES: tuple[str, ...] = (
130
+ HEALTH_ORPHANS,
131
+ HEALTH_DANGLING_LINKS,
132
+ HEALTH_UNMARKED_SUPERSESSIONS,
133
+ HEALTH_STALE_SOURCES,
134
+ HEALTH_NEEDS_REVIEW,
135
+ HEALTH_EXPIRED_PRESENT,
136
+ HEALTH_NEAR_DUPLICATES,
137
+ )
138
+
139
+
104
140
  class Memory(BaseModel):
105
141
  """A stored unit of knowledge with full content and metadata."""
106
142
 
@@ -127,6 +163,43 @@ class Memory(BaseModel):
127
163
  # Set on a ``document`` summary record and on the body/chunk records the
128
164
  # document store keeps for it (ADR-009).
129
165
  document_id: str | None = None
166
+ # 0.4: durable marker on a replaced record; owner link on a code chunk
167
+ # record; revalidation flags. All load with defaults from older stores.
168
+ superseded_by: str | None = None
169
+ parent_ref: str | None = None
170
+ needs_review: bool = False
171
+ review_reason: str = ""
172
+
173
+
174
+ class HealthItem(BaseModel):
175
+ """One finding of the store health pass.
176
+
177
+ For ``near_duplicates`` an item is one pair: ``ref_id`` is the
178
+ lexicographically smaller id and ``detail`` is ``"~ <other id> (cosine x)"``;
179
+ neither side is "the original".
180
+ """
181
+
182
+ ref_id: str
183
+ detail: str
184
+
185
+
186
+ class HealthReport(BaseModel):
187
+ """Read-only store health report: per category, every finding counted and up to a bounded number listed."""
188
+
189
+ full: bool = True
190
+ counts: dict[str, int] = {}
191
+ orphans: list[HealthItem] = []
192
+ dangling_links: list[HealthItem] = []
193
+ unmarked_supersessions: list[HealthItem] = []
194
+ stale_sources: list[HealthItem] = []
195
+ needs_review: list[HealthItem] = []
196
+ expired_present: list[HealthItem] = []
197
+ near_duplicates: list[HealthItem] = []
198
+
199
+ @property
200
+ def summary(self) -> dict[str, int]:
201
+ """``{category: count}`` — the counts-only view used by the status resource and consolidate reports."""
202
+ return dict(self.counts)
130
203
 
131
204
 
132
205
  class ReasonerError(RuntimeError):
@@ -37,10 +37,16 @@ class VectorStorePort(Protocol):
37
37
 
38
38
  def delete(self, ref_id: str) -> bool: ...
39
39
 
40
- def list_all(self, where: dict[str, Any] | None = None) -> list[Memory]: ...
40
+ def list_all(self, where: dict[str, Any] | None = None, include_content: bool = True) -> list[Memory]:
41
+ """All records matching ``where``; ``include_content=False`` fetches ids and metadata only (content empty)."""
42
+ ...
43
+
44
+ def find_by_hash(self, content_hash: str, memory_type: str, source: str | None = None) -> Memory | None:
45
+ """Find by content_hash + type, optionally + source (dedup key per ADR-5 / 0.4 sourced-code identity).
41
46
 
42
- def find_by_hash(self, content_hash: str, memory_type: str) -> Memory | None:
43
- """Find by content_hash + type (dedup key per ADR-5)."""
47
+ Never returns a code chunk record (a single-chunk file cannot shadow its
48
+ canonical) nor a superseded record (history is not a dedup hit).
49
+ """
44
50
  ...
45
51
 
46
52
  def update_metadata(self, ref_id: str, metadata: dict[str, Any]) -> None: