memkernel 0.6.0__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. {memkernel-0.6.0 → memkernel-0.7.0}/PKG-INFO +42 -8
  2. {memkernel-0.6.0 → memkernel-0.7.0}/README.md +41 -7
  3. {memkernel-0.6.0 → memkernel-0.7.0}/pyproject.toml +1 -1
  4. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/__init__.py +1 -1
  5. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/config.py +9 -0
  6. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/core/models.py +47 -0
  7. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/core/ports.py +24 -1
  8. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/core/services.py +199 -24
  9. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/lifecycle.py +166 -36
  10. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/mcp/tools/memory.py +7 -0
  11. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/mcp/tools/wiki.py +60 -10
  12. memkernel-0.7.0/src/memkernel/session_spool.py +154 -0
  13. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/storage/chroma.py +163 -19
  14. memkernel-0.7.0/src/memkernel/wiki/curation.py +254 -0
  15. memkernel-0.7.0/src/memkernel/wiki/ledger.py +282 -0
  16. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/wiki/studio.py +174 -1
  17. memkernel-0.7.0/src/memkernel/wiki/sync.py +1064 -0
  18. memkernel-0.6.0/src/memkernel/wiki/sync.py +0 -504
  19. {memkernel-0.6.0 → memkernel-0.7.0}/LICENSE +0 -0
  20. {memkernel-0.6.0 → memkernel-0.7.0}/NOTICE +0 -0
  21. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/adapters/__init__.py +0 -0
  22. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/adapters/rlmstudio.py +0 -0
  23. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/api/__init__.py +0 -0
  24. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/api/app.py +0 -0
  25. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/api/routes/__init__.py +0 -0
  26. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/api/routes/admin.py +0 -0
  27. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/core/__init__.py +0 -0
  28. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/factory.py +0 -0
  29. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/mcp/__init__.py +0 -0
  30. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/mcp/server.py +0 -0
  31. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/mcp/tools/__init__.py +0 -0
  32. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/memory/__init__.py +0 -0
  33. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/memory/dedup.py +0 -0
  34. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/memory/lifecycle.py +0 -0
  35. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/memory/types.py +0 -0
  36. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/py.typed +0 -0
  37. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/retrieval/__init__.py +0 -0
  38. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/retrieval/chunking.py +0 -0
  39. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/retrieval/embeddings.py +0 -0
  40. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/retrieval/reasoner.py +0 -0
  41. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/retrieval/search.py +0 -0
  42. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/retrieval/sparse.py +0 -0
  43. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/storage/__init__.py +0 -0
  44. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/storage/documents.py +0 -0
  45. {memkernel-0.6.0 → memkernel-0.7.0}/src/memkernel/wiki/__init__.py +0 -0
  46. {memkernel-0.6.0 → memkernel-0.7.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.6.0
3
+ Version: 0.7.0
4
4
  Summary: Code-aware memory server for AI coding copilots
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -150,8 +150,9 @@ Copy the hook scripts from [`templates/hooks/`](templates/hooks/) to `.claude/ho
150
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
151
  | `pre-compact.py` | `PreCompact` | Runs a consolidation pass (Claude Code passes no summary here, so no checkpoint is retained) |
152
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
+ | `session-end.py` | `SessionEnd` | Writes one line to the session spool in `MEMKERNEL_STATE_DIR` (no store, no model, within SessionEnd's 1.5-second budget); the next session start says when a session ended without a saved checkpoint. A session that never recorded an end (a crash, or this hook not registered) is dropped from the spool after 7 days, and the next start says how many |
153
154
 
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.
155
+ Each injected memory line ends with its `ref_id`, so `get(ref_id)` returns the full text; a block never exceeds 9,000 characters (Claude Code caps hook output at 10,000), and a cut checkpoint says which `get` holds the rest. At session start the long-term context is the most important decisions, conventions, procedures and facts, not the matches of a fixed query; after a compaction it is recalled with the checkpoint's text. Register `SessionStart` without a matcher, so it runs for every `source`, and `session-end.py` under `SessionEnd`. 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.
155
156
 
156
157
  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.
157
158
 
@@ -176,14 +177,14 @@ Kiro (and the Kiro CLI, formerly Amazon Q Developer CLI) connects to MemKernel a
176
177
  | Tool | What it does |
177
178
  |------|-------------|
178
179
  | `retain(content, type, session_id?, checkpoint?, source?, importance?, tags?, supersedes?)` | Store a memory. Returns `ref_id`. Idempotent — identical content + type returns the same `ref_id`. `supersedes` hides the memory it replaces. |
179
- | `recall(query, type?, tier?, session_id?, checkpoint_only?, top_k?, include_needs_review?)` | Search by natural language or identifier. Returns lightweight `ArtifactRef` list ranked by relevance + recency + importance. Records an access (reinforces retention). Code memories flagged `needs_review` are hidden unless `include_needs_review=true`. |
180
+ | `recall(query, type?, tier?, session_id?, checkpoint_only?, top_k?, include_needs_review?, order?)` | Search by natural language or identifier. Returns lightweight `ArtifactRef` list ranked by relevance + recency + importance. Records an access (reinforces retention). Code memories flagged `needs_review` are hidden unless `include_needs_review=true`. |
180
181
  | `get(ref_id)` | Fetch full content of a specific memory. Records an access. |
181
182
  | `forget(ref_id)` | Delete a memory from all indexes. |
182
183
  | `promote(ref_id, type, content?, importance?, tags?)` | Turn a short-term/working memory into a long-term one (you supply the distilled text). Keeps provenance via `promoted_from`. |
183
184
  | `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. |
184
185
  | `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`. |
185
186
  | `wiki_export(wiki_dir?)` | Compile long-term memories without a wiki page into draft markdown pages (`index.md`, `log.md` updated). |
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). |
187
+ | `wiki_import(wiki_dir?, adopt_legacy?, adopt_pages?, adopt_manifest?)` | Index wiki pages as long-term memories, each wiki keeping to its own: approved revisions only with wiki-studio ≥ 0.7.0, every page without it (idempotent; changed pages supersede; `corrects` supersedes the memories a page corrects). |
187
188
 
