memkernel 0.3.0__tar.gz → 0.4.1__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 (42) hide show
  1. {memkernel-0.3.0 → memkernel-0.4.1}/PKG-INFO +87 -15
  2. {memkernel-0.3.0 → memkernel-0.4.1}/README.md +86 -14
  3. {memkernel-0.3.0 → memkernel-0.4.1}/pyproject.toml +2 -1
  4. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/__init__.py +1 -1
  5. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/api/routes/admin.py +16 -1
  6. memkernel-0.4.1/src/memkernel/config.py +129 -0
  7. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/core/models.py +73 -0
  8. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/core/ports.py +9 -3
  9. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/core/services.py +474 -44
  10. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/lifecycle.py +34 -2
  11. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/mcp/server.py +41 -8
  12. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/mcp/tools/memory.py +10 -1
  13. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/storage/chroma.py +43 -16
  14. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/wiki/sync.py +2 -3
  15. memkernel-0.3.0/src/memkernel/config.py +0 -62
  16. {memkernel-0.3.0 → memkernel-0.4.1}/LICENSE +0 -0
  17. {memkernel-0.3.0 → memkernel-0.4.1}/NOTICE +0 -0
  18. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/adapters/__init__.py +0 -0
  19. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/adapters/rlmstudio.py +0 -0
  20. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/api/__init__.py +0 -0
  21. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/api/app.py +0 -0
  22. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/api/routes/__init__.py +0 -0
  23. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/core/__init__.py +0 -0
  24. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/factory.py +0 -0
  25. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/mcp/__init__.py +0 -0
  26. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/mcp/tools/__init__.py +0 -0
  27. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/mcp/tools/wiki.py +0 -0
  28. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/memory/__init__.py +0 -0
  29. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/memory/dedup.py +0 -0
  30. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/memory/lifecycle.py +0 -0
  31. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/memory/types.py +0 -0
  32. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/py.typed +0 -0
  33. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/retrieval/__init__.py +0 -0
  34. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/retrieval/chunking.py +0 -0
  35. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/retrieval/embeddings.py +0 -0
  36. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/retrieval/reasoner.py +0 -0
  37. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/retrieval/search.py +0 -0
  38. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/retrieval/sparse.py +0 -0
  39. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/storage/__init__.py +0 -0
  40. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/storage/documents.py +0 -0
  41. {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/wiki/__init__.py +0 -0
  42. {memkernel-0.3.0 → memkernel-0.4.1}/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.1
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/)
@@ -44,27 +47,55 @@ MemKernel gives Claude Code (and any MCP-compatible copilot) persistent memory a
44
47
 
45
48
  **Tiered memory (0.2)**: memories live in three tiers with different persistence rules — *working* (the current task, hours), *short-term* (episodes and observations, decays unless recalled) and *long-term* (decisions, conventions, facts, procedures, code; never decays, replaced by supersession). Recall ranks by relevance, recency and importance; `consolidate` and `promote` move what proved useful into long-term memory; an LLM-wiki bridge keeps a human-readable, git-versioned copy of long-term knowledge. Design: [`docs/architecture/memory-tiers.md`](docs/architecture/memory-tiers.md).
46
49
 
