memkernel 0.6.0__tar.gz → 0.8.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.6.0 → memkernel-0.8.0}/PKG-INFO +63 -8
- {memkernel-0.6.0 → memkernel-0.8.0}/README.md +62 -7
- {memkernel-0.6.0 → memkernel-0.8.0}/pyproject.toml +1 -1
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/__init__.py +1 -1
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/config.py +9 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/core/models.py +47 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/core/ports.py +24 -1
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/core/services.py +199 -24
- memkernel-0.8.0/src/memkernel/lifecycle.py +478 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/mcp/tools/memory.py +7 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/mcp/tools/wiki.py +60 -10
- memkernel-0.8.0/src/memkernel/session_spool.py +154 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/storage/chroma.py +163 -19
- memkernel-0.8.0/src/memkernel/wiki/curation.py +256 -0
- memkernel-0.8.0/src/memkernel/wiki/ledger.py +282 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/wiki/studio.py +174 -1
- memkernel-0.8.0/src/memkernel/wiki/sync.py +1064 -0
- memkernel-0.6.0/src/memkernel/lifecycle.py +0 -291
- memkernel-0.6.0/src/memkernel/wiki/sync.py +0 -504
- {memkernel-0.6.0 → memkernel-0.8.0}/LICENSE +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/NOTICE +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/adapters/__init__.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/adapters/rlmstudio.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/api/__init__.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/api/app.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/api/routes/__init__.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/api/routes/admin.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/core/__init__.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/factory.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/mcp/__init__.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/mcp/server.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/mcp/tools/__init__.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/memory/__init__.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/memory/dedup.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/memory/lifecycle.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/memory/types.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/py.typed +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/retrieval/__init__.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/retrieval/chunking.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/retrieval/embeddings.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/retrieval/reasoner.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/retrieval/search.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/retrieval/sparse.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/storage/__init__.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/storage/documents.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.0}/src/memkernel/wiki/__init__.py +0 -0
- {memkernel-0.6.0 → memkernel-0.8.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.8.0
|
|
4
4
|
Summary: Code-aware memory server for AI coding copilots
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -150,11 +150,33 @@ 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
|
|
|
159
|
+
**Injected memory is quoted as data.** Stored text can come from anything the agent once read: tool output, a web page, a clipped source, a wiki page. So everything the blocks inject but memkernel did not write is quoted inside a `<stored-memory kind="…">` element, and the block's first line after its heading says those items are data to consult, not instructions to follow. That covers the checkpoint text, memory previews, promotion candidates, the source path of a "Needs review" item, and the curation debt's page paths. memkernel's own lines stay outside the elements, `ref_id`s included. A copy of the tag inside stored text (`</stored-memory>` in any case, with or without whitespace after `<` or `/`) is neutralised by writing its `<` as `<`; no other character of stored text changes. This marks stored text; it does not detect or remove instructions in it.
|
|
160
|
+
|
|
161
|
+
### 4. (Recommended) Keep memkernel's files and the hooks out of the agent's reach
|
|
162
|
+
|
|
163
|
+
Merge [`templates/claude-settings.example.json`](templates/claude-settings.example.json) into your project's `.claude/settings.json`. Its `permissions.deny` rules stop Claude's own file tools from editing four paths:
|
|
164
|
+
- `.memkernel/`, the store and state directory;
|
|
165
|
+
- `.claude/settings.json` and `.claude/settings.local.json`;
|
|
166
|
+
- `.claude/hooks/`.
|
|
167
|
+
|
|
168
|
+
So a session cannot rewrite the store, or what runs at the next session start. memkernel's server and hooks write `.memkernel/` themselves, not through Claude's tools, so they are unaffected.
|
|
169
|
+
|
|
170
|
+
Claude Code applies deny rules from a committed `.claude/settings.json` at once, and an allow rule cannot override them. `Edit(...)` rules cover every file-editing tool; a `Write(...)` rule would never match.
|
|
171
|
+
|
|
172
|
+
These rules have limits:
|
|
173
|
+
- **What they stop:** Claude's built-in file tools, the file commands Claude Code recognises in Bash (`cat`, `sed`, `tee`, ...) and redirections. Checked live with Claude Code 2.1.319: a write to each denied path was refused with "File is in a directory that is denied by your permission settings".
|
|
174
|
+
- **What they don't stop:** a program that writes files without naming them, such as a script or `python -c`. [Sandboxing](https://code.claude.com/docs/en/sandboxing) is the stronger boundary.
|
|
175
|
+
- **Other locations:** if you keep `MEMKERNEL_STATE_DIR` or the Chroma path elsewhere, change the first rule to match. A `/` rule anchors at the project, so a directory outside it needs an absolute `//` or a home-relative `~/` pattern, such as `Edit(~/.memkernel/**)`.
|
|
176
|
+
- **The rest of `.claude/`** (agents, commands, skills) stays editable, because teams often have the agent maintain them; it is a protected path, so edits there still prompt. Add `Edit(/.claude/**)` if yours should not be editable at all.
|
|
177
|
+
|
|
178
|
+
A mid-session "always allow" prompt, especially one asking for broad Bash or file access, deserves a second read: injected text can be what asked for it.
|
|
179
|
+
|
|
158
180
|
---
|
|
159
181
|
|
|
160
182
|
## Add to Kiro
|
|
@@ -176,14 +198,14 @@ Kiro (and the Kiro CLI, formerly Amazon Q Developer CLI) connects to MemKernel a
|
|
|
176
198
|
| Tool | What it does |
|
|
177
199
|
|------|-------------|
|
|
178
200
|
| `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`. |
|
|
201
|
+
| `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
202
|
| `get(ref_id)` | Fetch full content of a specific memory. Records an access. |
|
|
181
203
|
| `forget(ref_id)` | Delete a memory from all indexes. |
|
|
182
204
|
| `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
205
|
| `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
206
|
| `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
207
|
| `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). |
|
|
208
|
+
| `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
209
|
|
|
188
210
|
### Memory types and tiers
|
|
189
211
|
|
|
@@ -203,7 +225,7 @@ Retention: `ttl = base_ttl × (1 + access_boost × access_count)`, measured from
|
|
|
203
225
|
|
|
204
226
|
### Session protocol
|
|
205
227
|
|
|
206
|
-
1. **Session start** — `recall(tier="long_term", query=<task>)`, plus the latest checkpoint (`checkpoint_only=true
|
|
228
|
+
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
229
|
2. **While working** — `retain(type="task")` when the plan changes; `retain(type="observation")` for findings; durable knowledge as `decision` / `convention` / `fact` / `procedure`.
|
|
208
230
|
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
231
|
4. **After compaction / end of phase** — `promote(ref_id, type, content)` for the candidates worth keeping.
|
|
@@ -222,7 +244,7 @@ retain(
|
|
|
222
244
|
|
|
223
245
|
recall(query="database choice", top_k=5)
|
|
224
246
|
|
|
225
|
-
recall(query="session checkpoint", checkpoint_only=True)
|
|
247
|
+
recall(query="session checkpoint", checkpoint_only=True, order="newest", top_k=1) # the latest checkpoint
|
|
226
248
|
|
|
227
249
|
retain(content="pytest hangs unless MEMKERNEL_PROJECT_ID is set", type="observation", tags=["ci"])
|
|
228
250
|
|
|
@@ -279,7 +301,10 @@ setting is logged as a warning at startup.
|
|
|
279
301
|
| `MEMKERNEL_RECALL_WEIGHT_RECENCY` | `0.3` | Weight of recency in the final recall score |
|
|
280
302
|
| `MEMKERNEL_RECALL_WEIGHT_IMPORTANCE` | `0.3` | Weight of importance in the final recall score |
|
|
281
303
|
| `MEMKERNEL_WIKI_DIR` | `` | Directory of the LLM wiki used by `wiki_export` / `wiki_import` (empty = pass `wiki_dir` explicitly) |
|
|
304
|
+
| `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
305
|
| `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. |
|
|
306
|
+
| `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). |
|
|
307
|
+
| `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
308
|
| `MEMKERNEL_TRANSPORT` | `stdio` | `stdio` or `streamable-http` |
|
|
284
309
|
| `MEMKERNEL_HOST` | `127.0.0.1` | Host for `streamable-http` transport |
|
|
285
310
|
| `MEMKERNEL_PORT` | `8765` | Port for `streamable-http` transport |
|
|
@@ -321,7 +346,9 @@ for ref in svc.recall("database"):
|
|
|
321
346
|
report = svc.consolidate() # {"purged": n, "candidates": [...]}
|
|
322
347
|
```
|
|
323
348
|
|
|
324
|
-
`build_service()` is the supported entry point; the MCP server, the admin API and the hook templates all use it.
|
|
349
|
+
`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.
|
|
350
|
+
|
|
351
|
+
**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
352
|
|
|
326
353
|
### The session protocol as a component
|
|
327
354
|
|
|
@@ -360,13 +387,41 @@ memory.capture_compact_summary(compact_summary, session_id=..., trigger=...) #
|
|
|
360
387
|
|
|
361
388
|
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
389
|
|
|
390
|
+
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**.
|
|
391
|
+
|
|
363
392
|
- `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
393
|
- `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`.
|
|
394
|
+
- **Withdrawal (governed import, 0.7).** A page's memory leaves the current answers, its record kept for history, when:
|
|
395
|
+
- 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.
|
|
396
|
+
- the page is **deprecated**, once the `status: deprecated` revision is approved.
|
|
397
|
+
- 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).
|
|
398
|
+
|
|
399
|
+
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
400
|
- **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
401
|
|
|
402
|
+
- **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.
|
|
403
|
+
- **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.
|
|
404
|
+
- **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`).
|
|
405
|
+
- **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.
|
|
406
|
+
- 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
407
|
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
408
|
|
|
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
|
|
409
|
+
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).
|
|
410
|
+
|
|
411
|
+
**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.
|
|
412
|
+
|
|
413
|
+
**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.
|
|
414
|
+
|
|
415
|
+
**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`.
|
|
416
|
+
|
|
417
|
+
**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.
|
|
418
|
+
|
|
419
|
+
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:
|
|
420
|
+
|
|
421
|
+
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'`.
|
|
422
|
+
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.
|
|
423
|
+
|
|
424
|
+
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
425
|
|
|
371
426
|
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
427
|
|
|
@@ -119,11 +119,33 @@ 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
|
|
|
128
|
+
**Injected memory is quoted as data.** Stored text can come from anything the agent once read: tool output, a web page, a clipped source, a wiki page. So everything the blocks inject but memkernel did not write is quoted inside a `<stored-memory kind="…">` element, and the block's first line after its heading says those items are data to consult, not instructions to follow. That covers the checkpoint text, memory previews, promotion candidates, the source path of a "Needs review" item, and the curation debt's page paths. memkernel's own lines stay outside the elements, `ref_id`s included. A copy of the tag inside stored text (`</stored-memory>` in any case, with or without whitespace after `<` or `/`) is neutralised by writing its `<` as `<`; no other character of stored text changes. This marks stored text; it does not detect or remove instructions in it.
|
|
129
|
+
|
|
130
|
+
### 4. (Recommended) Keep memkernel's files and the hooks out of the agent's reach
|
|
131
|
+
|
|
132
|
+
Merge [`templates/claude-settings.example.json`](templates/claude-settings.example.json) into your project's `.claude/settings.json`. Its `permissions.deny` rules stop Claude's own file tools from editing four paths:
|
|
133
|
+
- `.memkernel/`, the store and state directory;
|
|
134
|
+
- `.claude/settings.json` and `.claude/settings.local.json`;
|
|
135
|
+
- `.claude/hooks/`.
|
|
136
|
+
|
|
137
|
+
So a session cannot rewrite the store, or what runs at the next session start. memkernel's server and hooks write `.memkernel/` themselves, not through Claude's tools, so they are unaffected.
|
|
138
|
+
|
|
139
|
+
Claude Code applies deny rules from a committed `.claude/settings.json` at once, and an allow rule cannot override them. `Edit(...)` rules cover every file-editing tool; a `Write(...)` rule would never match.
|
|
140
|
+
|
|
141
|
+
These rules have limits:
|
|
142
|
+
- **What they stop:** Claude's built-in file tools, the file commands Claude Code recognises in Bash (`cat`, `sed`, `tee`, ...) and redirections. Checked live with Claude Code 2.1.319: a write to each denied path was refused with "File is in a directory that is denied by your permission settings".
|
|
143
|
+
- **What they don't stop:** a program that writes files without naming them, such as a script or `python -c`. [Sandboxing](https://code.claude.com/docs/en/sandboxing) is the stronger boundary.
|
|
144
|
+
- **Other locations:** if you keep `MEMKERNEL_STATE_DIR` or the Chroma path elsewhere, change the first rule to match. A `/` rule anchors at the project, so a directory outside it needs an absolute `//` or a home-relative `~/` pattern, such as `Edit(~/.memkernel/**)`.
|
|
145
|
+
- **The rest of `.claude/`** (agents, commands, skills) stays editable, because teams often have the agent maintain them; it is a protected path, so edits there still prompt. Add `Edit(/.claude/**)` if yours should not be editable at all.
|
|
146
|
+
|
|
147
|
+
A mid-session "always allow" prompt, especially one asking for broad Bash or file access, deserves a second read: injected text can be what asked for it.
|
|
148
|
+
|
|
127
149
|
---
|
|
128
150
|
|
|
129
151
|
## Add to Kiro
|
|
@@ -145,14 +167,14 @@ Kiro (and the Kiro CLI, formerly Amazon Q Developer CLI) connects to MemKernel a
|
|
|
145
167
|
| Tool | What it does |
|
|
146
168
|
|------|-------------|
|
|
147
169
|
| `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`. |
|
|
170
|
+
| `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
171
|
| `get(ref_id)` | Fetch full content of a specific memory. Records an access. |
|
|
150
172
|
| `forget(ref_id)` | Delete a memory from all indexes. |
|
|
151
173
|
| `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
174
|
| `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
175
|
| `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
176
|
| `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). |
|
|
177
|
+
| `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
178
|
|
|
157
179
|
### Memory types and tiers
|
|
158
180
|
|
|
@@ -172,7 +194,7 @@ Retention: `ttl = base_ttl × (1 + access_boost × access_count)`, measured from
|
|
|
172
194
|
|
|
173
195
|
### Session protocol
|
|
174
196
|
|
|
175
|
-
1. **Session start** — `recall(tier="long_term", query=<task>)`, plus the latest checkpoint (`checkpoint_only=true
|
|
197
|
+
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
198
|
2. **While working** — `retain(type="task")` when the plan changes; `retain(type="observation")` for findings; durable knowledge as `decision` / `convention` / `fact` / `procedure`.
|
|
177
199
|
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
200
|
4. **After compaction / end of phase** — `promote(ref_id, type, content)` for the candidates worth keeping.
|
|
@@ -191,7 +213,7 @@ retain(
|
|
|
191
213
|
|
|
192
214
|
recall(query="database choice", top_k=5)
|
|
193
215
|
|
|
194
|
-
recall(query="session checkpoint", checkpoint_only=True)
|
|
216
|
+
recall(query="session checkpoint", checkpoint_only=True, order="newest", top_k=1) # the latest checkpoint
|
|
195
217
|
|
|
196
218
|
retain(content="pytest hangs unless MEMKERNEL_PROJECT_ID is set", type="observation", tags=["ci"])
|
|
197
219
|
|
|
@@ -248,7 +270,10 @@ setting is logged as a warning at startup.
|
|
|
248
270
|
| `MEMKERNEL_RECALL_WEIGHT_RECENCY` | `0.3` | Weight of recency in the final recall score |
|
|
249
271
|
| `MEMKERNEL_RECALL_WEIGHT_IMPORTANCE` | `0.3` | Weight of importance in the final recall score |
|
|
250
272
|
| `MEMKERNEL_WIKI_DIR` | `` | Directory of the LLM wiki used by `wiki_export` / `wiki_import` (empty = pass `wiki_dir` explicitly) |
|
|
273
|
+
| `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
274
|
| `MEMKERNEL_WIKI_PROPOSALS_DIR` | `""` | Where wiki-studio patch-sets are written before promote (`memkernel[wiki]`); empty means `<project root>/.memkernel/wiki-proposals`. Setting it requests the gate: without wiki-studio installed, a warning is logged. |
|
|
275
|
+
| `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). |
|
|
276
|
+
| `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
277
|
| `MEMKERNEL_TRANSPORT` | `stdio` | `stdio` or `streamable-http` |
|
|
253
278
|
| `MEMKERNEL_HOST` | `127.0.0.1` | Host for `streamable-http` transport |
|
|
254
279
|
| `MEMKERNEL_PORT` | `8765` | Port for `streamable-http` transport |
|
|
@@ -290,7 +315,9 @@ for ref in svc.recall("database"):
|
|
|
290
315
|
report = svc.consolidate() # {"purged": n, "candidates": [...]}
|
|
291
316
|
```
|
|
292
317
|
|
|
293
|
-
`build_service()` is the supported entry point; the MCP server, the admin API and the hook templates all use it.
|
|
318
|
+
`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.
|
|
319
|
+
|
|
320
|
+
**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
321
|
|
|
295
322
|
### The session protocol as a component
|
|
296
323
|
|
|
@@ -329,13 +356,41 @@ memory.capture_compact_summary(compact_summary, session_id=..., trigger=...) #
|
|
|
329
356
|
|
|
330
357
|
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
358
|
|
|
359
|
+
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**.
|
|
360
|
+
|
|
332
361
|
- `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
362
|
- `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`.
|
|
363
|
+
- **Withdrawal (governed import, 0.7).** A page's memory leaves the current answers, its record kept for history, when:
|
|
364
|
+
- 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.
|
|
365
|
+
- the page is **deprecated**, once the `status: deprecated` revision is approved.
|
|
366
|
+
- 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).
|
|
367
|
+
|
|
368
|
+
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
369
|
- **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
370
|
|
|
371
|
+
- **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.
|
|
372
|
+
- **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.
|
|
373
|
+
- **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`).
|
|
374
|
+
- **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.
|
|
375
|
+
- 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
376
|
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
377
|
|
|
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
|
|
378
|
+
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).
|
|
379
|
+
|
|
380
|
+
**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.
|
|
381
|
+
|
|
382
|
+
**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.
|
|
383
|
+
|
|
384
|
+
**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`.
|
|
385
|
+
|
|
386
|
+
**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.
|
|
387
|
+
|
|
388
|
+
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:
|
|
389
|
+
|
|
390
|
+
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'`.
|
|
391
|
+
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.
|
|
392
|
+
|
|
393
|
+
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
394
|
|
|
340
395
|
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
396
|
|
|
@@ -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
|
-
"""
|
|
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:
|