188
189
  ### Memory types and tiers
189
190
 
@@ -203,7 +204,7 @@ Retention: `ttl = base_ttl × (1 + access_boost × access_count)`, measured from
203
204
 
204
205
  ### Session protocol
205
206
 
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.
207
+ 1. **Session start** — `recall(tier="long_term", query=<task>)`, plus the latest checkpoint (`checkpoint_only=true, order="newest"`: newest first, not the most relevant). The `session-start.py` hook does this and prints promotion candidates; after a compaction it prints a shorter recovery block instead.
207
208
  2. **While working** — `retain(type="task")` when the plan changes; `retain(type="observation")` for findings; durable knowledge as `decision` / `convention` / `fact` / `procedure`.
208
209
  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.
209
210
  4. **After compaction / end of phase** — `promote(ref_id, type, content)` for the candidates worth keeping.
@@ -222,7 +223,7 @@ retain(
222
223
 
223
224
  recall(query="database choice", top_k=5)
224
225
 
225
- recall(query="session checkpoint", checkpoint_only=True)
226
+ recall(query="session checkpoint", checkpoint_only=True, order="newest", top_k=1) # the latest checkpoint
226
227
 
227
228
  retain(content="pytest hangs unless MEMKERNEL_PROJECT_ID is set", type="observation", tags=["ci"])
228
229
 
@@ -279,7 +280,10 @@ setting is logged as a warning at startup.
279
280
  | `MEMKERNEL_RECALL_WEIGHT_RECENCY` | `0.3` | Weight of recency in the final recall score |
280
281
  | `MEMKERNEL_RECALL_WEIGHT_IMPORTANCE` | `0.3` | Weight of importance in the final recall score |
281
282
  | `MEMKERNEL_WIKI_DIR` | `` | Directory of the LLM wiki used by `wiki_export` / `wiki_import` (empty = pass `wiki_dir` explicitly) |
283
+ | `MEMKERNEL_STATE_DIR` | `.memkernel/state` | memkernel's own state outside ChromaDB: the wiki sync ledger. Resolved like `MEMKERNEL_CHROMA_PERSIST_PATH` (a relative path is relative to the process's working directory, so set it absolute when the persist path is); keep it out of git |
282
284
  | `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. |
285
+ | `MEMKERNEL_WIKI_MODE` | `auto` | `auto`: wiki-studio's gate and review state when wiki-studio 0.7.0 or later is installed, the direct writer and file-reading import when it is not. `governed`: `wiki_export` and `wiki_import` refuse, writing nothing, unless wiki-studio is installed with every capability memkernel uses and the wiki's manifest is `governed` (`approval_required: true`) without schema drift. `ungated`: wiki-studio is never loaded. Responses report `mode` (`gated` or `ungated`, the path that ran) and `configured_mode` (this setting). |
286
+ | `MEMKERNEL_WIKI_EXPORT_BATCH` | `10` | New pages one `wiki_export` writes at most, the most important memories first; the response's `remaining` counts the rest (`null` when the gate's outcome is uncertain or `governed` refused before reading the wiki). `0` = no limit; a negative value is refused |
283
287
  | `MEMKERNEL_TRANSPORT` | `stdio` | `stdio` or `streamable-http` |
284
288
  | `MEMKERNEL_HOST` | `127.0.0.1` | Host for `streamable-http` transport |
285
289
  | `MEMKERNEL_PORT` | `8765` | Port for `streamable-http` transport |
@@ -321,7 +325,9 @@ for ref in svc.recall("database"):
321
325
  report = svc.consolidate() # {"purged": n, "candidates": [...]}
322
326
  ```
323
327
 
324
- `build_service()` is the supported entry point; the MCP server, the admin API and the hook templates all use it. Integration notes for `code-copilot-team` and `haa` are in [`docs/integration/harness-integration.md`](docs/integration/harness-integration.md).
328
+ `build_service()` is the supported entry point; the MCP server, the admin API and the hook templates all use it. If you pass a `PersistentClient` as `chroma_client` and also set `MEMKERNEL_CHROMA_PERSIST_PATH` (in the environment or in the settings), both must name the same directory, or `ValueError` is raised; with the default setting, the client's own directory is used.
329
+
330
+ **Store locks.** Processes that share a store (the MCP server, a hook, a script) coordinate through locks keyed by the store itself: lock files in the persist directory, `.memkernel-<name>-<key>.lock`, whatever each process's `MEMKERNEL_STATE_DIR` is; an in-memory store's locks are per process. Only one writer changes a record's tags, under the store's `tags` lock, so a tag added by one process is never dropped by another's write. Locks are not re-entrant (taking one the thread already holds raises `LockReentryError`), a wait longer than 10 seconds is logged once, and spelling the persist directory with different letter case in two processes gives two locks, so use one spelling. The locks use `fcntl` and are verified on POSIX (Linux, macOS) only; on Windows they run unlocked across processes, with one logged warning per lock. Integration notes for `code-copilot-team` and `haa` are in [`docs/integration/harness-integration.md`](docs/integration/harness-integration.md).
325
331
 
326
332
  ### The session protocol as a component
327
333
 
@@ -360,13 +366,41 @@ memory.capture_compact_summary(compact_summary, session_id=..., trigger=...) #
360
366
 
361
367
  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`).
362
368
 
369
+ To see the whole loop on a clean install, follow wiki-studio's [memkernel composition quickstart](https://github.com/gosha70/wiki-studio/blob/main/docs/quickstart-memkernel.md): retain a memory, export it through wiki-studio's gate, correct and approve the page, import only the approved revision, and see the next `recall` return it. CI runs the quickstart against memkernel **0.6.0**.
370
+
363
371
  - `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.
364
372
  - `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`.