50
+ ## What MemKernel is, and what it is not
51
+
52
+ **It is:**
53
+
54
+ - **Persistent memory for a coding agent.** What the agent retains survives context compaction and the end of a session, in a local ChromaDB store per project.
55
+ - **Explicit.** The agent decides what to retain (decisions, conventions, facts, code snapshots, tasks) and what to recall. The lifecycle hooks only automate the protocol around that: checkpoint before compaction, recovery after it, consolidation and revalidation at session start.
56
+ - **Code-aware.** AST chunking, hybrid search that finds identifiers as well as meaning, and revalidation of `code` memories against the files they came from.
57
+ - **No mandatory generative LLM.** Default retrieval uses a local sentence-transformer embedding model plus BM25 and RRF; nothing generates text inside MemKernel. `ask` with the built-in reasoner returns the best-matching chunks; synthesis is the agent's job, or an optional external reasoner's.
58
+
59
+ **It is not:**
60
+
61
+ - **A token optimizer or context compressor.** MemKernel does not shrink the context window, compress prompts, or lower model cost by itself. It adds tool calls and returned content to a session. Any token saving comes from the agent re-reading fewer files or documents after a resume; that is a side effect to measure, not a promise.
62
+ - **A replacement for files, specs and git.** Progress notes, task lists and review records already carry state between sessions. MemKernel adds retrieval by meaning and identifier, tiers with decay, supersession and revalidation on top. When a file already holds everything the next session needs, expect no gain.
63
+ - **An index of the whole repository.** `code` memories are the snapshots the agent chose to retain, not an automatic scan of every file.
64
+ - **A cross-project knowledge base.** One store per project id. Sharing is deliberate: a second server with a shared id, or the wiki bridge.
65
+
66
+ **Evidence.** `eval/run_eval.py` measures retrieval (recall@k, precision@k, MRR). An agent-level benchmark that runs the same agent with and without MemKernel, including a negative control where files already carry all state, is tracked in [#17](https://github.com/gosha70/memkernel/issues/17). Results will be published per minor release, including the cases with no gain.
67
+
47
68
  ---
48
69
 
49
70
  ## Requirements
50
71
 
51
- - Python 3.11 – 3.13
72
+ - 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
73
  - [Poetry](https://python-poetry.org) (for development installs)
53
74
 
54
75
  ---
55
76
 
56
77
  ## Install
57
78
 
79
+ 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.
80
+
81
+ **Recommended: an isolated install with [pipx](https://pipx.pypa.io)**, which puts the `memkernel` command on your PATH without touching any other Python environment:
82
+
58
83
  ```bash
59
- pip install memkernel
84
+ pipx install memkernel
85
+ # if pipx's default interpreter is outside 3.11–3.13:
86
+ pipx install --python python3.11 memkernel
60
87
  ```
61
88
 
62
- Or with Poetry:
89
+ **As a library inside a project**, install it into that project's virtual environment:
63
90
 
64
91
  ```bash
65
- poetry add memkernel
92
+ pip install memkernel # or: poetry add memkernel
66
93
  ```
67
94
 
95
+ 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.
96
+
97
+ 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.
98
+
68
99
  ---
69
100
 
70
101
  ## Add to Claude Code
@@ -77,17 +108,29 @@ Add MemKernel to `.claude/settings.json` in your project:
77
108
  {
78
109
  "mcpServers": {
79
110
  "memkernel": {
80
- "command": "memkernel",
111
+ "command": "/Users/you/.local/bin/memkernel",
81
112
  "args": [],
82
113
  "env": {
83
- "MEMKERNEL_PROJECT_ID": "my-project"
114
+ "MEMKERNEL_PROJECT_ID": "my-project",
115
+ "MEMKERNEL_CHROMA_PERSIST_PATH": "/Users/you/.memkernel/my-project"
84
116
  }
85
117
  }
86
118
  }
87
119
  }
88
120
  ```
89
121
 
90
- Replace `my-project` with a short identifier for the project — memories are isolated per project.
122
+ 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.
123
+
124
+ <details>
125
+ <summary>Troubleshooting the install</summary>
126
+
127
+ - **`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`.
128
+ - **`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`.
129
+ - **`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.
130
+ - **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.
131
+ - **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.
132
+
133
+ </details>
91
134
 
92
135
  ### 2. Add memory instructions to CLAUDE.md
93
136
 
@@ -107,16 +150,30 @@ See the Prerequisites section inside each script for the Python environment requ
107
150
 
108
151
  ---
109
152
 
153
+ ## Add to Kiro
154
+
155
+ 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:
156
+
157
+ | Template | Destination | Role |
158
+ |----------|-------------|------|
159
+ | `settings/mcp.json` | `.kiro/settings/mcp.json` | Server entry with `env` and `autoApprove` |
160
+ | `steering/memory.md` | `.kiro/steering/memory.md` | Memory instructions, always included |
161
+ | `hooks/memkernel.json` | `.kiro/hooks/memkernel.json` | `SessionStart` recall, `Stop` retain, manual checkpoint |
162
+
163
+ [`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).
164
+
165
+ ---
166
+
110
167
  ## MCP Tools
111
168
 
112
169
  | Tool | What it does |
113
170
  |------|-------------|
114
171
  | `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). |
172
+ | `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
173
  | `get(ref_id)` | Fetch full content of a specific memory. Records an access. |
117
174
  | `forget(ref_id)` | Delete a memory from all indexes. |
118
175
  | `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`). |
176
+ | `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
177
  | `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
178
  | `wiki_export(wiki_dir?)` | Compile long-term memories without a wiki page into draft markdown pages (`index.md`, `log.md` updated). |
122
179
  | `wiki_import(wiki_dir?)` | Index curated wiki pages as long-term memories (idempotent; changed pages supersede). |
@@ -129,7 +186,7 @@ See the Prerequisites section inside each script for the Python environment requ
129
186
  | `convention` | long-term | Naming rules, code style, project patterns | 0.9 | Never |
130
187
  | `fact` | long-term | Durable project facts: build commands, environments, contracts | 0.7 | Never |
131
188
  | `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 |
189
+ | `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
190
  | `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
191
  | `episode` | short-term | Session events, compaction checkpoints | 0.4 | 30 days, extended by every recall |
135
192
  | `observation` | short-term | Findings, tool outputs, things noticed | 0.3 | 7 days, extended by every recall |
@@ -144,6 +201,7 @@ Retention: `ttl = base_ttl × (1 + access_boost × access_count)`, measured from
144
201
  3. **Before compaction** — `retain(type="episode", checkpoint=true)` (the `pre-compact.py` hook) and `consolidate()`.
145
202
  4. **After compaction / end of phase** — `promote(ref_id, type, content)` for the candidates worth keeping.
146
203
  5. **Large artefacts** — `retain(type="document", content=<body>, source=<name>)` then `ask(ref_id, question)`; never `get` a document to read it.
204
+ 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
205
  6. **Curate** — `wiki_export` → review the drafts → `wiki_import`.
148
206
 
149
207
  ### Quick examples
@@ -181,6 +239,8 @@ ask(ref_id="<document ref_id>", question="Which endpoints need rate limiting?",
181
239
  ## Configuration
182
240
 
183
241
  All settings are read from environment variables with the `MEMKERNEL_` prefix, or from a `.env` file.
242
+ Entries in `.env` without the prefix (keys for other tools) are ignored; a `MEMKERNEL_` key that matches no
243
+ setting is logged as a warning at startup.
184
244
 
185
245
  | Variable | Default | Description |
186
246
  |----------|---------|-------------|
@@ -199,6 +259,10 @@ All settings are read from environment variables with the `MEMKERNEL_` prefix, o
199
259
  | `MEMKERNEL_DOCUMENT_SUMMARY_CHARS` | `1000` | Characters of the body kept in a `document` memory's summary record |
200
260
  | `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
261
  | `MEMKERNEL_ASK_TOP_K` | `5` | Chunks the local reasoner returns per `ask` |
262
+ | `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 |
263
+ | `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 |
264
+ | `MEMKERNEL_HEALTH_DUPLICATE_LIMIT` | `500` | Most recent long-term memories the near-duplicate check covers |
265
+ | `MEMKERNEL_HEALTH_REPORT_LIMIT` | `20` | Items listed per category in a health report |
202
266
  | `MEMKERNEL_EPISODE_TTL_DAYS` | `30` | Base TTL for `episode` memories (short-term) |
203
267
  | `MEMKERNEL_OBSERVATION_TTL_DAYS` | `7` | Base TTL for `observation` memories (short-term) |
204
268
  | `MEMKERNEL_TASK_TTL_HOURS` | `24` | Base TTL for `task` memories (working) |
@@ -287,7 +351,13 @@ Long-term memory has a human-readable twin: a markdown wiki in the [LLM Wiki](ht
287
351
  - `wiki_export` compiles every long-term memory that has no page yet into a **draft** page under `decisions/`, `concepts/` or `workflows/`, with the required sections and a `memkernel://<project>/<ref_id>` citation. Existing pages are never overwritten.
288
352
  - `wiki_import` indexes curated pages as long-term memories so `recall` finds the reviewed version; a changed page supersedes its earlier memory.
289
353
 
290
- 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).
354
+ Set `MEMKERNEL_WIKI_DIR` (for example `knowledge/wiki`) or pass `wiki_dir` to the tools. The standalone maintainer that owns this page schema is [wiki-studio](https://github.com/gosha70/wiki-studio) (`pip install wiki-studio`; `wiki-studio lint --health` checks what `wiki_export` writes). Rationale: [`docs/adr/ADR-008-llm-wiki-as-semantic-layer.md`](docs/adr/ADR-008-llm-wiki-as-semantic-layer.md).
355
+
356
+ ### Revalidation and store health
357
+
358
+ 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.
359
+
360
+ `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.
291
361
 
292
362
  ## Admin API
293
363
 
@@ -302,6 +372,8 @@ uvicorn memkernel.api.app:app --host 127.0.0.1 --port 8766
302
372
  | `GET /health` | Server health and memory count |
303
373
  | `GET /stats` | Total memory count, per-type and per-tier breakdown, growth rate, last recall latency |
304
374
  | `GET /collections` | List ChromaDB collections |
375
+ | `GET /health/store` | Full store health report (orphans, dangling links, stale sources, near-duplicates, …) |
376
+ | `POST /revalidate` | Check every code memory against its source file; flag changed or missing ones |
305
377
 
306
378
  ---
307
379
 
@@ -324,7 +396,7 @@ Releases are published to PyPI by [`.github/workflows/release.yml`](.github/work
324
396
 
325
397
  1. Set `version` in `pyproject.toml` and move the `[Unreleased]` entries in [`CHANGELOG.md`](CHANGELOG.md) under the new version.
326
398
  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`.
399
+ 3. Tag and push: `git tag -a v0.4.0 -m "memkernel 0.4.0" && git push origin v0.4.0`.
328
400
 
329
401
  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
402
 
@@ -359,7 +431,7 @@ MemKernel is the memory component of a small family of projects by the same auth
359
431
 
360
432
  | Project | Role | Relationship |
361
433
  |---|---|---|
362
- | [ai-wiki](https://github.com/gosha70/ai-wiki) | LLM-maintained markdown wiki (ingest / promote / query / lint) | the human-readable form of long-term memory; `wiki_export` / `wiki_import` speak its page schema (ADR-008) |
434
+ | [wiki-studio](https://github.com/gosha70/wiki-studio) | LLM-wiki maintainer (ingest / promote / query / lint / audit), stdlib-only | the human-readable form of long-term memory; `wiki_export` / `wiki_import` speak its page schema (ADR-008), and CI lints exporter output with it |
363
435
  | [rlm-studio](https://github.com/gosha70/rlm-studio) | Recursive Language Models workbench: Direct / RAG / RLM / auto over large documents | the document layer behind `document` memories and the `ask()` tool (`MEMKERNEL_REASONER=rlmstudio`, ADR-009) |
364
436
  | [code-copilot-team](https://github.com/gosha70/code-copilot-team) | Enforceable harness configuration for Claude Code, Pi and other copilots | consumes MemKernel through MCP and hooks |
365
437
  | [haa](https://github.com/gosha70/haa) | Home-built AI coding harness, future component assembler | consumes MemKernel in-process through `build_service()` |
@@ -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/)
@@ -15,27 +18,55 @@ MemKernel gives Claude Code (and any MCP-compatible copilot) persistent memory a
15
18
 
16
19
  **Tiered memory (0.2)**: memories live in three tiers with different persistence rules — *working* (the current task, hours), *short-term* (episodes and observations, decays unless recalled) and *long-term* (decisions, conventions, facts, procedures, code; never decays, replaced by supersession). Recall ranks by relevance, recency and importance; `consolidate` and `promote` move what proved useful into long-term memory; an LLM-wiki bridge keeps a human-readable, git-versioned copy of long-term knowledge. Design: [`docs/architecture/memory-tiers.md`](docs/architecture/memory-tiers.md).
17
20
 
21
+ ## What MemKernel is, and what it is not
22
+
23
+ **It is:**
24
+
25
+ - **Persistent memory for a coding agent.** What the agent retains survives context compaction and the end of a session, in a local ChromaDB store per project.
26
+ - **Explicit.** The agent decides what to retain (decisions, conventions, facts, code snapshots, tasks) and what to recall. The lifecycle hooks only automate the protocol around that: checkpoint before compaction, recovery after it, consolidation and revalidation at session start.
27
+ - **Code-aware.** AST chunking, hybrid search that finds identifiers as well as meaning, and revalidation of `code` memories against the files they came from.
28
+ - **No mandatory generative LLM.** Default retrieval uses a local sentence-transformer embedding model plus BM25 and RRF; nothing generates text inside MemKernel. `ask` with the built-in reasoner returns the best-matching chunks; synthesis is the agent's job, or an optional external reasoner's.
29
+
30
+ **It is not:**
31
+
32
+ - **A token optimizer or context compressor.** MemKernel does not shrink the context window, compress prompts, or lower model cost by itself. It adds tool calls and returned content to a session. Any token saving comes from the agent re-reading fewer files or documents after a resume; that is a side effect to measure, not a promise.
33
+ - **A replacement for files, specs and git.** Progress notes, task lists and review records already carry state between sessions. MemKernel adds retrieval by meaning and identifier, tiers with decay, supersession and revalidation on top. When a file already holds everything the next session needs, expect no gain.
34
+ - **An index of the whole repository.** `code` memories are the snapshots the agent chose to retain, not an automatic scan of every file.
35
+ - **A cross-project knowledge base.** One store per project id. Sharing is deliberate: a second server with a shared id, or the wiki bridge.
36
+
37
+ **Evidence.** `eval/run_eval.py` measures retrieval (recall@k, precision@k, MRR). An agent-level benchmark that runs the same agent with and without MemKernel, including a negative control where files already carry all state, is tracked in [#17](https://github.com/gosha70/memkernel/issues/17). Results will be published per minor release, including the cases with no gain.
38
+
18
39
  ---
19
40
 
20
41
  ## Requirements
21
42
 
22
- - Python 3.11 – 3.13
43
+ - 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
44
  - [Poetry](https://python-poetry.org) (for development installs)
24
45
 
25
46
  ---
26
47
 
27
48
  ## Install
28
49
 
50
+ 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.
51
+
52
+ **Recommended: an isolated install with [pipx](https://pipx.pypa.io)**, which puts the `memkernel` command on your PATH without touching any other Python environment:
53
+
29
54
  ```bash
30
- pip install memkernel
55
+ pipx install memkernel
56
+ # if pipx's default interpreter is outside 3.11–3.13:
57
+ pipx install --python python3.11 memkernel
31
58
  ```
32
59
 
33
- Or with Poetry:
60
+ **As a library inside a project**, install it into that project's virtual environment:
34
61
 
35
62
  ```bash
36
- poetry add memkernel
63
+ pip install memkernel # or: poetry add memkernel
37
64
  ```
38
65
 
66
+ 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.
67
+
68
+ 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.
69
+
39
70
  ---
40
71
 
41
72
  ## Add to Claude Code
@@ -48,17 +79,29 @@ Add MemKernel to `.claude/settings.json` in your project:
48
79
  {
49
80
  "mcpServers": {
50
81
  "memkernel": {
51
- "command": "memkernel",
82
+ "command": "/Users/you/.local/bin/memkernel",
52
83
  "args": [],
53
84
  "env": {
54
- "MEMKERNEL_PROJECT_ID": "my-project"
85
+ "MEMKERNEL_PROJECT_ID": "my-project",
86
+ "MEMKERNEL_CHROMA_PERSIST_PATH": "/Users/you/.memkernel/my-project"
55
87
  }
56
88
  }
57
89
  }
58
90
  }
59
91
  ```
60
92
 
61
- Replace `my-project` with a short identifier for the project — memories are isolated per project.
93
+ 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.
94
+
95
+ <details>
96
+ <summary>Troubleshooting the install</summary>
97
+
98
+ - **`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`.
99
+ - **`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`.
100
+ - **`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.
101
+ - **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.
102
+ - **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.
103
+
104
+ </details>
62
105
 
63
106
  ### 2. Add memory instructions to CLAUDE.md
64
107
 
@@ -78,16 +121,30 @@ See the Prerequisites section inside each script for the Python environment requ
78
121
 
79
122
  ---
80
123
 
124
+ ## Add to Kiro
125
+
126
+ 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:
127
+
128
+ | Template | Destination | Role |
129
+ |----------|-------------|------|
130
+ | `settings/mcp.json` | `.kiro/settings/mcp.json` | Server entry with `env` and `autoApprove` |
131
+ | `steering/memory.md` | `.kiro/steering/memory.md` | Memory instructions, always included |
132
+ | `hooks/memkernel.json` | `.kiro/hooks/memkernel.json` | `SessionStart` recall, `Stop` retain, manual checkpoint |
133
+
134
+ [`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).
135
+
136
+ ---
137
+
81
138
  ## MCP Tools
82
139
 
83
140
  | Tool | What it does |
84
141
  |------|-------------|
85
142
  | `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). |
143
+ | `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
144
  | `get(ref_id)` | Fetch full content of a specific memory. Records an access. |
88
145
  | `forget(ref_id)` | Delete a memory from all indexes. |
89
146
  | `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`). |
147
+ | `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
148
  | `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
149
  | `wiki_export(wiki_dir?)` | Compile long-term memories without a wiki page into draft markdown pages (`index.md`, `log.md` updated). |
93
150
  | `wiki_import(wiki_dir?)` | Index curated wiki pages as long-term memories (idempotent; changed pages supersede). |
@@ -100,7 +157,7 @@ See the Prerequisites section inside each script for the Python environment requ
100
157
  | `convention` | long-term | Naming rules, code style, project patterns | 0.9 | Never |
101
158
  | `fact` | long-term | Durable project facts: build commands, environments, contracts | 0.7 | Never |
102
159
  | `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 |
160
+ | `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
161
  | `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
162
  | `episode` | short-term | Session events, compaction checkpoints | 0.4 | 30 days, extended by every recall |
106
163
  | `observation` | short-term | Findings, tool outputs, things noticed | 0.3 | 7 days, extended by every recall |
@@ -115,6 +172,7 @@ Retention: `ttl = base_ttl × (1 + access_boost × access_count)`, measured from
115
172
  3. **Before compaction** — `retain(type="episode", checkpoint=true)` (the `pre-compact.py` hook) and `consolidate()`.
116
173
  4. **After compaction / end of phase** — `promote(ref_id, type, content)` for the candidates worth keeping.
117
174
  5. **Large artefacts** — `retain(type="document", content=<body>, source=<name>)` then `ask(ref_id, question)`; never `get` a document to read it.
175
+ 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
176
  6. **Curate** — `wiki_export` → review the drafts → `wiki_import`.
119
177
 
120
178
  ### Quick examples
@@ -152,6 +210,8 @@ ask(ref_id="<document ref_id>", question="Which endpoints need rate limiting?",
152
210
  ## Configuration
153
211
 
154
212
  All settings are read from environment variables with the `MEMKERNEL_` prefix, or from a `.env` file.
213
+ Entries in `.env` without the prefix (keys for other tools) are ignored; a `MEMKERNEL_` key that matches no
214
+ setting is logged as a warning at startup.
155
215
 
156
216
  | Variable | Default | Description |
157
217
  |----------|---------|-------------|
@@ -170,6 +230,10 @@ All settings are read from environment variables with the `MEMKERNEL_` prefix, o
170
230
  | `MEMKERNEL_DOCUMENT_SUMMARY_CHARS` | `1000` | Characters of the body kept in a `document` memory's summary record |
171
231
  | `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
232
  | `MEMKERNEL_ASK_TOP_K` | `5` | Chunks the local reasoner returns per `ask` |
233
+ | `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 |
234
+ | `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 |
235
+ | `MEMKERNEL_HEALTH_DUPLICATE_LIMIT` | `500` | Most recent long-term memories the near-duplicate check covers |
236
+ | `MEMKERNEL_HEALTH_REPORT_LIMIT` | `20` | Items listed per category in a health report |
173
237
  | `MEMKERNEL_EPISODE_TTL_DAYS` | `30` | Base TTL for `episode` memories (short-term) |
174
238
  | `MEMKERNEL_OBSERVATION_TTL_DAYS` | `7` | Base TTL for `observation` memories (short-term) |
175
239
  | `MEMKERNEL_TASK_TTL_HOURS` | `24` | Base TTL for `task` memories (working) |
@@ -258,7 +322,13 @@ Long-term memory has a human-readable twin: a markdown wiki in the [LLM Wiki](ht
258
322
  - `wiki_export` compiles every long-term memory that has no page yet into a **draft** page under `decisions/`, `concepts/` or `workflows/`, with the required sections and a `memkernel://<project>/<ref_id>` citation. Existing pages are never overwritten.
259
323
  - `wiki_import` indexes curated pages as long-term memories so `recall` finds the reviewed version; a changed page supersedes its earlier memory.
260
324
 
261
- 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).
325
+ Set `MEMKERNEL_WIKI_DIR` (for example `knowledge/wiki`) or pass `wiki_dir` to the tools. The standalone maintainer that owns this page schema is [wiki-studio](https://github.com/gosha70/wiki-studio) (`pip install wiki-studio`; `wiki-studio lint --health` checks what `wiki_export` writes). Rationale: [`docs/adr/ADR-008-llm-wiki-as-semantic-layer.md`](docs/adr/ADR-008-llm-wiki-as-semantic-layer.md).
326
+
327
+ ### Revalidation and store health
328
+
329
+ 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.
330
+
331
+ `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.
262
332
 
263
333
  ## Admin API
264
334
 
@@ -273,6 +343,8 @@ uvicorn memkernel.api.app:app --host 127.0.0.1 --port 8766
273
343
  | `GET /health` | Server health and memory count |
274
344
  | `GET /stats` | Total memory count, per-type and per-tier breakdown, growth rate, last recall latency |
275
345
  | `GET /collections` | List ChromaDB collections |
346
+ | `GET /health/store` | Full store health report (orphans, dangling links, stale sources, near-duplicates, …) |
347
+ | `POST /revalidate` | Check every code memory against its source file; flag changed or missing ones |
276
348
 
277
349
  ---
278
350
 
@@ -295,7 +367,7 @@ Releases are published to PyPI by [`.github/workflows/release.yml`](.github/work
295
367
 
296
368
  1. Set `version` in `pyproject.toml` and move the `[Unreleased]` entries in [`CHANGELOG.md`](CHANGELOG.md) under the new version.
297
369
  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`.
370
+ 3. Tag and push: `git tag -a v0.4.0 -m "memkernel 0.4.0" && git push origin v0.4.0`.
299
371
 
300
372
  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
373
 
@@ -330,7 +402,7 @@ MemKernel is the memory component of a small family of projects by the same auth
330
402
 
331
403
  | Project | Role | Relationship |
332
404
  |---|---|---|
333
- | [ai-wiki](https://github.com/gosha70/ai-wiki) | LLM-maintained markdown wiki (ingest / promote / query / lint) | the human-readable form of long-term memory; `wiki_export` / `wiki_import` speak its page schema (ADR-008) |
405
+ | [wiki-studio](https://github.com/gosha70/wiki-studio) | LLM-wiki maintainer (ingest / promote / query / lint / audit), stdlib-only | the human-readable form of long-term memory; `wiki_export` / `wiki_import` speak its page schema (ADR-008), and CI lints exporter output with it |
334
406
  | [rlm-studio](https://github.com/gosha70/rlm-studio) | Recursive Language Models workbench: Direct / RAG / RLM / auto over large documents | the document layer behind `document` memories and the `ask()` tool (`MEMKERNEL_REASONER=rlmstudio`, ADR-009) |
335
407
  | [code-copilot-team](https://github.com/gosha70/code-copilot-team) | Enforceable harness configuration for Claude Code, Pi and other copilots | consumes MemKernel through MCP and hooks |
336
408
  | [haa](https://github.com/gosha70/haa) | Home-built AI coding harness, future component assembler | consumes MemKernel in-process through `build_service()` |
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "memkernel"
3
- version = "0.3.0"
3
+ version = "0.4.1"
4
4
  description = "Code-aware memory server for AI coding copilots"
5
5
  authors = ["EGOGE"]
6
6
  license = "Apache-2.0"
@@ -40,6 +40,7 @@ ruff = ">=0.9"
40
40
  mypy = ">=1.13"
41
41
  httpx = ">=0.27"
42
42
  pip-audit = "^2.10.0"
43
+ wiki-studio = ">=0.1,<1"
43
44
 
44
45
  [build-system]
45
46
  requires = ["poetry-core"]
@@ -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.1"
@@ -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()
@@ -0,0 +1,129 @@
1
+ # Copyright (c) 2026 EGOGE
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """MemKernel configuration via environment variables."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ from typing import TYPE_CHECKING, Any, Literal
9
+
10
+ from pydantic_settings import BaseSettings, EnvSettingsSource, PydanticBaseSettingsSource, SettingsConfigDict
11
+
12
+ if TYPE_CHECKING:
13
+ from pydantic.fields import FieldInfo
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ ENV_PREFIX = "MEMKERNEL_"
18
+ ENV_FILE = ".env"
19
+
20
+
21
+ class MemKernelSettings(BaseSettings):
22
+ """All settings are configurable via MEMKERNEL_ prefixed env vars."""
23
+
24
+ # Unknown keys stay forbidden (constructor typos raise), but the .env source
25
+ # is filtered to known settings: a project .env usually holds keys for other
26
+ # tools and must not stop the server from starting (issue #15). Misspelled
27
+ # MEMKERNEL_ keys are reported by settings_customise_sources().
28
+ model_config = SettingsConfigDict(env_prefix=ENV_PREFIX, env_file=ENV_FILE)
29
+
30
+ project_id: str = "default"
31
+ default_session_id: str = "default"
32
+
33
+ # ChromaDB
34
+ chroma_persist_path: str = ".memkernel/chroma"
35
+ chroma_collection_prefix: str = "memkernel"
36
+
37
+ # Embedding
38
+ embedding_model: str = "all-MiniLM-L6-v2"
39
+ embedding_dimension: int = 384
40
+
41
+ # Retrieval
42
+ search_top_k: int = 5
43
+ rrf_k: int = 60
44
+ preview_length: int = 200
45
+
46
+ # Chunking
47
+ chunk_size: int = 1500 # max chars per chunk
48
+ chunk_overlap: int = 200 # char overlap between token chunks (fallback only)
49
+
50
+ # Documents and ask() (ADR-009)
51
+ document_store: Literal["local"] = "local" # where `document` bodies live
52
+ document_summary_chars: int = 1000 # characters of the body kept in the document's summary record
53
+ reasoner: Literal["local", "rlmstudio"] = "local" # ReasonerPort behind ask(); falls back to local
54
+ ask_top_k: int = 5 # chunks the local reasoner returns per ask()
55
+
56
+ # Sources, revalidation and store health (0.4)
57
+ project_root: str = "." # relative `source` paths resolve under this directory; nothing outside it is read
58
+ health_duplicate_threshold: float = (
59
+ 0.95 # cosine similarity in [0, 1] from which two long-term memories are near-duplicates
60
+ )
61
+ health_duplicate_limit: int = 500 # most recent long-term memories checked for near-duplicates
62
+ health_report_limit: int = 20 # items listed per health category
63
+
64
+ # Lifecycle / decay (see docs/architecture/memory-tiers.md)
65
+ episode_ttl_days: int = 30 # base TTL for short-term `episode` memories
66
+ observation_ttl_days: int = 7 # base TTL for short-term `observation` memories
67
+ task_ttl_hours: int = 24 # base TTL for working-memory `task` records
68
+ access_boost: float = 0.5 # each recall extends TTL by this fraction of the base TTL
69
+ recency_half_life_days: float = 7.0 # recency decay half-life for short-term memories
70
+
71
+ # Recall scoring: final = w_relevance * fused + w_recency * recency + w_importance * importance
72
+ recall_weight_relevance: float = 1.0
73
+ recall_weight_recency: float = 0.3
74
+ recall_weight_importance: float = 0.3
75
+
76
+ # LLM wiki bridge: directory holding the markdown wiki (index.md, decisions/, ...).
77
+ # Empty = no wiki configured; wiki_export/wiki_import then require an explicit path.
78
+ wiki_dir: str = ""
79
+
80
+ # Server
81
+ transport: Literal["stdio", "streamable-http"] = "stdio"
82
+ host: str = "127.0.0.1"
83
+ port: int = 8765
84
+
85
+ @classmethod
86
+ def settings_customise_sources(
87
+ cls,
88
+ settings_cls: type[BaseSettings],
89
+ init_settings: PydanticBaseSettingsSource,
90
+ env_settings: PydanticBaseSettingsSource,
91
+ dotenv_settings: PydanticBaseSettingsSource,
92
+ file_secret_settings: PydanticBaseSettingsSource,
93
+ ) -> tuple[PydanticBaseSettingsSource, ...]:
94
+ """Default source order with the .env source limited to known settings.
95
+
96
+ Warns about MEMKERNEL_ keys in the environment or in .env that match no setting.
97
+ """
98
+ for source, origin in ((env_settings, "the environment"), (dotenv_settings, ENV_FILE)):
99
+ for key in unknown_prefixed_keys(settings_cls, source):
100
+ logger.warning(
101
+ "memkernel: unknown setting %s in %s is ignored; check the spelling against the README", key, origin
102
+ )
103
+ return init_settings, env_settings, KnownFieldsOnly(dotenv_settings), file_secret_settings
104
+
105
+
106
+ class KnownFieldsOnly(PydanticBaseSettingsSource):
107
+ """Pass through only the values of ``inner`` that belong to a settings field."""
108
+
109
+ def __init__(self, inner: PydanticBaseSettingsSource) -> None:
110
+ super().__init__(inner.settings_cls)
111
+ self._inner = inner
112
+
113
+ def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]:
114
+ return self._inner.get_field_value(field, field_name)
115
+
116
+ def __call__(self) -> dict[str, Any]:
117
+ return {key: value for key, value in self._inner().items() if key in self.settings_cls.model_fields}
118
+
119
+
120
+ def unknown_prefixed_keys(settings_cls: type[BaseSettings], source: PydanticBaseSettingsSource) -> list[str]:
121
+ """Return the MEMKERNEL_ keys ``source`` holds that are not fields of ``settings_cls``, upper-cased."""
122
+ if not isinstance(source, EnvSettingsSource):
123
+ return []
124
+ prefix = ENV_PREFIX.lower() # case-insensitive sources expose lower-cased keys
125
+ return sorted(
126
+ key.upper()
127
+ for key in source.env_vars
128
+ if key.lower().startswith(prefix) and key.lower()[len(prefix) :] not in settings_cls.model_fields
129
+ )