memkernel 0.5.0__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.5.0 → memkernel-0.6.0}/PKG-INFO +32 -19
- {memkernel-0.5.0 → memkernel-0.6.0}/README.md +30 -17
- {memkernel-0.5.0 → memkernel-0.6.0}/pyproject.toml +3 -3
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/__init__.py +1 -1
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/core/services.py +68 -7
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/lifecycle.py +92 -22
- memkernel-0.6.0/src/memkernel/mcp/tools/wiki.py +127 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/storage/chroma.py +53 -13
- memkernel-0.6.0/src/memkernel/wiki/studio.py +555 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/wiki/sync.py +201 -122
- memkernel-0.5.0/src/memkernel/mcp/tools/wiki.py +0 -84
- memkernel-0.5.0/src/memkernel/wiki/studio.py +0 -193
- {memkernel-0.5.0 → memkernel-0.6.0}/LICENSE +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/NOTICE +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/adapters/__init__.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/adapters/rlmstudio.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/api/__init__.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/api/app.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/api/routes/__init__.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/api/routes/admin.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/config.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/core/__init__.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/core/models.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/core/ports.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/factory.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/mcp/__init__.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/mcp/server.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/mcp/tools/__init__.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/mcp/tools/memory.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/memory/__init__.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/memory/dedup.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/memory/lifecycle.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/memory/types.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/py.typed +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/retrieval/__init__.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/retrieval/chunking.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/retrieval/embeddings.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/retrieval/reasoner.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/retrieval/search.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/retrieval/sparse.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/storage/__init__.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/storage/documents.py +0 -0
- {memkernel-0.5.0 → memkernel-0.6.0}/src/memkernel/wiki/__init__.py +0 -0
- {memkernel-0.5.0 → 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
|
|
@@ -23,7 +23,7 @@ Requires-Dist: rank-bm25 (>=0.2)
|
|
|
23
23
|
Requires-Dist: sentence-transformers (>=4.0,<6)
|
|
24
24
|
Requires-Dist: tree-sitter (>=0.22,<1)
|
|
25
25
|
Requires-Dist: tree-sitter-python (>=0.23)
|
|
26
|
-
Requires-Dist: wiki-studio (>=0.
|
|
26
|
+
Requires-Dist: wiki-studio (>=0.7.0,<1) ; extra == "wiki"
|
|
27
27
|
Project-URL: Documentation, https://memkernel.dev
|
|
28
28
|
Project-URL: Homepage, https://memkernel.dev
|
|
29
29
|
Project-URL: Repository, https://github.com/gosha70/memkernel
|
|
@@ -65,7 +65,7 @@ MemKernel gives Claude Code (and any MCP-compatible copilot) persistent memory a
|
|
|
65
65
|
- **An index of the whole repository.** `code` memories are the snapshots the agent chose to retain, not an automatic scan of every file.
|
|
66
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.
|
|
67
67
|
|
|
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
|
|
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.
|
|
69
69
|
|
|
70
70
|
---
|
|
71
71
|
|
|
@@ -88,7 +88,7 @@ pipx install memkernel
|
|
|
88
88
|
pipx install --python python3.11 memkernel
|
|
89
89
|
# optional: wiki-studio lint and health findings on wiki_export (ADR-010)
|
|
90
90
|
pipx install "memkernel[wiki]"
|
|
91
|
-
pipx inject memkernel "wiki-studio>=0.
|
|
91
|
+
pipx inject memkernel "wiki-studio>=0.7.0,<1" # add it to an existing install
|
|
92
92
|
```
|
|
93
93
|
|
|
94
94
|
**As a library inside a project**, install it into that project's virtual environment:
|
|
@@ -147,9 +147,11 @@ Copy the hook scripts from [`templates/hooks/`](templates/hooks/) to `.claude/ho
|
|
|
147
147
|
|
|
148
148
|
| Hook | Claude Code event | What it does |
|
|
149
149
|
|------|-------------------|--------------|
|
|
150
|
-
| `session-start.py` | `SessionStart` |
|
|
151
|
-
| `pre-compact.py` | `PreCompact` |
|
|
152
|
-
| `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.
|
|
153
155
|
|
|
154
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.
|
|
155
157
|
|
|
@@ -181,7 +183,7 @@ Kiro (and the Kiro CLI, formerly Amazon Q Developer CLI) connects to MemKernel a
|
|
|
181
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. |
|
|
182
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`. |
|
|
183
185
|
| `wiki_export(wiki_dir?)` | Compile long-term memories without a wiki page into draft markdown pages (`index.md`, `log.md` updated). |
|
|
184
|
-
| `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). |
|
|
185
187
|
|
|
186
188
|
### Memory types and tiers
|
|
187
189
|
|
|
@@ -201,13 +203,13 @@ Retention: `ttl = base_ttl × (1 + access_boost × access_count)`, measured from
|
|
|
201
203
|
|
|
202
204
|
### Session protocol
|
|
203
205
|
|
|
204
|
-
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.
|
|
205
207
|
2. **While working** — `retain(type="task")` when the plan changes; `retain(type="observation")` for findings; durable knowledge as `decision` / `convention` / `fact` / `procedure`.
|
|
206
|
-
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.
|
|
207
209
|
4. **After compaction / end of phase** — `promote(ref_id, type, content)` for the candidates worth keeping.
|
|
208
210
|
5. **Large artefacts** — `retain(type="document", content=<body>, source=<name>)` then `ask(ref_id, question)`; never `get` a document to read it.
|
|
209
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.
|
|
210
|
-
6. **Curate** — `wiki_export` → review the drafts → `wiki_import`.
|
|
212
|
+
6. **Curate** — `wiki_export` → review and approve the drafts in wiki-studio → `wiki_import`.
|
|
211
213
|
|
|
212
214
|
### Quick examples
|
|
213
215
|
|
|
@@ -251,7 +253,7 @@ setting is logged as a warning at startup.
|
|
|
251
253
|
|----------|---------|-------------|
|
|
252
254
|
| `MEMKERNEL_PROJECT_ID` | `default` | Project namespace — memories are isolated per project |
|
|
253
255
|
| `MEMKERNEL_DEFAULT_SESSION_ID` | `default` | Session tag applied when `session_id` is omitted from `retain` |
|
|
254
|
-
| `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 |
|
|
255
257
|
| `MEMKERNEL_CHROMA_COLLECTION_PREFIX` | `memkernel` | Prefix for ChromaDB collection names (`{prefix}_{project_id}`) |
|
|
256
258
|
| `MEMKERNEL_EMBEDDING_MODEL` | `all-MiniLM-L6-v2` | Sentence-transformers model name |
|
|
257
259
|
| `MEMKERNEL_EMBEDDING_DIMENSION` | `384` | Output dimension of the embedding model |
|
|
@@ -323,7 +325,7 @@ report = svc.consolidate() # {"purged": n, "candidates": [...]}
|
|
|
323
325
|
|
|
324
326
|
### The session protocol as a component
|
|
325
327
|
|
|
326
|
-
`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:
|
|
327
329
|
|
|
328
330
|
```python
|
|
329
331
|
from memkernel.factory import build_service
|
|
@@ -332,9 +334,13 @@ from memkernel.lifecycle import MemoryLifecycle
|
|
|
332
334
|
memory = MemoryLifecycle(build_service())
|
|
333
335
|
system_context = memory.on_session_start() # checkpoint, long-term context, promotion candidates, wiki pointer
|
|
334
336
|
...
|
|
335
|
-
memory.on_pre_compact(summary) # checkpoint episode
|
|
336
|
-
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
|
|
337
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
|
|
338
344
|
```
|
|
339
345
|
|
|
340
346
|
### Documents and `ask`
|
|
@@ -355,11 +361,12 @@ memory.on_session_end(summary) # final checkpoint + consolidat
|
|
|
355
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`).
|
|
356
362
|
|
|
357
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.
|
|
358
|
-
- `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.
|
|
359
366
|
|
|
360
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.
|
|
361
368
|
|
|
362
|
-
With wiki-studio 0.
|
|
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`).
|
|
363
370
|
|
|
364
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).
|
|
365
372
|
|
|
@@ -404,9 +411,15 @@ CI runs on every push and PR: lint, type check, tests (≥ 80% coverage), securi
|
|
|
404
411
|
|
|
405
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:
|
|
406
413
|
|
|
407
|
-
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>`.
|
|
408
415
|
2. Merge to `main` with CI green.
|
|
409
|
-
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
|
+
```
|
|
410
423
|
|
|
411
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.
|
|
412
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). 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
|
|
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
|
|
|
@@ -57,7 +57,7 @@ pipx install memkernel
|
|
|
57
57
|
pipx install --python python3.11 memkernel
|
|
58
58
|
# optional: wiki-studio lint and health findings on wiki_export (ADR-010)
|
|
59
59
|
pipx install "memkernel[wiki]"
|
|
60
|
-
pipx inject memkernel "wiki-studio>=0.
|
|
60
|
+
pipx inject memkernel "wiki-studio>=0.7.0,<1" # add it to an existing install
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
**As a library inside a project**, install it into that project's virtual environment:
|
|
@@ -116,9 +116,11 @@ Copy the hook scripts from [`templates/hooks/`](templates/hooks/) to `.claude/ho
|
|
|
116
116
|
|
|
117
117
|
| Hook | Claude Code event | What it does |
|
|
118
118
|
|------|-------------------|--------------|
|
|
119
|
-
| `session-start.py` | `SessionStart` |
|
|
120
|
-
| `pre-compact.py` | `PreCompact` |
|
|
121
|
-
| `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.
|
|
122
124
|
|
|
123
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.
|
|
124
126
|
|
|
@@ -150,7 +152,7 @@ Kiro (and the Kiro CLI, formerly Amazon Q Developer CLI) connects to MemKernel a
|
|
|
150
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. |
|
|
151
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`. |
|
|
152
154
|
| `wiki_export(wiki_dir?)` | Compile long-term memories without a wiki page into draft markdown pages (`index.md`, `log.md` updated). |
|
|
153
|
-
| `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). |
|
|
154
156
|
|
|
155
157
|
### Memory types and tiers
|
|
156
158
|
|
|
@@ -170,13 +172,13 @@ Retention: `ttl = base_ttl × (1 + access_boost × access_count)`, measured from
|
|
|
170
172
|
|
|
171
173
|
### Session protocol
|
|
172
174
|
|
|
173
|
-
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.
|
|
174
176
|
2. **While working** — `retain(type="task")` when the plan changes; `retain(type="observation")` for findings; durable knowledge as `decision` / `convention` / `fact` / `procedure`.
|
|
175
|
-
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.
|
|
176
178
|
4. **After compaction / end of phase** — `promote(ref_id, type, content)` for the candidates worth keeping.
|
|
177
179
|
5. **Large artefacts** — `retain(type="document", content=<body>, source=<name>)` then `ask(ref_id, question)`; never `get` a document to read it.
|
|
178
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.
|
|
179
|
-
6. **Curate** — `wiki_export` → review the drafts → `wiki_import`.
|
|
181
|
+
6. **Curate** — `wiki_export` → review and approve the drafts in wiki-studio → `wiki_import`.
|
|
180
182
|
|
|
181
183
|
### Quick examples
|
|
182
184
|
|
|
@@ -220,7 +222,7 @@ setting is logged as a warning at startup.
|
|
|
220
222
|
|----------|---------|-------------|
|
|
221
223
|
| `MEMKERNEL_PROJECT_ID` | `default` | Project namespace — memories are isolated per project |
|
|
222
224
|
| `MEMKERNEL_DEFAULT_SESSION_ID` | `default` | Session tag applied when `session_id` is omitted from `retain` |
|
|
223
|
-
| `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 |
|
|
224
226
|
| `MEMKERNEL_CHROMA_COLLECTION_PREFIX` | `memkernel` | Prefix for ChromaDB collection names (`{prefix}_{project_id}`) |
|
|
225
227
|
| `MEMKERNEL_EMBEDDING_MODEL` | `all-MiniLM-L6-v2` | Sentence-transformers model name |
|
|
226
228
|
| `MEMKERNEL_EMBEDDING_DIMENSION` | `384` | Output dimension of the embedding model |
|
|
@@ -292,7 +294,7 @@ report = svc.consolidate() # {"purged": n, "candidates": [...]}
|
|
|
292
294
|
|
|
293
295
|
### The session protocol as a component
|
|
294
296
|
|
|
295
|
-
`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:
|
|
296
298
|
|
|
297
299
|
```python
|
|
298
300
|
from memkernel.factory import build_service
|
|
@@ -301,9 +303,13 @@ from memkernel.lifecycle import MemoryLifecycle
|
|
|
301
303
|
memory = MemoryLifecycle(build_service())
|
|
302
304
|
system_context = memory.on_session_start() # checkpoint, long-term context, promotion candidates, wiki pointer
|
|
303
305
|
...
|
|
304
|
-
memory.on_pre_compact(summary) # checkpoint episode
|
|
305
|
-
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
|
|
306
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
|
|
307
313
|
```
|
|
308
314
|
|
|
309
315
|
### Documents and `ask`
|
|
@@ -324,11 +330,12 @@ memory.on_session_end(summary) # final checkpoint + consolidat
|
|
|
324
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`).
|
|
325
331
|
|
|
326
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.
|
|
327
|
-
- `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.
|
|
328
335
|
|
|
329
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.
|
|
330
337
|
|
|
331
|
-
With wiki-studio 0.
|
|
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`).
|
|
332
339
|
|
|
333
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).
|
|
334
341
|
|
|
@@ -373,9 +380,15 @@ CI runs on every push and PR: lint, type check, tests (≥ 80% coverage), securi
|
|
|
373
380
|
|
|
374
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:
|
|
375
382
|
|
|
376
|
-
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>`.
|
|
377
384
|
2. Merge to `main` with CI green.
|
|
378
|
-
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
|
+
```
|
|
379
392
|
|
|
380
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.
|
|
381
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"
|
|
@@ -32,7 +32,7 @@ pydantic = ">=2.0,<3"
|
|
|
32
32
|
pydantic-settings = ">=2.0,<3"
|
|
33
33
|
fastapi = ">=0.115,<1"
|
|
34
34
|
# Optional integrations (ADR-010): installed only through an extra; imported lazily.
|
|
35
|
-
wiki-studio = { version = ">=0.
|
|
35
|
+
wiki-studio = { version = ">=0.7.0,<1", optional = true }
|
|
36
36
|
|
|
37
37
|
[tool.poetry.extras]
|
|
38
38
|
wiki = ["wiki-studio"]
|
|
@@ -45,7 +45,7 @@ ruff = ">=0.9"
|
|
|
45
45
|
mypy = ">=1.13"
|
|
46
46
|
httpx = ">=0.27"
|
|
47
47
|
pip-audit = "^2.10.0"
|
|
48
|
-
wiki-studio = ">=0.
|
|
48
|
+
wiki-studio = ">=0.7.0,<1"
|
|
49
49
|
|
|
50
50
|
[build-system]
|
|
51
51
|
requires = ["poetry-core"]
|
|
@@ -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."""
|