lichen-core
v0.1.1
Published
A local fact-mesh brain over your own notes: honest recall, reinforce/decay memory, zero dependencies. Grown, not trained.
Maintainers
Readme
lichen-core
A local fact-mesh brain over your own notes. You teach it facts — by pointing it at a folder of markdown — and it answers from those facts only, telling you honestly when it doesn't know. No account, no cloud, no API key, no cost. Everything lives in a JSON file on your disk that you can open and read.
Published by Zero under the MIT license. Zero npm dependencies — pure Node.js (CommonJS), Node 18 or newer.
Why honest recall matters
Every LLM chatbot answers every question — including the ones it knows nothing about. It fills gaps
with confident invention, and you can't tell which is which. lichen-core takes the opposite deal:
it retrieves from facts you taught it, scores how well your notes actually cover the question
(coverage: high / partial / low, plus the exact words no fact mentions), and when coverage is low
its standing instruction is to say "I don't have that in my notes yet — tell me and I'll remember
it." A memory that admits its gaps is one you can trust with the rest.
And the memory itself is alive: facts you actually use get stronger and decay slower; facts you never
touch fade and are pruned. Teach it a correction and the old fact is superseded, not silently
contradicted. Tell it whether an answer helped (/outcome) and usefulness — not just recency —
shapes what survives. Every mutation is written to a hash-chained audit journal you can replay.
Quickstart
# 1. Install Node.js 18+ (https://nodejs.org). That's the only requirement.
git clone https://github.com/thefinalmilkman/lichen-core && cd lichen-core # or unzip it anywhere
# 2. (Optional but recommended) Install Ollama (https://ollama.com) for real
# semantic embeddings + grounded answers:
ollama pull nomic-embed-text phi4-mini
# 3. Start the brain
node server.js # http://127.0.0.1:4174 — open it for the chat page
# 4. Teach it your notes (in a second terminal)
node grow.js ./my-notes # .md/.txt, walked recursively; --dry-run to preview firstThen ask it things:
# recall raw facts (no LLM call at all — works with Ollama down)
curl -s http://127.0.0.1:4174/recall -d '{"query":"oil change interval"}'
# ask (grounded answer via Ollama; without Ollama you get the facts + an honest "unavailable" note)
curl -s http://127.0.0.1:4174/ask -d '{"prompt":"what oil does my truck take?"}'
# teach a single fact directly
curl -s http://127.0.0.1:4174/learn -d '{"text":"The truck takes 5W-30 full synthetic, 6 quarts."}'
# tell it whether a fact helped (useful facts decay slower; harmful ones faster)
curl -s http://127.0.0.1:4174/outcome -d '{"factId":1,"good":true}'
# health + the audit trail of every memory mutation
curl -s http://127.0.0.1:4174/health
curl -s "http://127.0.0.1:4174/journal?tail=20"Windows note: curl.exe works the same; -d sends JSON with the default Content-Type the server
expects.
The HTTP API
| Route | Method | What it does |
|---|---|---|
| /health | GET | version, fact count, edge count, unread attention flags |
| /learn | POST | {facts:[{text,meta}]} or {text,meta}; replaceSrc re-teaches a note |
| /recall | POST | {query, k?} → top-k facts + coverage. No model call. |
| /ask | POST | {prompt} or OpenAI-style {messages} → grounded answer + sources + coverage |
| /outcome | POST | {factId|text, good:true|false} → reward/punish a fact |
| /journal | GET | ?tail=N → recent memory mutations (hash-chained audit log) |
| /attention | GET | flags raised via /notify (a small inbox for "worth a look" items) |
| /notify | POST | {title, detail?, level?, source?} → raise an attention flag |
| / | GET | a minimal chat page (works offline; optional local speech) |
The server binds 127.0.0.1 only — your notes never leave the machine.
Configuration (all env vars)
| Var | Default | Purpose |
|---|---|---|
| LICHEN_PORT | 4174 | server port |
| LICHEN_DATA | ./data | where mesh.json, graph.json, journal.jsonl, attention.jsonl live |
| OLLAMA_URL | http://127.0.0.1:11434 | Ollama base URL |
| LICHEN_MODEL | phi4-mini | procedure model used by /ask |
LICHEN_DATA means you can run several independent brains side by side:
LICHEN_PORT=4201 LICHEN_DATA=./brains/work node server.js
LICHEN_PORT=4202 LICHEN_DATA=./brains/home node server.jsWhen Ollama is down
Nothing breaks and nothing is faked. At startup the server probes Ollama and prints a loud
warning if it's unreachable. Embeddings then degrade to a built-in lexical embedder (hashed
bag-of-words): retrieval still works, but "similar" means shared words, not shared meaning.
/ask still responds — it returns the retrieved facts with an explicit
[procedure model ... unavailable] note instead of a synthesized answer. /recall, /learn,
/outcome, /journal are fully functional offline. Install Ollama and pull
nomic-embed-text + any small chat model to get the real thing.
How it works (short version)
- Facts live in a reinforce/decay mesh (
mesh.js): recall strengthens, disuse decays, a hybrid exponential→power-law forgetting curve decides what fades, and a floor prunes the dead. - Retrieval is hybrid (
retrieval.js): dense vector similarity and from-scratch BM25, fused by reciprocal rank fusion — so exact tokens (error codes, part numbers) are never smeared by vectors. - Ingest triage dedups by exact-text hash, reinforces restatements, and supersedes old facts only
when a cheap contradiction signal fires — near-duplicates that aren't contradictions stay both
live, journaled as
related. - Associations (
graph.js): facts co-recalled together grow Hebbian edges; frequently-used edges potentiate and decay at half rate. - Audit (
journal.js): every mutation is one JSONL line in a hash-chained log. Verify withnode journal.js --verify data/journal.jsonl.
Honest limits
- Brute-force retrieval. Ranking is an O(N) scan over all facts per query. That's instant up to tens of thousands of facts and fine past 100k on a desktop, but there is no ANN index — the ceiling is real.
- Whole-file JSON storage. Save/load is one big
mesh.json. Known behavior at scale: a brain grown to ~120,000 facts is a ~124 MB JSON file; loading takes seconds and saves block briefly. Fine for a personal notes brain; not a multi-tenant store. - Single-user by design. One mesh, no auth, localhost-only. It is your brain, not a service.
- A small procedure model is still a small model. With Ollama,
/asksynthesis can ramble on abstract questions and may occasionally leak a parametric fact despite instructions. The retrieved facts (from/recall) are the ground truth; the prose is a convenience. - Retrieval quality is capped by the embedder. Lexical fallback = keyword overlap. Nomic via Ollama = real semantics. Same code, honest difference.
Tests
npm test # = node test/run.js — fully offline, no Ollama, no servicesThree suites: test/verify.js (mesh behavior: grow/reinforce/decay/supersede/outcomes/journal —
deterministic, lexical embedder), test/verify-graph.js (associative edges, consolidation, journal
hash chain), and test/smoke.js (boots a real server on a scratch port with a temp data dir, grows a
fixture folder through grow.js, recalls, checks outcomes + journal, shuts down).
Files
server.js HTTP brain (routes above), binds 127.0.0.1
grow.js folder-of-notes ingestion -> POST /learn (the way your notes get in)
lichen.js the three-substrate model: procedure + voice + facts
mesh.js the living fact store (reinforce/decay, supersede, outcomes, pruning)
retrieval.js BM25 + reciprocal rank fusion
embed.js Ollama embeddings with a zero-dep lexical fallback
graph.js Hebbian association edges, spreading activation, sleep consolidation
journal.js hash-chained append-only audit log
notify.js attention inbox (a tiny flag queue for "worth a look")
distill.js optional sleep pass: turn unread attention flags into mesh facts
test/ offline test suites (npm test)Everything in data/ is yours. Delete it and the brain is gone; copy it and the brain moves.
Nothing else anywhere knows it exists.
Ecosystem
- openclaw-factmesh — makes this brain the memory backend for OpenClaw agents.
- The Local AI Field Manual — the field guide this project was built alongside: hardware tiers, Ollama tuning, honest retrieval. Pay-what-you-want; lichen-core stays free.
If lichen-core earns it, the storefront above is where a tip lives — USDT or ETH on Base, no accounts.
