memkernel 0.4.1__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {memkernel-0.4.1 → memkernel-0.6.0}/PKG-INFO +40 -17
- {memkernel-0.4.1 → memkernel-0.6.0}/README.md +37 -16
- {memkernel-0.4.1 → memkernel-0.6.0}/pyproject.toml +7 -2
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/__init__.py +1 -1
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/config.py +3 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/core/services.py +68 -7
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/lifecycle.py +92 -22
- memkernel-0.6.0/src/memkernel/mcp/tools/wiki.py +127 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/storage/chroma.py +53 -13
- memkernel-0.6.0/src/memkernel/wiki/studio.py +555 -0
- memkernel-0.6.0/src/memkernel/wiki/sync.py +504 -0
- memkernel-0.4.1/src/memkernel/mcp/tools/wiki.py +0 -59
- memkernel-0.4.1/src/memkernel/wiki/sync.py +0 -228
- {memkernel-0.4.1 → memkernel-0.6.0}/LICENSE +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/NOTICE +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/adapters/__init__.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/adapters/rlmstudio.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/api/__init__.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/api/app.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/api/routes/__init__.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/api/routes/admin.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/core/__init__.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/core/models.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/core/ports.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/factory.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/mcp/__init__.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/mcp/server.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/mcp/tools/__init__.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/mcp/tools/memory.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/memory/__init__.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/memory/dedup.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/memory/lifecycle.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/memory/types.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/py.typed +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/retrieval/__init__.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/retrieval/chunking.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/retrieval/embeddings.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/retrieval/reasoner.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/retrieval/search.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/retrieval/sparse.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/storage/__init__.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/storage/documents.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/wiki/__init__.py +0 -0
- {memkernel-0.4.1 → memkernel-0.6.0}/src/memkernel/wiki/pages.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memkernel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Code-aware memory server for AI coding copilots
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Provides-Extra: wiki
|
|
16
17
|
Requires-Dist: chromadb (>=1.0,<2)
|
|
17
18
|
Requires-Dist: fastapi (>=0.115,<1)
|
|
18
19
|
Requires-Dist: mcp (>=1.25,<2)
|
|
@@ -22,6 +23,7 @@ Requires-Dist: rank-bm25 (>=0.2)
|
|
|
22
23
|
Requires-Dist: sentence-transformers (>=4.0,<6)
|
|
23
24
|
Requires-Dist: tree-sitter (>=0.22,<1)
|
|
24
25
|
Requires-Dist: tree-sitter-python (>=0.23)
|
|
26
|
+
Requires-Dist: wiki-studio (>=0.7.0,<1) ; extra == "wiki"
|
|
25
27
|
Project-URL: Documentation, https://memkernel.dev
|
|
26
28
|
Project-URL: Homepage, https://memkernel.dev
|
|
27
29
|
Project-URL: Repository, https://github.com/gosha70/memkernel
|
|
@@ -63,7 +65,7 @@ MemKernel gives Claude Code (and any MCP-compatible copilot) persistent memory a
|
|
|
63
65
|
- **An index of the whole repository.** `code` memories are the snapshots the agent chose to retain, not an automatic scan of every file.
|
|
64
66
|
- **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
67
|
|
|
66
|
-
**Evidence.** `eval/run_eval.py` measures retrieval (recall@k, precision@k, MRR).
|
|
68
|
+
**Evidence.** `eval/run_eval.py` measures retrieval (recall@k, precision@k, MRR). The 0.5 lifecycle evaluation, a **maintainer-run supplemental evaluation** (memkernel's own runner, preregistered and announced in [#17](https://github.com/gosha70/memkernel/issues/17) before the run), ran the same agent in seven arms on 20 tasks: [report](eval/lifecycle-suite/report-0.5.0.md). **No gain is claimed.** Against files alone (`claude_md`), memkernel's memory store was ahead on 6 tasks, behind on none and tied on 14 (Holm-adjusted p 0.0625: "no detectable difference"; with so many ties this task set could not have reached significance); against earlier transcripts (`fs_grep`) it was equivalent. Two of the four task groups hit a ceiling, every arm passing every trial. The report's disclosures list its deviations and known defects. Results are published with the cases that show no gain; the 0.5 report is the latest evaluation, and 0.6.0 had no new measured run.
|
|
67
69
|
|
|
68
70
|
---
|
|
69
71
|
|
|
@@ -84,6 +86,9 @@ MemKernel is a Python package on PyPI. Its `memkernel` command is the MCP server
|
|
|
84
86
|
pipx install memkernel
|
|
85
87
|
# if pipx's default interpreter is outside 3.11–3.13:
|
|
86
88
|
pipx install --python python3.11 memkernel
|
|
89
|
+
# optional: wiki-studio lint and health findings on wiki_export (ADR-010)
|
|
90
|
+
pipx install "memkernel[wiki]"
|
|
91
|
+
pipx inject memkernel "wiki-studio>=0.7.0,<1" # add it to an existing install
|
|
87
92
|
```
|
|
88
93
|
|
|
89
94
|
**As a library inside a project**, install it into that project's virtual environment:
|
|
@@ -142,9 +147,11 @@ Copy the hook scripts from [`templates/hooks/`](templates/hooks/) to `.claude/ho
|
|
|
142
147
|
|
|
143
148
|
| Hook | Claude Code event | What it does |
|
|
144
149
|
|------|-------------------|--------------|
|
|
145
|
-
| `session-start.py` | `SessionStart` |
|
|
146
|
-
| `pre-compact.py` | `PreCompact` |
|
|
147
|
-
| `post-compact.py` | `PostCompact` |
|
|
150
|
+
| `session-start.py` | `SessionStart` | By `source`: on `startup` and `clear`, consolidates, recovers the latest checkpoint and long-term context, lists promotion candidates; on `compact`, the recovery after a compaction (the latest checkpoint the agent saved, with its age, and long-term context; no consolidation); on `resume` and `fork`, nothing |
|
|
151
|
+
| `pre-compact.py` | `PreCompact` | Runs a consolidation pass (Claude Code passes no summary here, so no checkpoint is retained) |
|
|
152
|
+
| `post-compact.py` | `PostCompact` | Retains Claude Code's compaction summary (`compact_summary`) as a checkpoint for the next session; prints nothing for the model |
|
|
153
|
+
|
|
154
|
+
Register `SessionStart` without a matcher, so it runs for every `source`. Claude Code adds only `SessionStart`'s output to the model's context, not `PreCompact`'s or `PostCompact`'s, so the recovery after a compaction comes from `SessionStart` with `source: "compact"`. After upgrading memkernel, copy the three scripts again.
|
|
148
155
|
|
|
149
156
|
See the Prerequisites section inside each script for the Python environment requirement. Each script is a thin wrapper over [`MemoryLifecycle`](#embedding-memkernel-in-your-own-harness), so an in-process harness gets the same behaviour without the scripts.
|
|
150
157
|
|
|
@@ -176,7 +183,7 @@ Kiro (and the Kiro CLI, formerly Amazon Q Developer CLI) connects to MemKernel a
|
|
|
176
183
|
| `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. |
|
|
177
184
|
| `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`. |
|
|
178
185
|
| `wiki_export(wiki_dir?)` | Compile long-term memories without a wiki page into draft markdown pages (`index.md`, `log.md` updated). |
|
|
179
|
-
| `wiki_import(wiki_dir?)` | Index
|
|
186
|
+
| `wiki_import(wiki_dir?)` | Index wiki pages as long-term memories: approved revisions only with wiki-studio ≥ 0.7.0, every page without it (idempotent; changed pages supersede; `corrects` supersedes the memories a page corrects). |
|
|
180
187
|
|
|
181
188
|
### Memory types and tiers
|
|
182
189
|
|
|
@@ -196,13 +203,13 @@ Retention: `ttl = base_ttl × (1 + access_boost × access_count)`, measured from
|
|
|
196
203
|
|
|
197
204
|
### Session protocol
|
|
198
205
|
|
|
199
|
-
1. **Session start** — `recall(tier="long_term", query=<task>)`, plus the latest checkpoint (`checkpoint_only=true`). The `session-start.py` hook does this and prints promotion candidates.
|
|
206
|
+
1. **Session start** — `recall(tier="long_term", query=<task>)`, plus the latest checkpoint (`checkpoint_only=true`). The `session-start.py` hook does this and prints promotion candidates; after a compaction it prints a shorter recovery block instead.
|
|
200
207
|
2. **While working** — `retain(type="task")` when the plan changes; `retain(type="observation")` for findings; durable knowledge as `decision` / `convention` / `fact` / `procedure`.
|
|
201
|
-
3. **Before compaction** — `retain(type="episode", checkpoint=true)`
|
|
208
|
+
3. **Before compaction** — `retain(type="episode", checkpoint=true)` with your own summary of where the work stands; the `pre-compact.py` hook runs `consolidate()`, and the `post-compact.py` hook keeps Claude Code's compaction summary as a checkpoint.
|
|
202
209
|
4. **After compaction / end of phase** — `promote(ref_id, type, content)` for the candidates worth keeping.
|
|
203
210
|
5. **Large artefacts** — `retain(type="document", content=<body>, source=<name>)` then `ask(ref_id, question)`; never `get` a document to read it.
|
|
204
211
|
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.
|
|
205
|
-
6. **Curate** — `wiki_export` → review the drafts → `wiki_import`.
|
|
212
|
+
6. **Curate** — `wiki_export` → review and approve the drafts in wiki-studio → `wiki_import`.
|
|
206
213
|
|
|
207
214
|
### Quick examples
|
|
208
215
|
|
|
@@ -246,7 +253,7 @@ setting is logged as a warning at startup.
|
|
|
246
253
|
|----------|---------|-------------|
|
|
247
254
|
| `MEMKERNEL_PROJECT_ID` | `default` | Project namespace — memories are isolated per project |
|
|
248
255
|
| `MEMKERNEL_DEFAULT_SESSION_ID` | `default` | Session tag applied when `session_id` is omitted from `retain` |
|
|
249
|
-
| `MEMKERNEL_CHROMA_PERSIST_PATH` | `.memkernel/chroma` | ChromaDB data directory. Empty string = in-memory (ephemeral) |
|
|
256
|
+
| `MEMKERNEL_CHROMA_PERSIST_PATH` | `.memkernel/chroma` | ChromaDB data directory. Empty string = in-memory (ephemeral). On POSIX systems (Linux, macOS), opening it takes a short lock on `.memkernel-open.lock` inside it, so the MCP server and a hook opening a new store at once do not collide; where `fcntl` is unavailable (Windows) the store opens without the lock, and two processes creating a new store at the same moment can still fail |
|
|
250
257
|
| `MEMKERNEL_CHROMA_COLLECTION_PREFIX` | `memkernel` | Prefix for ChromaDB collection names (`{prefix}_{project_id}`) |
|
|
251
258
|
| `MEMKERNEL_EMBEDDING_MODEL` | `all-MiniLM-L6-v2` | Sentence-transformers model name |
|
|
252
259
|
| `MEMKERNEL_EMBEDDING_DIMENSION` | `384` | Output dimension of the embedding model |
|
|
@@ -272,6 +279,7 @@ setting is logged as a warning at startup.
|
|
|
272
279
|
| `MEMKERNEL_RECALL_WEIGHT_RECENCY` | `0.3` | Weight of recency in the final recall score |
|
|
273
280
|
| `MEMKERNEL_RECALL_WEIGHT_IMPORTANCE` | `0.3` | Weight of importance in the final recall score |
|
|
274
281
|
| `MEMKERNEL_WIKI_DIR` | `` | Directory of the LLM wiki used by `wiki_export` / `wiki_import` (empty = pass `wiki_dir` explicitly) |
|
|
282
|
+
| `MEMKERNEL_WIKI_PROPOSALS_DIR` | `""` | Where wiki-studio patch-sets are written before promote (`memkernel[wiki]`); empty means `<project root>/.memkernel/wiki-proposals`. Setting it requests the gate: without wiki-studio installed, a warning is logged. |
|
|
275
283
|
| `MEMKERNEL_TRANSPORT` | `stdio` | `stdio` or `streamable-http` |
|
|
276
284
|
| `MEMKERNEL_HOST` | `127.0.0.1` | Host for `streamable-http` transport |
|
|
277
285
|
| `MEMKERNEL_PORT` | `8765` | Port for `streamable-http` transport |
|
|
@@ -317,7 +325,7 @@ report = svc.consolidate() # {"purged": n, "candidates": [...]}
|
|
|
317
325
|
|
|
318
326
|
### The session protocol as a component
|
|
319
327
|
|
|
320
|
-
`MemoryLifecycle` packages what the hook scripts do, so a harness calls
|
|
328
|
+
`MemoryLifecycle` packages what the hook scripts do, so a harness calls five methods instead of re-implementing the protocol. Every method returns markdown ready for injection and does nothing but talk to the service:
|
|
321
329
|
|
|
322
330
|
```python
|
|
323
331
|
from memkernel.factory import build_service
|
|
@@ -326,9 +334,13 @@ from memkernel.lifecycle import MemoryLifecycle
|
|
|
326
334
|
memory = MemoryLifecycle(build_service())
|
|
327
335
|
system_context = memory.on_session_start() # checkpoint, long-term context, promotion candidates, wiki pointer
|
|
328
336
|
...
|
|
329
|
-
memory.on_pre_compact(summary) # checkpoint episode
|
|
330
|
-
recovered = memory.on_post_compact() # what to re-inject after compaction
|
|
337
|
+
memory.on_pre_compact(summary) # checkpoint episode (only if summary is not blank) + consolidation
|
|
338
|
+
recovered = memory.on_post_compact() # what to re-inject after your own compaction
|
|
331
339
|
memory.on_session_end(summary) # final checkpoint + consolidation
|
|
340
|
+
|
|
341
|
+
# Driving it from Claude Code's hook events instead (what templates/hooks/ do):
|
|
342
|
+
memory.on_session_start(source) # source "startup"/"clear": full block; "compact": recovery block; "resume"/"fork": ""
|
|
343
|
+
memory.capture_compact_summary(compact_summary, session_id=..., trigger=...) # keep Claude Code's own summary as a checkpoint
|
|
332
344
|
```
|
|
333
345
|
|
|
334
346
|
### Documents and `ask`
|
|
@@ -349,9 +361,14 @@ memory.on_session_end(summary) # final checkpoint + consolidat
|
|
|
349
361
|
Long-term memory has a human-readable twin: a markdown wiki in the [LLM Wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) pattern (`index.md`, `log.md`, typed pages with cited sources — the same schema as `code-copilot-team/knowledge/wiki`).
|
|
350
362
|
|
|
351
363
|
- `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.
|
|
352
|
-
- `wiki_import` indexes
|
|
364
|
+
- `wiki_import` indexes pages as long-term memories so `recall` finds the reviewed version; a changed page supersedes its earlier memory. With wiki-studio 0.7.0 or later installed it is **governed** (`"governed": true`): it imports only pages wiki-studio's review state reports approved, and only when the page's bytes are still the approved revision, so drafts, self-declared approvals and pages edited after approval are not imported (`not_approved` says why); an edited page keeps its last approved memory until it is approved again. memkernel never approves a page; a curator does, with `wiki-studio approve`. Without wiki-studio the import reads the page files, drafts included, and reports `"governed": false`. An installed wiki-studio that is broken or older than 0.7.0 imports nothing and reports `import_error`.
|
|
365
|
+
- **Corrections.** An approved page can name the memories it corrects in its frontmatter: `corrects: <ref_id>, <ref_id>` (one line; tokens of letters, digits, `-` and `_`). On import each named memory that did not come from the wiki is superseded by the page's memory: it stops being a current answer, and its record is kept. References that cannot be applied are listed in `unresolved` with a reason (unknown, another project's, already superseded, or a wiki memory: correct that page instead); a value that breaks the grammar is listed in `invalid_corrects`, and the page is still imported. An approved page that is already current is listed in `unchanged`, and its corrections are reconciled again on every import, so a reference that could not be applied earlier takes effect once it can; corrections already applied are not repeated.
|
|
366
|
+
|
|
367
|
+
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). With the `memkernel[wiki]` extra installed, every `wiki_export` response carries wiki-studio's lint and health findings for the whole wiki after the export, curated pages included, under `lint` (`violations`, `audit_errors`, `pages_checked`, `health` items with `kind`, `severity`, `pages`, `description`); without it `lint` is `null` and `lint_hint` says how to enable it; if the lint pass itself fails, `lint` is `null` and `lint_error` says why. The files written are the same in every case.
|
|
353
368
|
|
|
354
|
-
|
|
369
|
+
With wiki-studio 0.7.0 or later installed, `wiki_export` writes through wiki-studio's promote gate: wiki-studio's `init` creates `index.md` and `log.md` if they are missing (wiki-studio's own scaffold, `status: stable`), then every new page goes with its index and log lines into one patch-set that is proposed and promoted. Other writers can work in the same wiki: wiki-studio's commit writes only the paths the patch-set names, under its lock, and refuses a page someone else created in the meantime. A rejected export writes no page and the response carries `gate_error` (a new wiki keeps the `index.md` and `log.md` that `init` created); so does an installed wiki-studio that is broken or older than 0.7.0, rather than falling back to the direct writer. A promoted export's response adds `proposal`, `submission_digest` and `archived_dir` from wiki-studio's receipt. If `archived_dir` is `null`, the pages landed but wiki-studio did not finish: it could not archive the proposal, or it failed after committing the pages (memkernel checks wiki-studio's receipt: while it still says `committing`, every planned write, index and log included, must be at its post-write sha256; a `committed` receipt needs memkernel's pages at theirs; an unusable receipt falls back to comparing the pages' bytes). `archive_hint` gives the `promote` command that tries to finish it, with absolute paths, so it runs from any directory. It applies nothing twice, but if the wiki or the proposal changed since, for example another writer appended to `index.md`, it may refuse with `uncertain_outcome` or `payload_changed` and write nothing; then compare the wiki with the proposal's `receipt.json` by hand. memkernel never runs it itself. If wiki-studio failed during its commit and its receipt shows neither a commit nor a rollback (say the page was written, the `index.md` write failed and removing the page failed too), the response carries `gate_error` with `uncertain_outcome` and `proposal`: `written` is empty, but the pages it names may be on disk; compare the wiki with the `receipt.json` it names before exporting again. `receipt_error` says the receipt could not be read or used. A failed export leaves its patch-set under the proposals directory for inspection; delete it once the cause is fixed. Patch-sets go to `MEMKERNEL_WIKI_PROPOSALS_DIR`, by default `.memkernel/wiki-proposals/` under the project root. Commit `<wiki>/.audit/`, wiki-studio's audit trail of promoted proposals; ignore `.memkernel/wiki-proposals/` and wiki-studio's lock file next to the wiki directory, `.<wiki-dir>.wiki-studio.lock` (for `knowledge/wiki`, `knowledge/.wiki.wiki-studio.lock`).
|
|
370
|
+
|
|
371
|
+
Rationale: [`docs/adr/ADR-008-llm-wiki-as-semantic-layer.md`](docs/adr/ADR-008-llm-wiki-as-semantic-layer.md) and [`docs/adr/ADR-010-wiki-studio-as-wiki-maintainer.md`](docs/adr/ADR-010-wiki-studio-as-wiki-maintainer.md).
|
|
355
372
|
|
|
356
373
|
### Revalidation and store health
|
|
357
374
|
|
|
@@ -394,9 +411,15 @@ CI runs on every push and PR: lint, type check, tests (≥ 80% coverage), securi
|
|
|
394
411
|
|
|
395
412
|
Releases are published to PyPI by [`.github/workflows/release.yml`](.github/workflows/release.yml) using trusted publishing (no token in the repo). To cut a release:
|
|
396
413
|
|
|
397
|
-
1. Set `version` in `pyproject.toml` and move the `[Unreleased]` entries in [`CHANGELOG.md`](CHANGELOG.md) under
|
|
414
|
+
1. Set `version` in `pyproject.toml` and `__version__` in `src/memkernel/__init__.py` to `X.Y.Z`, and move the `[Unreleased]` entries in [`CHANGELOG.md`](CHANGELOG.md) under `[X.Y.Z] - <tag date>`.
|
|
398
415
|
2. Merge to `main` with CI green.
|
|
399
|
-
3. Tag and push: `git tag -a
|
|
416
|
+
3. Tag and push: `git tag -a vX.Y.Z -m "memkernel X.Y.Z" && git push origin vX.Y.Z`.
|
|
417
|
+
4. Create the GitHub Release from the tag, with the version's CHANGELOG section as its notes:
|
|
418
|
+
|
|
419
|
+
```bash
|
|
420
|
+
awk -v v=X.Y.Z 'index($0, "## [" v "]") == 1 {f=1; next} f && /^## \[/ {exit} f' CHANGELOG.md > release-notes.md
|
|
421
|
+
gh release create vX.Y.Z --title X.Y.Z --notes-file release-notes.md --verify-tag
|
|
422
|
+
```
|
|
400
423
|
|
|
401
424
|
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.
|
|
402
425
|
|
|
@@ -34,7 +34,7 @@ MemKernel gives Claude Code (and any MCP-compatible copilot) persistent memory a
|
|
|
34
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
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
36
|
|
|
37
|
-
**Evidence.** `eval/run_eval.py` measures retrieval (recall@k, precision@k, MRR).
|
|
37
|
+
**Evidence.** `eval/run_eval.py` measures retrieval (recall@k, precision@k, MRR). The 0.5 lifecycle evaluation, a **maintainer-run supplemental evaluation** (memkernel's own runner, preregistered and announced in [#17](https://github.com/gosha70/memkernel/issues/17) before the run), ran the same agent in seven arms on 20 tasks: [report](eval/lifecycle-suite/report-0.5.0.md). **No gain is claimed.** Against files alone (`claude_md`), memkernel's memory store was ahead on 6 tasks, behind on none and tied on 14 (Holm-adjusted p 0.0625: "no detectable difference"; with so many ties this task set could not have reached significance); against earlier transcripts (`fs_grep`) it was equivalent. Two of the four task groups hit a ceiling, every arm passing every trial. The report's disclosures list its deviations and known defects. Results are published with the cases that show no gain; the 0.5 report is the latest evaluation, and 0.6.0 had no new measured run.
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
@@ -55,6 +55,9 @@ MemKernel is a Python package on PyPI. Its `memkernel` command is the MCP server
|
|
|
55
55
|
pipx install memkernel
|
|
56
56
|
# if pipx's default interpreter is outside 3.11–3.13:
|
|
57
57
|
pipx install --python python3.11 memkernel
|
|
58
|
+
# optional: wiki-studio lint and health findings on wiki_export (ADR-010)
|
|
59
|
+
pipx install "memkernel[wiki]"
|
|
60
|
+
pipx inject memkernel "wiki-studio>=0.7.0,<1" # add it to an existing install
|
|
58
61
|
```
|
|
59
62
|
|
|
60
63
|
**As a library inside a project**, install it into that project's virtual environment:
|
|
@@ -113,9 +116,11 @@ Copy the hook scripts from [`templates/hooks/`](templates/hooks/) to `.claude/ho
|
|
|
113
116
|
|
|
114
117
|
| Hook | Claude Code event | What it does |
|
|
115
118
|
|------|-------------------|--------------|
|
|
116
|
-
| `session-start.py` | `SessionStart` |
|
|
117
|
-
| `pre-compact.py` | `PreCompact` |
|
|
118
|
-
| `post-compact.py` | `PostCompact` |
|
|
119
|
+
| `session-start.py` | `SessionStart` | By `source`: on `startup` and `clear`, consolidates, recovers the latest checkpoint and long-term context, lists promotion candidates; on `compact`, the recovery after a compaction (the latest checkpoint the agent saved, with its age, and long-term context; no consolidation); on `resume` and `fork`, nothing |
|
|
120
|
+
| `pre-compact.py` | `PreCompact` | Runs a consolidation pass (Claude Code passes no summary here, so no checkpoint is retained) |
|
|
121
|
+
| `post-compact.py` | `PostCompact` | Retains Claude Code's compaction summary (`compact_summary`) as a checkpoint for the next session; prints nothing for the model |
|
|
122
|
+
|
|
123
|
+
Register `SessionStart` without a matcher, so it runs for every `source`. Claude Code adds only `SessionStart`'s output to the model's context, not `PreCompact`'s or `PostCompact`'s, so the recovery after a compaction comes from `SessionStart` with `source: "compact"`. After upgrading memkernel, copy the three scripts again.
|
|
119
124
|
|
|
120
125
|
See the Prerequisites section inside each script for the Python environment requirement. Each script is a thin wrapper over [`MemoryLifecycle`](#embedding-memkernel-in-your-own-harness), so an in-process harness gets the same behaviour without the scripts.
|
|
121
126
|
|
|
@@ -147,7 +152,7 @@ Kiro (and the Kiro CLI, formerly Amazon Q Developer CLI) connects to MemKernel a
|
|
|
147
152
|
| `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. |
|
|
148
153
|
| `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`. |
|
|
149
154
|
| `wiki_export(wiki_dir?)` | Compile long-term memories without a wiki page into draft markdown pages (`index.md`, `log.md` updated). |
|
|
150
|
-
| `wiki_import(wiki_dir?)` | Index
|
|
155
|
+
| `wiki_import(wiki_dir?)` | Index wiki pages as long-term memories: approved revisions only with wiki-studio ≥ 0.7.0, every page without it (idempotent; changed pages supersede; `corrects` supersedes the memories a page corrects). |
|
|
151
156
|
|
|
152
157
|
### Memory types and tiers
|
|
153
158
|
|
|
@@ -167,13 +172,13 @@ Retention: `ttl = base_ttl × (1 + access_boost × access_count)`, measured from
|
|
|
167
172
|
|
|
168
173
|
### Session protocol
|
|
169
174
|
|
|
170
|
-
1. **Session start** — `recall(tier="long_term", query=<task>)`, plus the latest checkpoint (`checkpoint_only=true`). The `session-start.py` hook does this and prints promotion candidates.
|
|
175
|
+
1. **Session start** — `recall(tier="long_term", query=<task>)`, plus the latest checkpoint (`checkpoint_only=true`). The `session-start.py` hook does this and prints promotion candidates; after a compaction it prints a shorter recovery block instead.
|
|
171
176
|
2. **While working** — `retain(type="task")` when the plan changes; `retain(type="observation")` for findings; durable knowledge as `decision` / `convention` / `fact` / `procedure`.
|
|
172
|
-
3. **Before compaction** — `retain(type="episode", checkpoint=true)`
|
|
177
|
+
3. **Before compaction** — `retain(type="episode", checkpoint=true)` with your own summary of where the work stands; the `pre-compact.py` hook runs `consolidate()`, and the `post-compact.py` hook keeps Claude Code's compaction summary as a checkpoint.
|
|
173
178
|
4. **After compaction / end of phase** — `promote(ref_id, type, content)` for the candidates worth keeping.
|
|
174
179
|
5. **Large artefacts** — `retain(type="document", content=<body>, source=<name>)` then `ask(ref_id, question)`; never `get` a document to read it.
|
|
175
180
|
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.
|
|
176
|
-
6. **Curate** — `wiki_export` → review the drafts → `wiki_import`.
|
|
181
|
+
6. **Curate** — `wiki_export` → review and approve the drafts in wiki-studio → `wiki_import`.
|
|
177
182
|
|
|
178
183
|
### Quick examples
|
|
179
184
|
|
|
@@ -217,7 +222,7 @@ setting is logged as a warning at startup.
|
|
|
217
222
|
|----------|---------|-------------|
|
|
218
223
|
| `MEMKERNEL_PROJECT_ID` | `default` | Project namespace — memories are isolated per project |
|
|
219
224
|
| `MEMKERNEL_DEFAULT_SESSION_ID` | `default` | Session tag applied when `session_id` is omitted from `retain` |
|
|
220
|
-
| `MEMKERNEL_CHROMA_PERSIST_PATH` | `.memkernel/chroma` | ChromaDB data directory. Empty string = in-memory (ephemeral) |
|
|
225
|
+
| `MEMKERNEL_CHROMA_PERSIST_PATH` | `.memkernel/chroma` | ChromaDB data directory. Empty string = in-memory (ephemeral). On POSIX systems (Linux, macOS), opening it takes a short lock on `.memkernel-open.lock` inside it, so the MCP server and a hook opening a new store at once do not collide; where `fcntl` is unavailable (Windows) the store opens without the lock, and two processes creating a new store at the same moment can still fail |
|
|
221
226
|
| `MEMKERNEL_CHROMA_COLLECTION_PREFIX` | `memkernel` | Prefix for ChromaDB collection names (`{prefix}_{project_id}`) |
|
|
222
227
|
| `MEMKERNEL_EMBEDDING_MODEL` | `all-MiniLM-L6-v2` | Sentence-transformers model name |
|
|
223
228
|
| `MEMKERNEL_EMBEDDING_DIMENSION` | `384` | Output dimension of the embedding model |
|
|
@@ -243,6 +248,7 @@ setting is logged as a warning at startup.
|
|
|
243
248
|
| `MEMKERNEL_RECALL_WEIGHT_RECENCY` | `0.3` | Weight of recency in the final recall score |
|
|
244
249
|
| `MEMKERNEL_RECALL_WEIGHT_IMPORTANCE` | `0.3` | Weight of importance in the final recall score |
|
|
245
250
|
| `MEMKERNEL_WIKI_DIR` | `` | Directory of the LLM wiki used by `wiki_export` / `wiki_import` (empty = pass `wiki_dir` explicitly) |
|
|
251
|
+
| `MEMKERNEL_WIKI_PROPOSALS_DIR` | `""` | Where wiki-studio patch-sets are written before promote (`memkernel[wiki]`); empty means `<project root>/.memkernel/wiki-proposals`. Setting it requests the gate: without wiki-studio installed, a warning is logged. |
|
|
246
252
|
| `MEMKERNEL_TRANSPORT` | `stdio` | `stdio` or `streamable-http` |
|
|
247
253
|
| `MEMKERNEL_HOST` | `127.0.0.1` | Host for `streamable-http` transport |
|
|
248
254
|
| `MEMKERNEL_PORT` | `8765` | Port for `streamable-http` transport |
|
|
@@ -288,7 +294,7 @@ report = svc.consolidate() # {"purged": n, "candidates": [...]}
|
|
|
288
294
|
|
|
289
295
|
### The session protocol as a component
|
|
290
296
|
|
|
291
|
-
`MemoryLifecycle` packages what the hook scripts do, so a harness calls
|
|
297
|
+
`MemoryLifecycle` packages what the hook scripts do, so a harness calls five methods instead of re-implementing the protocol. Every method returns markdown ready for injection and does nothing but talk to the service:
|
|
292
298
|
|
|
293
299
|
```python
|
|
294
300
|
from memkernel.factory import build_service
|
|
@@ -297,9 +303,13 @@ from memkernel.lifecycle import MemoryLifecycle
|
|
|
297
303
|
memory = MemoryLifecycle(build_service())
|
|
298
304
|
system_context = memory.on_session_start() # checkpoint, long-term context, promotion candidates, wiki pointer
|
|
299
305
|
...
|
|
300
|
-
memory.on_pre_compact(summary) # checkpoint episode
|
|
301
|
-
recovered = memory.on_post_compact() # what to re-inject after compaction
|
|
306
|
+
memory.on_pre_compact(summary) # checkpoint episode (only if summary is not blank) + consolidation
|
|
307
|
+
recovered = memory.on_post_compact() # what to re-inject after your own compaction
|
|
302
308
|
memory.on_session_end(summary) # final checkpoint + consolidation
|
|
309
|
+
|
|
310
|
+
# Driving it from Claude Code's hook events instead (what templates/hooks/ do):
|
|
311
|
+
memory.on_session_start(source) # source "startup"/"clear": full block; "compact": recovery block; "resume"/"fork": ""
|
|
312
|
+
memory.capture_compact_summary(compact_summary, session_id=..., trigger=...) # keep Claude Code's own summary as a checkpoint
|
|
303
313
|
```
|
|
304
314
|
|
|
305
315
|
### Documents and `ask`
|
|
@@ -320,9 +330,14 @@ memory.on_session_end(summary) # final checkpoint + consolidat
|
|
|
320
330
|
Long-term memory has a human-readable twin: a markdown wiki in the [LLM Wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) pattern (`index.md`, `log.md`, typed pages with cited sources — the same schema as `code-copilot-team/knowledge/wiki`).
|
|
321
331
|
|
|
322
332
|
- `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.
|
|
323
|
-
- `wiki_import` indexes
|
|
333
|
+
- `wiki_import` indexes pages as long-term memories so `recall` finds the reviewed version; a changed page supersedes its earlier memory. With wiki-studio 0.7.0 or later installed it is **governed** (`"governed": true`): it imports only pages wiki-studio's review state reports approved, and only when the page's bytes are still the approved revision, so drafts, self-declared approvals and pages edited after approval are not imported (`not_approved` says why); an edited page keeps its last approved memory until it is approved again. memkernel never approves a page; a curator does, with `wiki-studio approve`. Without wiki-studio the import reads the page files, drafts included, and reports `"governed": false`. An installed wiki-studio that is broken or older than 0.7.0 imports nothing and reports `import_error`.
|
|
334
|
+
- **Corrections.** An approved page can name the memories it corrects in its frontmatter: `corrects: <ref_id>, <ref_id>` (one line; tokens of letters, digits, `-` and `_`). On import each named memory that did not come from the wiki is superseded by the page's memory: it stops being a current answer, and its record is kept. References that cannot be applied are listed in `unresolved` with a reason (unknown, another project's, already superseded, or a wiki memory: correct that page instead); a value that breaks the grammar is listed in `invalid_corrects`, and the page is still imported. An approved page that is already current is listed in `unchanged`, and its corrections are reconciled again on every import, so a reference that could not be applied earlier takes effect once it can; corrections already applied are not repeated.
|
|
335
|
+
|
|
336
|
+
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). With the `memkernel[wiki]` extra installed, every `wiki_export` response carries wiki-studio's lint and health findings for the whole wiki after the export, curated pages included, under `lint` (`violations`, `audit_errors`, `pages_checked`, `health` items with `kind`, `severity`, `pages`, `description`); without it `lint` is `null` and `lint_hint` says how to enable it; if the lint pass itself fails, `lint` is `null` and `lint_error` says why. The files written are the same in every case.
|
|
324
337
|
|
|
325
|
-
|
|
338
|
+
With wiki-studio 0.7.0 or later installed, `wiki_export` writes through wiki-studio's promote gate: wiki-studio's `init` creates `index.md` and `log.md` if they are missing (wiki-studio's own scaffold, `status: stable`), then every new page goes with its index and log lines into one patch-set that is proposed and promoted. Other writers can work in the same wiki: wiki-studio's commit writes only the paths the patch-set names, under its lock, and refuses a page someone else created in the meantime. A rejected export writes no page and the response carries `gate_error` (a new wiki keeps the `index.md` and `log.md` that `init` created); so does an installed wiki-studio that is broken or older than 0.7.0, rather than falling back to the direct writer. A promoted export's response adds `proposal`, `submission_digest` and `archived_dir` from wiki-studio's receipt. If `archived_dir` is `null`, the pages landed but wiki-studio did not finish: it could not archive the proposal, or it failed after committing the pages (memkernel checks wiki-studio's receipt: while it still says `committing`, every planned write, index and log included, must be at its post-write sha256; a `committed` receipt needs memkernel's pages at theirs; an unusable receipt falls back to comparing the pages' bytes). `archive_hint` gives the `promote` command that tries to finish it, with absolute paths, so it runs from any directory. It applies nothing twice, but if the wiki or the proposal changed since, for example another writer appended to `index.md`, it may refuse with `uncertain_outcome` or `payload_changed` and write nothing; then compare the wiki with the proposal's `receipt.json` by hand. memkernel never runs it itself. If wiki-studio failed during its commit and its receipt shows neither a commit nor a rollback (say the page was written, the `index.md` write failed and removing the page failed too), the response carries `gate_error` with `uncertain_outcome` and `proposal`: `written` is empty, but the pages it names may be on disk; compare the wiki with the `receipt.json` it names before exporting again. `receipt_error` says the receipt could not be read or used. A failed export leaves its patch-set under the proposals directory for inspection; delete it once the cause is fixed. Patch-sets go to `MEMKERNEL_WIKI_PROPOSALS_DIR`, by default `.memkernel/wiki-proposals/` under the project root. Commit `<wiki>/.audit/`, wiki-studio's audit trail of promoted proposals; ignore `.memkernel/wiki-proposals/` and wiki-studio's lock file next to the wiki directory, `.<wiki-dir>.wiki-studio.lock` (for `knowledge/wiki`, `knowledge/.wiki.wiki-studio.lock`).
|
|
339
|
+
|
|
340
|
+
Rationale: [`docs/adr/ADR-008-llm-wiki-as-semantic-layer.md`](docs/adr/ADR-008-llm-wiki-as-semantic-layer.md) and [`docs/adr/ADR-010-wiki-studio-as-wiki-maintainer.md`](docs/adr/ADR-010-wiki-studio-as-wiki-maintainer.md).
|
|
326
341
|
|
|
327
342
|
### Revalidation and store health
|
|
328
343
|
|
|
@@ -365,9 +380,15 @@ CI runs on every push and PR: lint, type check, tests (≥ 80% coverage), securi
|
|
|
365
380
|
|
|
366
381
|
Releases are published to PyPI by [`.github/workflows/release.yml`](.github/workflows/release.yml) using trusted publishing (no token in the repo). To cut a release:
|
|
367
382
|
|
|
368
|
-
1. Set `version` in `pyproject.toml` and move the `[Unreleased]` entries in [`CHANGELOG.md`](CHANGELOG.md) under
|
|
383
|
+
1. Set `version` in `pyproject.toml` and `__version__` in `src/memkernel/__init__.py` to `X.Y.Z`, and move the `[Unreleased]` entries in [`CHANGELOG.md`](CHANGELOG.md) under `[X.Y.Z] - <tag date>`.
|
|
369
384
|
2. Merge to `main` with CI green.
|
|
370
|
-
3. Tag and push: `git tag -a
|
|
385
|
+
3. Tag and push: `git tag -a vX.Y.Z -m "memkernel X.Y.Z" && git push origin vX.Y.Z`.
|
|
386
|
+
4. Create the GitHub Release from the tag, with the version's CHANGELOG section as its notes:
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
awk -v v=X.Y.Z 'index($0, "## [" v "]") == 1 {f=1; next} f && /^## \[/ {exit} f' CHANGELOG.md > release-notes.md
|
|
390
|
+
gh release create vX.Y.Z --title X.Y.Z --notes-file release-notes.md --verify-tag
|
|
391
|
+
```
|
|
371
392
|
|
|
372
393
|
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.
|
|
373
394
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "memkernel"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "Code-aware memory server for AI coding copilots"
|
|
5
5
|
authors = ["EGOGE"]
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -31,6 +31,11 @@ tree-sitter-python = ">=0.23"
|
|
|
31
31
|
pydantic = ">=2.0,<3"
|
|
32
32
|
pydantic-settings = ">=2.0,<3"
|
|
33
33
|
fastapi = ">=0.115,<1"
|
|
34
|
+
# Optional integrations (ADR-010): installed only through an extra; imported lazily.
|
|
35
|
+
wiki-studio = { version = ">=0.7.0,<1", optional = true }
|
|
36
|
+
|
|
37
|
+
[tool.poetry.extras]
|
|
38
|
+
wiki = ["wiki-studio"]
|
|
34
39
|
|
|
35
40
|
[tool.poetry.group.dev.dependencies]
|
|
36
41
|
pytest = ">=8"
|
|
@@ -40,7 +45,7 @@ ruff = ">=0.9"
|
|
|
40
45
|
mypy = ">=1.13"
|
|
41
46
|
httpx = ">=0.27"
|
|
42
47
|
pip-audit = "^2.10.0"
|
|
43
|
-
wiki-studio = ">=0.
|
|
48
|
+
wiki-studio = ">=0.7.0,<1"
|
|
44
49
|
|
|
45
50
|
[build-system]
|
|
46
51
|
requires = ["poetry-core"]
|
|
@@ -76,6 +76,9 @@ class MemKernelSettings(BaseSettings):
|
|
|
76
76
|
# LLM wiki bridge: directory holding the markdown wiki (index.md, decisions/, ...).
|
|
77
77
|
# Empty = no wiki configured; wiki_export/wiki_import then require an explicit path.
|
|
78
78
|
wiki_dir: str = ""
|
|
79
|
+
# Where wiki-studio patch-sets are written before promote (memkernel[wiki], ADR-010).
|
|
80
|
+
# Empty = <project_root>/.memkernel/wiki-proposals; set it to request the gate explicitly.
|
|
81
|
+
wiki_proposals_dir: str = ""
|
|
79
82
|
|
|
80
83
|
# Server
|
|
81
84
|
transport: Literal["stdio", "streamable-http"] = "stdio"
|
|
@@ -259,6 +259,8 @@ class MemoryService:
|
|
|
259
259
|
tags: list[str] | None = None,
|
|
260
260
|
supersedes: str | None = None,
|
|
261
261
|
promoted_from: str | None = None,
|
|
262
|
+
*,
|
|
263
|
+
dedup_source: bool = False,
|
|
262
264
|
) -> str:
|
|
263
265
|
"""Store content and return its ref_id.
|
|
264
266
|
|
|
@@ -277,7 +279,10 @@ class MemoryService:
|
|
|
277
279
|
memkernel record keeps only a summary (``get`` never returns the
|
|
278
280
|
body; use ``ask``). ``source`` names the document.
|
|
279
281
|
|
|
280
|
-
Idempotent: identical content + type returns the existing ref_id.
|
|
282
|
+
Idempotent: identical content + type returns the existing ref_id. With
|
|
283
|
+
``dedup_source`` a single record's dedup hit also requires the same ``source``
|
|
284
|
+
(memkernel-0-6 FR-012: one memory per wiki page, never merged with another
|
|
285
|
+
page's or an agent's identical text).
|
|
281
286
|
"""
|
|
282
287
|
extra = _ExtraMeta(importance=importance, tags=tags or [], supersedes=supersedes, promoted_from=promoted_from)
|
|
283
288
|
if type == MemoryType.DOCUMENT and self._document_store is not None:
|
|
@@ -285,7 +290,7 @@ class MemoryService:
|
|
|
285
290
|
if type == MemoryType.CODE:
|
|
286
291
|
return self._retain_code(content, session_id, checkpoint, source, extra)
|
|
287
292
|
|
|
288
|
-
return self._retain_single(content, type, session_id, checkpoint, source, extra)
|
|
293
|
+
return self._retain_single(content, type, session_id, checkpoint, source, extra, dedup_source=dedup_source)
|
|
289
294
|
|
|
290
295
|
def _retain_code(
|
|
291
296
|
self,
|
|
@@ -450,13 +455,47 @@ class MemoryService:
|
|
|
450
455
|
):
|
|
451
456
|
self._vector_store.update_metadata(chunk.ref_id, update)
|
|
452
457
|
|
|
458
|
+
def supersede(self, ref_id: str, by_ref: str) -> bool:
|
|
459
|
+
"""Mark the existing record ``ref_id`` as replaced by the existing record ``by_ref``.
|
|
460
|
+
|
|
461
|
+
Sets only the durable ``superseded_by`` marker (on ``ref_id`` and its owned
|
|
462
|
+
chunks), as ``retain(supersedes=...)`` does; the record's content and every
|
|
463
|
+
other field stay as they are, so its provenance remains. Returns True when it
|
|
464
|
+
set the marker, False when ``ref_id`` was already superseded by ``by_ref``; that
|
|
465
|
+
call still marks any owned chunk left without the marker, so a retry completes
|
|
466
|
+
a partial mark.
|
|
467
|
+
Raises ``ValueError`` and changes nothing when either record is not in this
|
|
468
|
+
store (unknown, or another project's), when they are the same record, or when
|
|
469
|
+
``ref_id`` is already superseded by a different record: an existing marker is
|
|
470
|
+
never overwritten.
|
|
471
|
+
"""
|
|
472
|
+
if ref_id == by_ref:
|
|
473
|
+
raise ValueError(f"{ref_id} cannot supersede itself")
|
|
474
|
+
target = self._vector_store.get_by_id(ref_id)
|
|
475
|
+
if target is None:
|
|
476
|
+
raise ValueError(f"{ref_id} is not a memory in this store")
|
|
477
|
+
if self._vector_store.get_by_id(by_ref) is None:
|
|
478
|
+
raise ValueError(f"{by_ref} is not a memory in this store")
|
|
479
|
+
if target.superseded_by == by_ref:
|
|
480
|
+
self._mark_superseded(ref_id, by_ref) # idempotent: repairs chunks a failed earlier mark left
|
|
481
|
+
return False
|
|
482
|
+
if target.superseded_by:
|
|
483
|
+
raise ValueError(f"{ref_id} is already superseded by {target.superseded_by}")
|
|
484
|
+
self._mark_superseded(ref_id, by_ref)
|
|
485
|
+
return True
|
|
486
|
+
|
|
453
487
|
def _mark_superseded(self, target_ref: str, by_ref: str) -> None:
|
|
454
|
-
"""Set the durable ``superseded_by`` marker on ``target_ref``
|
|
488
|
+
"""Set the durable ``superseded_by`` marker on ``target_ref``'s owned chunks, then on ``target_ref``.
|
|
489
|
+
|
|
490
|
+
The record is marked last: it is what ``supersede`` checks, so a failure part-way leaves it unmarked and
|
|
491
|
+
a retry marks everything. Only chunks without a marker are written: an existing marker is never overwritten.
|
|
492
|
+
"""
|
|
455
493
|
if target_ref == by_ref or self._vector_store.get_by_id(target_ref) is None:
|
|
456
494
|
return
|
|
457
|
-
self._vector_store.update_metadata(target_ref, {SUPERSEDED_BY_KEY: by_ref})
|
|
458
495
|
for chunk in self._owned_chunks(target_ref):
|
|
459
|
-
|
|
496
|
+
if not chunk.superseded_by:
|
|
497
|
+
self._vector_store.update_metadata(chunk.ref_id, {SUPERSEDED_BY_KEY: by_ref})
|
|
498
|
+
self._vector_store.update_metadata(target_ref, {SUPERSEDED_BY_KEY: by_ref})
|
|
460
499
|
|
|
461
500
|
def _delete_owned_chunks(self, canonical: Memory) -> None:
|
|
462
501
|
"""Delete a canonical's owned chunk records; legacy shared chunk_refs only when nothing else references them."""
|
|
@@ -523,18 +562,21 @@ class MemoryService:
|
|
|
523
562
|
source: str,
|
|
524
563
|
extra: _ExtraMeta | None = None,
|
|
525
564
|
dedup: bool = True,
|
|
565
|
+
dedup_source: bool = False,
|
|
526
566
|
) -> str:
|
|
527
567
|
"""Store a single content string and return its ref_id.
|
|
528
568
|
|
|
529
569
|
``dedup=False`` skips the content-hash check for records whose identity
|
|
530
|
-
is not their text (document summaries)
|
|
570
|
+
is not their text (document summaries); ``dedup_source`` scopes it to
|
|
571
|
+
records with the same ``source``.
|
|
531
572
|
"""
|
|
532
573
|
extra = extra or _ExtraMeta()
|
|
533
574
|
resolved_session = session_id or self._settings.default_session_id
|
|
534
575
|
content_hash = compute_content_hash(content)
|
|
535
576
|
|
|
536
577
|
# Dedup check: same hash + same type -> update metadata, return existing id
|
|
537
|
-
|
|
578
|
+
scope = source if dedup_source else None
|
|
579
|
+
existing = self._vector_store.find_by_hash(content_hash, type.value, source=scope) if dedup else None
|
|
538
580
|
if existing is not None:
|
|
539
581
|
self._refresh_existing(existing, resolved_session, checkpoint, extra)
|
|
540
582
|
return existing.ref_id
|
|
@@ -782,6 +824,25 @@ class MemoryService:
|
|
|
782
824
|
memories = [m for m in memories if m.tier == tier]
|
|
783
825
|
return memories
|
|
784
826
|
|
|
827
|
+
def checkpoints_newest_first(self) -> list[Memory]:
|
|
828
|
+
"""Every live checkpoint record, newest first (ties by ``ref_id``), without content and without touching.
|
|
829
|
+
|
|
830
|
+
Selected by metadata, so the newest is found however many older checkpoints a query would rank higher.
|
|
831
|
+
Live as for :meth:`recall`, judged with this service's settings: not superseded, not flagged for review,
|
|
832
|
+
not expired. Code-chunk and document-part records are left out: they are parts of a checkpoint, not one.
|
|
833
|
+
Read the chosen record with :meth:`get`.
|
|
834
|
+
"""
|
|
835
|
+
live = [
|
|
836
|
+
m
|
|
837
|
+
for m in self._vector_store.list_all({"checkpoint": True}, include_content=False)
|
|
838
|
+
if m.superseded_by is None
|
|
839
|
+
and not m.needs_review
|
|
840
|
+
and m.parent_ref is None
|
|
841
|
+
and not _is_document_part(m)
|
|
842
|
+
and not memory_is_expired(m, self._settings)
|
|
843
|
+
]
|
|
844
|
+
return sorted(live, key=lambda m: (m.timestamp, m.ref_id), reverse=True)
|
|
845
|
+
|
|
785
846
|
@property
|
|
786
847
|
def project_id(self) -> str:
|
|
787
848
|
"""Return the project_id for this service instance."""
|