373
+ - **Withdrawal (governed import, 0.7).** A page's memory leaves the current answers, its record kept for history, when:
374
+ - the page is **deleted**. Only memories this wiki's sync ledger recorded as imported are withdrawn this way; a memory imported before 0.7, or without wiki-studio, never is.
375
+ - the page is **deprecated**, once the `status: deprecated` revision is approved.
376
+ - the page is **superseded** (`superseded_by: <page path>`). Its memory is replaced by the successor page's memory; if the successor has none yet, the old memory is withdrawn and linked once it does (`pending` until then).
377
+
378
+ The response lists `withdrawn`, `superseded`, `pending` and `sync_failed` (retried at the next import). The ledger is one JSON file per project and wiki under `MEMKERNEL_STATE_DIR/wiki-sync/`; a corrupt one is kept aside, reported as `ledger_error`, and rebuilt.
365
379
  - **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
380
 
381
+ - **Each wiki keeps to its own memories (0.7).** Every memory `wiki_import` writes, or finds current, carries its wiki's identity: `wiki-path:<hash>` (the wiki directory's path relative to the project root) and, when the wiki has a wiki-studio manifest, `wiki-id:<uuid>`. A page's previous memory, its corrections, its supersession and its deprecation are only that wiki's, so two wikis in one store may share page paths and `page_id`s. These tags are reserved: `retain` refuses them, and an ordinary `retain` whose text matches a wiki memory gets its own record instead of merging into it; `promote` drops every wiki tag from its copy.
382
+ - **A wiki that may move, or that shares a store with another wiki, needs a manifest.** The path a wiki is known by is relative to its project root: two project roots sharing one store, with wikis at the same relative path, get the same `wiki-path:`, so the second import is refused (memories at this path belong to another wiki) even with manifests. Use one store per project, or different relative paths. Without one, a wiki *is* its path: moved, it starts afresh, and whatever wiki is later placed at the old path continues its memories. A copied wiki directory (two directories, one `wiki_id`) is one wiki: each import moves its memories to the directory imported last.
383
+ - **Upgrading a 0.6 store:** memories written before 0.7 carry no identity, so the first import writes nothing for their pages and lists them under `ambiguous` (`{"path", "ref_ids", "reason"}`). Import once with `adopt_legacy=true` to adopt each page's sole such memory; a page with several stays `ambiguous` (forget the ones that are not its own). With several wikis in one store, name the records instead: `adopt_pages=[{"path", "ref_id"}, …]` copied from `ambiguous`; a named record that changed meanwhile is not adopted (`candidate changed`).
384
+ - **Identity changes stop the import before any write, with the remedy:** a wiki that gained a manifest needs `adopt_manifest=true` once (its sync ledger moves to the `wiki_id` key; if both ledger files exist, delete the stale one); memories at this path that belong to another `wiki_id` (the manifest removed or replaced, or a moved wiki not yet imported from its new place) stop it until the manifest is restored, the moved wiki is imported first, or those memories are forgotten. A wiki with a manifest that moves keeps its memories; the next import moves their path.
385
+ - One import runs at a time per store (the `wiki-import` store lock). On Windows the import runs unlocked with one warning, as in 0.6: two concurrent imports of one wiki can leave two records for a page (the next import reports it as `duplicate records`; forget one), and two wikis adopting the same record at once can tag it for one while the other supersedes it. Give processes that share a store the same `MEMKERNEL_STATE_DIR`, so they share the sync ledger.
367
386
  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.
