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.
- {memkernel-0.3.0 → memkernel-0.4.1}/PKG-INFO +87 -15
- {memkernel-0.3.0 → memkernel-0.4.1}/README.md +86 -14
- {memkernel-0.3.0 → memkernel-0.4.1}/pyproject.toml +2 -1
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/__init__.py +1 -1
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/api/routes/admin.py +16 -1
- memkernel-0.4.1/src/memkernel/config.py +129 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/core/models.py +73 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/core/ports.py +9 -3
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/core/services.py +474 -44
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/lifecycle.py +34 -2
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/mcp/server.py +41 -8
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/mcp/tools/memory.py +10 -1
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/storage/chroma.py +43 -16
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/wiki/sync.py +2 -3
- memkernel-0.3.0/src/memkernel/config.py +0 -62
- {memkernel-0.3.0 → memkernel-0.4.1}/LICENSE +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/NOTICE +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/adapters/__init__.py +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/adapters/rlmstudio.py +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/api/__init__.py +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/api/app.py +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/api/routes/__init__.py +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/core/__init__.py +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/factory.py +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/mcp/__init__.py +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/mcp/tools/__init__.py +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/mcp/tools/wiki.py +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/memory/__init__.py +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/memory/dedup.py +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/memory/lifecycle.py +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/memory/types.py +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/py.typed +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/retrieval/__init__.py +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/retrieval/chunking.py +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/retrieval/embeddings.py +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/retrieval/reasoner.py +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/retrieval/search.py +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/retrieval/sparse.py +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/storage/__init__.py +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/storage/documents.py +0 -0
- {memkernel-0.3.0 → memkernel-0.4.1}/src/memkernel/wiki/__init__.py +0 -0
- {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
|
+
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
|
-
|
|
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
|
[](https://github.com/gosha70/memkernel/actions/workflows/ci.yml)
|
|
33
36
|
[](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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
| [
|
|
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
|
-
|
|
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
|
[](https://github.com/gosha70/memkernel/actions/workflows/ci.yml)
|
|
4
7
|
[](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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
| [
|
|
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
|
+
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"]
|
|
@@ -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
|
+
)
|