File-first memory for AI agents: RecallFiles + wiki-links over plain markdown.
No database, no embedding model, no docker — pip install wikimem and it works.
pip install wikimem # the default — everything works out of the box pip install "wikimem[all]" # optional enhancements included, if you'd rather not choose
There are no modes. wikimem is one pipeline. Extras only unlock optional enhancements, they activate automatically, and they never conflict with each other — installing all of them changes nothing until you actually use them.
| Install | Adds | Use case |
|---|---|---|
wikimem |
nothing — zero dependencies | Always fully works: storage, BM25 retrieval (Chinese via char-bigrams), wiki-links, journal |
wikimem[zh] |
jieba | Sharper Chinese keyword recall than bigrams — picked up automatically once installed, nothing to configure |
wikimem[embed] |
httpx + numpy | Semantic recall (match by meaning, not wording) — only active when you pass an embedder; endpoint down → BM25 carries on |
wikimem[all] |
both of the above | The "don't make me think" option |
- Markdown files are the only source of truth. One RecallFile per topic under
wiki/(memory/wiki/preferences.md), one##heading per item; diary events live as per-day files underdiary/. Read them, edit them, diff them — your editor is the admin UI. - No unreadable truth on disk. Every derived artifact (indexes, vector caches) is deletable and rebuildable from the files. The BM25 index lives in memory, built at startup.
- Never block the conversation. Retrieval is synchronous, budgeted, and fail-open (0 LLM calls); memorization is async (≤ 1 LLM call by the host).
- What happened is always answerable. Every mutation appends one line to
journal.jsonl; retrieval can explain its scoring.
A wiki-link is an in-content reference — the [[...]] syntax you may know
from wikis and Obsidian — and in wikimem it always points at one item:
# preferences.md ## likes-the-sea 喜欢海边,提到过想去海边玩。[[daily_life:beach-trip-plan]] <!-- wikimem: owner=user:xnne | source=conv_20260710 | ts=2026年07月10日T03:00:00+00:00 --> # daily_life.md ## beach-trip-plan 计划夏天去海边旅行,看日出。
[[daily_life:beach-trip-plan]] is an address with two parts:
RecallFile (which file — daily_life.md) and item name (which ##
heading inside it). So the linked node is an item: a named, self-contained
entry of a few sentences — not a word, and not a whole file. When
retrieval hits likes-the-sea, it mechanically expands its links one hop and
injects the whole beach-trip-plan item alongside — no LLM call, no graph
database; the "graph" is just text, and expansion is an exact-name lookup.
Why links, when there's already search?
- Search finds similar wording; links encode related meaning. A coffee preference and a morning routine may share no words — no keyword (often not even embedding) match connects them. A link written at memorization time does.
- One unit everywhere. The link target is the same unit retrieval ranks and the token budget trims: an item. Finer-grained than Obsidian's file-sized notes, so expanding a link never dumps an entire document into the prompt.
- Readable and writable by everyone. The extraction LLM emits links in
the same single pass that writes the memory; you can add or fix them in any
text editor;
git diffshows them. - Zero infrastructure, fail-soft. This replaces a graph database (the design it supersedes ran Neo4j for exactly this). A dangling link — target renamed or deleted — is tolerated and reported, never a crash.
Pre-alpha, built milestone by milestone (design: XnneHangLab ADR-0001 — memory pipeline):
- M1 ✅ — storage layer: RecallFiles, item model + metadata,
wiki-link parsing,
journal.jsonl, atomic writes - M2 ✅ — retrieval: in-memory BM25 (char-bigram fallback,
[zh]extra for jieba), one-hop wiki-link expansion, token budget, explain - M3 ✅ — optional embedding fusion (
[embed]extra): content-hash vector cache (versioned.npy+ plain-text keys), memmap tiers with binary quantization above 10k items, pluggableVectorIndexport, silent BM25 fallback when the endpoint is down - M4 ✅ — CLI:
ls / show / grep / explain / graph - M5 ✅ — diary + time gate: append-only
diary/YYYY-MM-DD.mdevent files, a stdlib parser for relative time expressions, and one ranked list where diary and wiki items compete on the same scale - M6 — serve (HTTP + JSON thin shell): designed, deliberately not built yet
Past M5 the work is tracked per decision, not per milestone — see the ADR index, where every decision carries its own implementation status.
from wikimem import MemoryIndex, MemoryStore store = MemoryStore("memory/") store.add("preferences", "likes-the-sea", "喜欢海边,提到过想去海边玩。[[daily_life:beach-trip-plan]]", owner="user:xnne", source_conv="conv_20260710") store.add("daily_life", "beach-trip-plan", "计划夏天去海边旅行,看日出。") index = MemoryIndex(store) # in-memory BM25, rebuilds itself on store writes result = index.retrieve("想去海边玩", budget_tokens=800) for entry in result.items: # hits come ranked; each is followed by its one-hop wiki-link targets print(entry.source, entry.item.name, entry.score, entry.matched_terms)
Retrieval makes zero LLM calls and never persists the BM25 index — delete
nothing, lose nothing. Install wikimem[zh] for jieba-based Chinese
tokenization (default is character bigrams).
Optional semantic fusion (pip install wikimem[embed]) — BM25 is never
disabled: with an embedder, every query runs both signals and fuses them
(each min-max normalized — the same hybrid formula memU ADR-0007 converged
on). BM25 catches the wording, cosine catches the meaning:
from wikimem.vectors import HttpEmbedder embedder = HttpEmbedder("https://api.example.com/v1", "bge-m3", api_key="sk-...") index = MemoryIndex(store, embedder=embedder) result = index.retrieve("海滨度假") # finds 喜欢海边 even with zero shared words print(result.embedding_used) # False = endpoint was down, BM25 carried on
Vectors live in a content-hash cache next to your markdown (versioned
vectors-*.npy + readable vectors.keys.jsonl) — incrementally updated,
deletable anytime, never the source of truth. An unreachable embedding
endpoint silently degrades retrieval to BM25-only; it never raises.
The same store, from the shell — stdlib only, ships with the package:
wikimem -s memory/ ls # RecallFiles + item counts wikimem -s memory/ show preferences # print a RecallFile as stored wikimem -s memory/ grep 海边 # regex search, grep-style output wikimem -s memory/ explain "想去海边玩" # retrieval scoring breakdown wikimem -s memory/ graph --format mermaid # wiki-link graph (mermaid / json)
graph takes over the retired Neo4j semantic-layer visualization: the same
relation picture, parsed straight out of the markdown. Full reference:
https://wikimem.xnnehang.top/reference/cli.
uv sync uv run pytest
Apache-2.0. Extraction-prompt design borrows from memU (Apache-2.0) — see lab ADR-0002.