368
387
 
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`).
388
+ 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/`, `.memkernel/state/` (memkernel's sync ledger) and wiki-studio's files next to the wiki directory, `.<wiki-dir>.wiki-studio.*` (for `knowledge/wiki`, `knowledge/.wiki.wiki-studio.*`: its lock file and, from wiki-studio 0.8, its search cache).
389
+
390
+ **Wiki modes.** `MEMKERNEL_WIKI_MODE` is a property of the deployment, not of a call. `governed` runs one preflight before any page is written or read, for export and import alike: wiki-studio 0.7.0 or later installed and working; its `info` listing the capabilities `wiki-lock`, `receipts`, `root-manifest`, `page-id`, `approval-record`, `superseded-by` and `findings`; the wiki's manifest mode `governed`; no schema drift. A `managed` or `legacy` wiki is refused with the remedy: run `wiki-studio init` if there is no manifest, then set `approval_required: true` in the manifest's policy. A refusal reports `gate_error` (export) or `import_error` (import) and writes nothing; `update_index=False` is refused too, since the direct writer is never used. A gate rejection after the preflight commits no drafted page, but a new wiki keeps the `index.md` and `log.md` that wiki-studio's `init` created: memkernel keeps no outer rollback. `ungated` never loads wiki-studio (no gate, lint, review state or findings): `lint` is `null` and `lint_hint` names the mode.
391
+
392
+ **Export batches and grounding.** One `wiki_export` writes at most `MEMKERNEL_WIKI_EXPORT_BATCH` new pages (by importance, highest first, then oldest, then `ref_id`), so a curator reviews a small batch at a time; `remaining` counts the eligible memories still without a page, all of them after a rejected batch. Every exported page carries `memkernel_sha256`, the memory's content hash, beside `memkernel_ref`; a `path:` source gains `sha:`, the commit it is grounded at (`git log -1 --format=%h`, wiki-studio's citation rule), only when the project's `git ls-files` lists exactly that file and `git status` shows no change to it: a modified, staged or untracked file, a file in a submodule, or no git gets none. Pages created through the gate carry wiki-studio's `page_id`, so a page renamed or moved under its `page_id` keeps its memory: the next import supersedes the memory at the old path with one at the new path, also when the moved page's bytes are unchanged.
393
+
394
+ **Curation debt at session start.** With `MEMKERNEL_WIKI_DIR` set and wiki-studio available (in `governed`, only after the preflight passes), the session-start block (`startup`, `clear`) gains `### Curation debt`: one line of wiki-studio's `findings` counts in a fixed order (review kinds first) plus the sync ledger's unfinished actions (`sync pending`, `sync failed`), up to three example pages, and a hint to run `wiki-studio findings`. It uses at most 500 characters, taken from the long-term context's share, so the block stays under 9,000 characters. The check runs in a child process bounded to 2.5 seconds once it has started (a fork or exec the operating system holds up is not covered); it reads the sync ledger without changing it and never waits for an import that holds it. Any failure is one `Curation debt unavailable: <reason>` line. Not on `compact`, `resume` or `fork`, nor in `ungated`.
395
+
396
+ **wiki-studio's files beside the wiki, its ingest log, and wiki-studio 0.8.** When memkernel exports through the gate, wiki-studio creates its lock file, `.<wiki-dir>.wiki-studio.lock`, beside the wiki directory, and from 0.8 a curator's `find` or `query` adds a search cache, `.<wiki-dir>.wiki-studio.search.sqlite`, there too: for a wiki kept in git, ignore both with `.<wiki-dir>.wiki-studio.*` (for `knowledge/wiki`, `knowledge/.wiki.wiki-studio.*`). memkernel creates no file of its own there. memkernel reads no wiki-studio ingest log, so the log's `hash_recipe` has no consumer in memkernel. From wiki-studio 0.8.0 a page that is not valid UTF-8 fails lint for the whole wiki, so every promote, memkernel's gated export included, is refused with a `gate_error` naming that page until it is re-encoded or removed. From 0.8.0 an unknown `WIKI_STUDIO_RANKER` value makes wiki-studio's `info` fail, so a `governed` export or import, a gated import and the curation debt refuse with wiki-studio's message naming the variable. memkernel needs wiki-studio 0.7.0 or later and is tested at 0.7.0; an informational CI job runs the wiki tests against the newest release below 1.0 on every pull request and weekly.
397
+
398
+ wiki-studio's MCP server (`wiki-studio-mcp`, from wiki-studio 0.8.0) needs `mcp>=2.3`, which memkernel's `mcp<2` excludes. Run it in its own environment, at the same wiki-studio version as memkernel's:
399
+
400
+ 1. In memkernel's environment, read the version: `python -c "import importlib.metadata as m; print(m.version('wiki-studio'))"`. If it is below 0.8.0, upgrade it there first: `pip install 'wiki-studio>=0.8,<1'`.
401
+ 2. Install exactly that version for the server, for example `pipx install 'wiki-studio[mcp]==0.8.1'`, and register it beside memkernel's server.
402
+
403
+ On one machine and one filesystem, wiki-studio's lock serializes the two servers' writes to the same wiki. It does not coordinate other hosts, and network filesystems are outside its scope.
370
404
 
371
405
  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).
372
406
 
@@ -119,8 +119,9 @@ Copy the hook scripts from [`templates/hooks/`](templates/hooks/) to `.claude/ho
119
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
120
  | `pre-compact.py` | `PreCompact` | Runs a consolidation pass (Claude Code passes no summary here, so no checkpoint is retained) |
121
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
+ | `session-end.py` | `SessionEnd` | Writes one line to the session spool in `MEMKERNEL_STATE_DIR` (no store, no model, within SessionEnd's 1.5-second budget); the next session start says when a session ended without a saved checkpoint. A session that never recorded an end (a crash, or this hook not registered) is dropped from the spool after 7 days, and the next start says how many |
122
123
 
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.
124
+ Each injected memory line ends with its `ref_id`, so `get(ref_id)` returns the full text; a block never exceeds 9,000 characters (Claude Code caps hook output at 10,000), and a cut checkpoint says which `get` holds the rest. At session start the long-term context is the most important decisions, conventions, procedures and facts, not the matches of a fixed query; after a compaction it is recalled with the checkpoint's text. Register `SessionStart` without a matcher, so it runs for every `source`, and `session-end.py` under `SessionEnd`. 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.
124
125
 
125
126
  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.
126
127
 
@@ -145,14 +146,14 @@ Kiro (and the Kiro CLI, formerly Amazon Q Developer CLI) connects to MemKernel a
145
146
  | Tool | What it does |
146
147
  |------|-------------|
147
148
  | `retain(content, type, session_id?, checkpoint?, source?, importance?, tags?, supersedes?)` | Store a memory. Returns `ref_id`. Idempotent — identical content + type returns the same `ref_id`. `supersedes` hides the memory it replaces. |
148
- | `recall(query, type?, tier?, session_id?, checkpoint_only?, top_k?, include_needs_review?)` | Search by natural language or identifier. Returns lightweight `ArtifactRef` list ranked by relevance + recency + importance. Records an access (reinforces retention). Code memories flagged `needs_review` are hidden unless `include_needs_review=true`. |
149
+ | `recall(query, type?, tier?, session_id?, checkpoint_only?, top_k?, include_needs_review?, order?)` | Search by natural language or identifier. Returns lightweight `ArtifactRef` list ranked by relevance + recency + importance. Records an access (reinforces retention). Code memories flagged `needs_review` are hidden unless `include_needs_review=true`. |
149
150
  | `get(ref_id)` | Fetch full content of a specific memory. Records an access. |
150
151
  | `forget(ref_id)` | Delete a memory from all indexes. |
151
152
  | `promote(ref_id, type, content?, importance?, tags?)` | Turn a short-term/working memory into a long-term one (you supply the distilled text). Keeps provenance via `promoted_from`. |
152
153
  | `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. |
153
154
  | `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`. |
154
155
  | `wiki_export(wiki_dir?)` | Compile long-term memories without a wiki page into draft markdown pages (`index.md`, `log.md` updated). |
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). |
156
+ | `wiki_import(wiki_dir?, adopt_legacy?, adopt_pages?, adopt_manifest?)` | Index wiki pages as long-term memories, each wiki keeping to its own: approved revisions only with wiki-studio ≥ 0.7.0, every page without it (idempotent; changed pages supersede; `corrects` supersedes the memories a page corrects). |
156
157
 
157
158
  ### Memory types and tiers
158
159
 
@@ -172,7 +173,7 @@ Retention: `ttl = base_ttl × (1 + access_boost × access_count)`, measured from
172
173
 
173
174
  ### Session protocol
174
175
 
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.
176
+ 1. **Session start** — `recall(tier="long_term", query=<task>)`, plus the latest checkpoint (`checkpoint_only=true, order="newest"`: newest first, not the most relevant). The `session-start.py` hook does this and prints promotion candidates; after a compaction it prints a shorter recovery block instead.
176
177
  2. **While working** — `retain(type="task")` when the plan changes; `retain(type="observation")` for findings; durable knowledge as `decision` / `convention` / `fact` / `procedure`.
177
178
  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.
178
179
  4. **After compaction / end of phase** — `promote(ref_id, type, content)` for the candidates worth keeping.
@@ -191,7 +192,7 @@ retain(
191
192
 
192
193
  recall(query="database choice", top_k=5)
193
194
 
194
- recall(query="session checkpoint", checkpoint_only=True)
195
+ recall(query="session checkpoint", checkpoint_only=True, order="newest", top_k=1) # the latest checkpoint
195
196
 
196
197
  retain(content="pytest hangs unless MEMKERNEL_PROJECT_ID is set", type="observation", tags=["ci"])
197
198
 
@@ -248,7 +249,10 @@ setting is logged as a warning at startup.
248
249
  | `MEMKERNEL_RECALL_WEIGHT_RECENCY` | `0.3` | Weight of recency in the final recall score |
249
250
  | `MEMKERNEL_RECALL_WEIGHT_IMPORTANCE` | `0.3` | Weight of importance in the final recall score |
250
251
  | `MEMKERNEL_WIKI_DIR` | `` | Directory of the LLM wiki used by `wiki_export` / `wiki_import` (empty = pass `wiki_dir` explicitly) |
252
+ | `MEMKERNEL_STATE_DIR` | `.memkernel/state` | memkernel's own state outside ChromaDB: the wiki sync ledger. Resolved like `MEMKERNEL_CHROMA_PERSIST_PATH` (a relative path is relative to the process's working directory, so set it absolute when the persist path is); keep it out of git |
251
253
  | `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. |
254
+ | `MEMKERNEL_WIKI_MODE` | `auto` | `auto`: wiki-studio's gate and review state when wiki-studio 0.7.0 or later is installed, the direct writer and file-reading import when it is not. `governed`: `wiki_export` and `wiki_import` refuse, writing nothing, unless wiki-studio is installed with every capability memkernel uses and the wiki's manifest is `governed` (`approval_required: true`) without schema drift. `ungated`: wiki-studio is never loaded. Responses report `mode` (`gated` or `ungated`, the path that ran) and `configured_mode` (this setting). |
255
+ | `MEMKERNEL_WIKI_EXPORT_BATCH` | `10` | New pages one `wiki_export` writes at most, the most important memories first; the response's `remaining` counts the rest (`null` when the gate's outcome is uncertain or `governed` refused before reading the wiki). `0` = no limit; a negative value is refused |
252
256
  | `MEMKERNEL_TRANSPORT` | `stdio` | `stdio` or `streamable-http` |
253
257
  | `MEMKERNEL_HOST` | `127.0.0.1` | Host for `streamable-http` transport |
254
258
  | `MEMKERNEL_PORT` | `8765` | Port for `streamable-http` transport |
@@ -290,7 +294,9 @@ for ref in svc.recall("database"):
290
294
  report = svc.consolidate() # {"purged": n, "candidates": [...]}
291
295
  ```
292
296
 
293
- `build_service()` is the supported entry point; the MCP server, the admin API and the hook templates all use it. Integration notes for `code-copilot-team` and `haa` are in [`docs/integration/harness-integration.md`](docs/integration/harness-integration.md).
297
+ `build_service()` is the supported entry point; the MCP server, the admin API and the hook templates all use it. If you pass a `PersistentClient` as `chroma_client` and also set `MEMKERNEL_CHROMA_PERSIST_PATH` (in the environment or in the settings), both must name the same directory, or `ValueError` is raised; with the default setting, the client's own directory is used.
298
+
299
+ **Store locks.** Processes that share a store (the MCP server, a hook, a script) coordinate through locks keyed by the store itself: lock files in the persist directory, `.memkernel-<name>-<key>.lock`, whatever each process's `MEMKERNEL_STATE_DIR` is; an in-memory store's locks are per process. Only one writer changes a record's tags, under the store's `tags` lock, so a tag added by one process is never dropped by another's write. Locks are not re-entrant (taking one the thread already holds raises `LockReentryError`), a wait longer than 10 seconds is logged once, and spelling the persist directory with different letter case in two processes gives two locks, so use one spelling. The locks use `fcntl` and are verified on POSIX (Linux, macOS) only; on Windows they run unlocked across processes, with one logged warning per lock. Integration notes for `code-copilot-team` and `haa` are in [`docs/integration/harness-integration.md`](docs/integration/harness-integration.md).
294
300
 
295
301
  ### The session protocol as a component
296
302
 
@@ -329,13 +335,41 @@ memory.capture_compact_summary(compact_summary, session_id=..., trigger=...) #
329
335
 
330
336
  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`).
331
337
 
338
+ To see the whole loop on a clean install, follow wiki-studio's [memkernel composition quickstart](https://github.com/gosha70/wiki-studio/blob/main/docs/quickstart-memkernel.md): retain a memory, export it through wiki-studio's gate, correct and approve the page, import only the approved revision, and see the next `recall` return it. CI runs the quickstart against memkernel **0.6.0**.
339
+
332
340
  - `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.
333
341
  - `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`.
342
+ - **Withdrawal (governed import, 0.7).** A page's memory leaves the current answers, its record kept for history, when:
343
+ - the page is **deleted**. Only memories this wiki's sync ledger recorded as imported are withdrawn this way; a memory imported before 0.7, or without wiki-studio, never is.
344
+ - the page is **deprecated**, once the `status: deprecated` revision is approved.
345
+ - the page is **superseded** (`superseded_by: <page path>`). Its memory is replaced by the successor page's memory; if the successor has none yet, the old memory is withdrawn and linked once it does (`pending` until then).
346
+
347
+ The response lists `withdrawn`, `superseded`, `pending` and `sync_failed` (retried at the next import). The ledger is one JSON file per project and wiki under `MEMKERNEL_STATE_DIR/wiki-sync/`; a corrupt one is kept aside, reported as `ledger_error`, and rebuilt.
334
348
  - **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
349
 
350
+ - **Each wiki keeps to its own memories (0.7).** Every memory `wiki_import` writes, or finds current, carries its wiki's identity: `wiki-path:<hash>` (the wiki directory's path relative to the project root) and, when the wiki has a wiki-studio manifest, `wiki-id:<uuid>`. A page's previous memory, its corrections, its supersession and its deprecation are only that wiki's, so two wikis in one store may share page paths and `page_id`s. These tags are reserved: `retain` refuses them, and an ordinary `retain` whose text matches a wiki memory gets its own record instead of merging into it; `promote` drops every wiki tag from its copy.
351
+ - **A wiki that may move, or that shares a store with another wiki, needs a manifest.** The path a wiki is known by is relative to its project root: two project roots sharing one store, with wikis at the same relative path, get the same `wiki-path:`, so the second import is refused (memories at this path belong to another wiki) even with manifests. Use one store per project, or different relative paths. Without one, a wiki *is* its path: moved, it starts afresh, and whatever wiki is later placed at the old path continues its memories. A copied wiki directory (two directories, one `wiki_id`) is one wiki: each import moves its memories to the directory imported last.
352
+ - **Upgrading a 0.6 store:** memories written before 0.7 carry no identity, so the first import writes nothing for their pages and lists them under `ambiguous` (`{"path", "ref_ids", "reason"}`). Import once with `adopt_legacy=true` to adopt each page's sole such memory; a page with several stays `ambiguous` (forget the ones that are not its own). With several wikis in one store, name the records instead: `adopt_pages=[{"path", "ref_id"}, …]` copied from `ambiguous`; a named record that changed meanwhile is not adopted (`candidate changed`).
353
+ - **Identity changes stop the import before any write, with the remedy:** a wiki that gained a manifest needs `adopt_manifest=true` once (its sync ledger moves to the `wiki_id` key; if both ledger files exist, delete the stale one); memories at this path that belong to another `wiki_id` (the manifest removed or replaced, or a moved wiki not yet imported from its new place) stop it until the manifest is restored, the moved wiki is imported first, or those memories are forgotten. A wiki with a manifest that moves keeps its memories; the next import moves their path.
354
+ - One import runs at a time per store (the `wiki-import` store lock). On Windows the import runs unlocked with one warning, as in 0.6: two concurrent imports of one wiki can leave two records for a page (the next import reports it as `duplicate records`; forget one), and two wikis adopting the same record at once can tag it for one while the other supersedes it. Give processes that share a store the same `MEMKERNEL_STATE_DIR`, so they share the sync ledger.
336
355
  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.
337
356
 
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`).
357
+ 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/`, `.memkernel/state/` (memkernel's sync ledger) and wiki-studio's files next to the wiki directory, `.<wiki-dir>.wiki-studio.*` (for `knowledge/wiki`, `knowledge/.wiki.wiki-studio.*`: its lock file and, from wiki-studio 0.8, its search cache).
358
+
359
+ **Wiki modes.** `MEMKERNEL_WIKI_MODE` is a property of the deployment, not of a call. `governed` runs one preflight before any page is written or read, for export and import alike: wiki-studio 0.7.0 or later installed and working; its `info` listing the capabilities `wiki-lock`, `receipts`, `root-manifest`, `page-id`, `approval-record`, `superseded-by` and `findings`; the wiki's manifest mode `governed`; no schema drift. A `managed` or `legacy` wiki is refused with the remedy: run `wiki-studio init` if there is no manifest, then set `approval_required: true` in the manifest's policy. A refusal reports `gate_error` (export) or `import_error` (import) and writes nothing; `update_index=False` is refused too, since the direct writer is never used. A gate rejection after the preflight commits no drafted page, but a new wiki keeps the `index.md` and `log.md` that wiki-studio's `init` created: memkernel keeps no outer rollback. `ungated` never loads wiki-studio (no gate, lint, review state or findings): `lint` is `null` and `lint_hint` names the mode.
360
+
361
+ **Export batches and grounding.** One `wiki_export` writes at most `MEMKERNEL_WIKI_EXPORT_BATCH` new pages (by importance, highest first, then oldest, then `ref_id`), so a curator reviews a small batch at a time; `remaining` counts the eligible memories still without a page, all of them after a rejected batch. Every exported page carries `memkernel_sha256`, the memory's content hash, beside `memkernel_ref`; a `path:` source gains `sha:`, the commit it is grounded at (`git log -1 --format=%h`, wiki-studio's citation rule), only when the project's `git ls-files` lists exactly that file and `git status` shows no change to it: a modified, staged or untracked file, a file in a submodule, or no git gets none. Pages created through the gate carry wiki-studio's `page_id`, so a page renamed or moved under its `page_id` keeps its memory: the next import supersedes the memory at the old path with one at the new path, also when the moved page's bytes are unchanged.
362
+
363
+ **Curation debt at session start.** With `MEMKERNEL_WIKI_DIR` set and wiki-studio available (in `governed`, only after the preflight passes), the session-start block (`startup`, `clear`) gains `### Curation debt`: one line of wiki-studio's `findings` counts in a fixed order (review kinds first) plus the sync ledger's unfinished actions (`sync pending`, `sync failed`), up to three example pages, and a hint to run `wiki-studio findings`. It uses at most 500 characters, taken from the long-term context's share, so the block stays under 9,000 characters. The check runs in a child process bounded to 2.5 seconds once it has started (a fork or exec the operating system holds up is not covered); it reads the sync ledger without changing it and never waits for an import that holds it. Any failure is one `Curation debt unavailable: <reason>` line. Not on `compact`, `resume` or `fork`, nor in `ungated`.
364
+
365
+ **wiki-studio's files beside the wiki, its ingest log, and wiki-studio 0.8.** When memkernel exports through the gate, wiki-studio creates its lock file, `.<wiki-dir>.wiki-studio.lock`, beside the wiki directory, and from 0.8 a curator's `find` or `query` adds a search cache, `.<wiki-dir>.wiki-studio.search.sqlite`, there too: for a wiki kept in git, ignore both with `.<wiki-dir>.wiki-studio.*` (for `knowledge/wiki`, `knowledge/.wiki.wiki-studio.*`). memkernel creates no file of its own there. memkernel reads no wiki-studio ingest log, so the log's `hash_recipe` has no consumer in memkernel. From wiki-studio 0.8.0 a page that is not valid UTF-8 fails lint for the whole wiki, so every promote, memkernel's gated export included, is refused with a `gate_error` naming that page until it is re-encoded or removed. From 0.8.0 an unknown `WIKI_STUDIO_RANKER` value makes wiki-studio's `info` fail, so a `governed` export or import, a gated import and the curation debt refuse with wiki-studio's message naming the variable. memkernel needs wiki-studio 0.7.0 or later and is tested at 0.7.0; an informational CI job runs the wiki tests against the newest release below 1.0 on every pull request and weekly.
366
+
367
+ wiki-studio's MCP server (`wiki-studio-mcp`, from wiki-studio 0.8.0) needs `mcp>=2.3`, which memkernel's `mcp<2` excludes. Run it in its own environment, at the same wiki-studio version as memkernel's:
368
+
369
+ 1. In memkernel's environment, read the version: `python -c "import importlib.metadata as m; print(m.version('wiki-studio'))"`. If it is below 0.8.0, upgrade it there first: `pip install 'wiki-studio>=0.8,<1'`.
370
+ 2. Install exactly that version for the server, for example `pipx install 'wiki-studio[mcp]==0.8.1'`, and register it beside memkernel's server.
371
+
372
+ On one machine and one filesystem, wiki-studio's lock serializes the two servers' writes to the same wiki. It does not coordinate other hosts, and network filesystems are outside its scope.
339
373
 
340
374
  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).
341
375
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "memkernel"
3
- version = "0.6.0"
3
+ version = "0.7.0"
4
4
  description = "Code-aware memory server for AI coding copilots"
5
5
  authors = ["EGOGE"]
6
6
  license = "Apache-2.0"
@@ -2,4 +2,4 @@
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  """MemKernel — Code-aware memory server for AI coding copilots."""
4
4
 
5
- __version__ = "0.6.0"
5
+ __version__ = "0.7.0"
@@ -7,6 +7,7 @@ from __future__ import annotations
7
7
  import logging
8
8
  from typing import TYPE_CHECKING, Any, Literal
9
9
 
10
+ from pydantic import Field
10
11
  from pydantic_settings import BaseSettings, EnvSettingsSource, PydanticBaseSettingsSource, SettingsConfigDict
11
12
 
12
13
  if TYPE_CHECKING:
@@ -34,6 +35,10 @@ class MemKernelSettings(BaseSettings):
34
35
  chroma_persist_path: str = ".memkernel/chroma"
35
36
  chroma_collection_prefix: str = "memkernel"
36
37
 
38
+ # memkernel's own state outside Chroma (memkernel-0-7): the wiki sync ledger. Resolved like
39
+ # chroma_persist_path: a relative path is relative to the working directory of the process.
40
+ state_dir: str = ".memkernel/state"
41
+
37
42
  # Embedding
38
43
  embedding_model: str = "all-MiniLM-L6-v2"
39
44
  embedding_dimension: int = 384
@@ -79,6 +84,10 @@ class MemKernelSettings(BaseSettings):
79
84
  # Where wiki-studio patch-sets are written before promote (memkernel[wiki], ADR-010).
80
85
  # Empty = <project_root>/.memkernel/wiki-proposals; set it to request the gate explicitly.
81
86
  wiki_proposals_dir: str = ""
87
+ # Release B part 2c (G1): auto = the gate when wiki-studio is installed, the direct writer otherwise; governed =
88
+ # wiki-studio and a governed wiki required, refused otherwise; ungated = wiki-studio is never loaded.
89
+ wiki_mode: Literal["auto", "governed", "ungated"] = "auto"
90
+ wiki_export_batch: int = Field(default=10, ge=0) # new pages per wiki_export, most important first (G3); 0 = none
82
91
 
83
92
  # Server
84
93
  transport: Literal["stdio", "streamable-http"] = "stdio"
@@ -102,11 +102,34 @@ CODE_CHUNK_KEY = "code_chunk" # boolean metadata so adapters can skip chunk rec
102
102
  PARENT_REF_KEY = "parent_ref"
103
103
  # Durable liveness marker (0.4): set on a record that another record replaced.
104
104
  SUPERSEDED_BY_KEY = "superseded_by"
105
+ # Durable liveness marker (0.7, memkernel-0-7 D1): set on a record taken out of the current answers with no
106
+ # replacing record (a deleted, deprecated or superseded wiki page); the reason and the time it happened.
107
+ WITHDRAWN_REASON_KEY = "withdrawn_reason"
108
+ WITHDRAWN_AT_KEY = "withdrawn_at"
109
+ WITHDRAWN_DELETED = "deleted"
110
+ WITHDRAWN_DEPRECATED = "deprecated"
111
+ WITHDRAWN_SUPERSEDED = "superseded"
112
+ WITHDRAWN_REASONS = frozenset({WITHDRAWN_DELETED, WITHDRAWN_DEPRECATED, WITHDRAWN_SUPERSEDED})
105
113
  # Revalidation flags (0.4).
106
114
  NEEDS_REVIEW_KEY = "needs_review"
107
115
  REVIEW_REASON_KEY = "review_reason"
108
116
  REVIEW_REASON_CHANGED = "changed"
109
117
  REVIEW_REASON_MISSING = "missing"
118
+ # Wiki-import tags (memkernel-0-6, memkernel-0-7-wiki-scope): what the wiki import writes on a memory. The two
119
+ # identity tags name the wiki a memory belongs to; only the import sets them (S8).
120
+ WIKI_TAG = "wiki"
121
+ WIKI_PAGE_TAG = "page:"
122
+ WIKI_PAGE_TYPE_TAG = "page_type:"
123
+ PAGE_KEY_TAG = "page_key:"
124
+ PAGE_SHA_TAG = "page_sha:"
125
+ WIKI_PATH_TAG = "wiki-path:"
126
+ WIKI_ID_TAG = "wiki-id:"
127
+ WIKI_IDENTITY_TAGS = (WIKI_PATH_TAG, WIKI_ID_TAG)
128
+ WIKI_IMPORT_TAG_PREFIXES = (WIKI_PAGE_TAG, WIKI_PAGE_TYPE_TAG, PAGE_KEY_TAG, PAGE_SHA_TAG, *WIKI_IDENTITY_TAGS)
129
+ # Store locks (memkernel-0-7-store-locks L2): the names a caller may take with ``VectorStorePort.exclusive``.
130
+ STORE_LOCK_WIKI_IMPORT = "wiki-import"
131
+ STORE_LOCK_TAGS = "tags" # held by ``merge_tags``, the only writer of a record's tags
132
+ STORE_LOCK_NAMES = frozenset({STORE_LOCK_WIKI_IMPORT, STORE_LOCK_TAGS})
110
133
 
111
134
 
112
135
  def tier_of(memory_type: MemoryType) -> MemoryTier:
@@ -169,6 +192,13 @@ class Memory(BaseModel):
169
192
  parent_ref: str | None = None
170
193
  needs_review: bool = False
171
194
  review_reason: str = ""
195
+ # 0.7: withdrawn without a replacement (memkernel-0-7 FR-001); absent, so live, in older stores.
196
+ withdrawn_reason: str | None = None
197
+ withdrawn_at: datetime | None = None
198
+
199
+ def retired(self) -> bool:
200
+ """True for history: superseded by another record or withdrawn (memkernel-0-7 FR-002)."""
201
+ return self.superseded_by is not None or self.withdrawn_reason is not None
172
202
 
173
203
 
174
204
  class HealthItem(BaseModel):
@@ -206,6 +236,23 @@ class ReasonerError(RuntimeError):
206
236
  """A ReasonerPort implementation ran but did not produce an answer (provider error, timeout, budget)."""
207
237
 
208
238
 
239
+ class AmbiguousDedupError(ValueError):
240
+ """Two or more live records qualify as one retain's dedup hit (memkernel-0-7-wiki-scope S2): none is chosen."""
241
+
242
+ def __init__(self, ref_ids: list[str]) -> None:
243
+ super().__init__(f"several records qualify as the dedup hit: {', '.join(ref_ids)}")
244
+ self.ref_ids = ref_ids
245
+
246
+
247
+ def has_wiki_identity(tags: list[str]) -> bool:
248
+ """Whether a memory carries a wiki identity tag (``wiki-path:`` or ``wiki-id:``)."""
249
+ return any(tag.startswith(WIKI_IDENTITY_TAGS) for tag in tags)
250
+
251
+
252
+ class LockReentryError(RuntimeError):
253
+ """A thread took a store lock it already holds (memkernel-0-7-store-locks L2): store locks are not re-entrant."""
254
+
255
+
209
256
  class Answer(BaseModel):
210
257
  """Result of ``ask``: the answer text, the reasoning mode actually used, and provenance."""
211
258
 
@@ -11,6 +11,9 @@ from __future__ import annotations
11
11
  from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
12
12
 
13
13
  if TYPE_CHECKING:
14
+ from collections.abc import Sequence
15
+ from contextlib import AbstractContextManager
16
+
14
17
  from memkernel.core.models import Answer, Memory, SearchResult
15
18
 
16
19
 
@@ -50,7 +53,27 @@ class VectorStorePort(Protocol):
50
53
  ...
51
54
 
52
55
  def update_metadata(self, ref_id: str, metadata: dict[str, Any]) -> None:
53
- """Fetch-and-merge: only overwrites caller-specified keys."""
56
+ """Overwrite only the caller's keys; every other key is kept.
57
+
58
+ Raises ``ValueError`` for a ``tags`` key: ``merge_tags`` is the only writer of tags (memkernel-0-7-store-locks
59
+ L3), so a write of other keys can never put back a stale tag list.
60
+ """
61
+ ...
62
+
63
+ def merge_tags(self, ref_id: str, add: Sequence[str] = (), remove: Sequence[str] = ()) -> bool:
64
+ """Drop ``remove``, then append the missing ``add`` tags, read and written under the store's tags lock.
65
+
66
+ A tag in both ends up present, after the kept ones. Returns False when the record is unknown or its tags
67
+ are already as asked. Raises ``LockReentryError`` when the calling thread already holds the tags lock.
68
+ """
69
+ ...
70
+
71
+ def exclusive(self, name: str) -> AbstractContextManager[None]:
72
+ """Hold the store's lock ``name`` (one of ``STORE_LOCK_NAMES``) until the context exits.
73
+
74
+ Keyed by the store itself, not by settings; blocks until acquired; not re-entrant (``LockReentryError``).
75
+ Where the platform has no lock, it runs unlocked with one logged warning.
76
+ """
54
77
  ...
55
78
 
56
79
  def count(self, where: dict[str, Any] | None = None) -> int: