Your agent keeps rebuilding work you already did. Heimdall makes it stop.
Heimdall gives AI coding agents persistent memory across every repository and project you work on so the question "did I already solve this in another project?" gets answered by one verified search instead of twenty minutes of grep, find, and ls loops.
The problem it solves
1. Memory that doesn't live in one repository. Every other memory tool is per-project. But your work isn't: the optimized functions you built in one project could be useful somewhere else. Heimdall indexes everything you touch into one semantic graph, so knowledge follows you across repositories, languages, and months.
2. Orientation time, cut to seconds. A fresh agent session burns dozens of bash commands just figuring out the lay of the land — ls, grep, re-reading files it read last week. Heimdall injects the relevant prior work into the session's first prompt and backs a single kb_search call: ranked, scoped, verified. Fewer commands, fewer tokens, faster first useful action.
3. Zero token spend. Memory maintenance is a local daemon: file watching, tree-sitter AST parsing, sqlite. Indexing a file costs CPU only — never an LLM call. Retrieval is hybrid ranked search (lexical + semantic + graph walk) over locally-computed embeddings. Your context window stays for your actual work. You can also use your GPU for up to 3.4x speeds.
4. Retrieval you can act on. Semantic memory tools return plausible matches, but Heimdall also verifies it at runtime.
STRONG— path exists on disk, strong lexical coverage, and the file's actual content answers the query (content-aware scoring)WEAK— semantic match only; plausible but unverifiedREBUILT— file moved; Heimdall found it and re-anchored automaticallySTALE/REMOVED— dead path, logged and pruned so it stops ranking
An agent acting on a dead path is worse than no answer. Ranked retrieval is trustworthy enough to act on. Gives the real picture, instead of the best guess.
How a session changes
Without Heimdall:
$ grep -r "portfolio optimization" . # wrong repo, 40s
$ find ~/work -name "*.py" | xargs grep -l optimizer # 2 min
$ ls ~/work/... ; cat notes.md ; ... # 15 commands later
With Heimdall:
$ kb_search "portfolio optimization jam optimizer"
1. [STRONG] portfolio optimizer — ~/work/quant-bot/src — EV-optimizer entry point
2. [STRONG] excel report builder — ~/work/reports/excel — 276-session tracking table
One call. Verified paths. Straight to work.
How it compares
Architecture-level comparison of shipped defaults — not benchmark claims. "LLM extraction" etc. describe each project's default pipeline as documented; self-hosted or configured-differently deployments vary.
| Heimdall | mem0 | Zep (Graphiti) | Letta (MemGPT) | LangMem / LangChain Memory | Vector-DB RAG | Claude Memory | cAST / grep | |
|---|---|---|---|---|---|---|---|---|
| Scope | All your repos, one graph | per-app/per-user memories | per-user/session graph | per-agent | per-app/thread | per-corpus/index | per-conversation/account | per-repo |
| Runs on CPU only | yes | LLM+embeddings in the loop | LLM extraction for entities/edges | LLM-in-the-loop memory management | LLM extraction + embeddings | embeddings (GPU-friendly) | cloud | yes |
| Token cost of indexing | zero (tree-sitter + local embeddings) | LLM extraction per memory op | LLM calls per episode | LLM calls throughout | LLM extraction per write | embedding tokens only | LLM summarization | zero but manual |
| Trust verdicts on results | STRONG / WEAK / REBUILT / STALE | none | none | none | none | similarity score only | none | none |
| Self-healing (moved files re-anchored) | yes | no | no | no | no | no (stale chunks rank) | no | no |
| Convergent state (idempotent re-index) | yes (level-triggered reconciler) | append-oriented | event-sourced episodes | conversation-scoped | append-oriented | re-ingest to update | opaque | n/a |
| Reads private source locally | yes, never leaves disk | sent to extraction LLM | sent to extraction LLM | stays local w/ local models | sent to extraction LLM | local if self-hosted | cloud | yes |
| Harness integrations | pi, Claude Code, Codex, Cursor, Windsurf | SDK/API | SDK/API + Graphiti | SDK/API | LangChain-native | DIY per stack | Claude products | editor plugins |
Where Heimdall beats mem0 and common RAG
By design, not by benchmark — these follow from the architecture:
Zero-LLM indexing instead of extraction pipelines. mem0, Zep, Letta, and LangMem all use LLMs to write facts: every remembered fact costs extraction tokens, adds latency, and means your code/notes are processed by a cloud provider unless you wire your own. Heimdall's ingest is tree-sitter plus local CPU embeddings. It cannot leak data, nor does it cost anything.
Verified hits vs plausible hits. RAG returns nearest neighbors with a similarity score; nothing checks that the chunk still exists, let alone that it answers the question. Heimdall re-verifies every result against the live filesystem at query time (path exists? content still matches?) and labels it STRONG/WEAK/REBUILT/STALE. Agents can act on STRONG without a confirmation round-trip.
Self-healing vs stale corpora. In vector-RAG, a moved file leaves orphaned chunks ranking forever until someone re-runs ingestion. Heimdall's level-triggered reconciler converges: moved files re-anchor automatically (REBUILT), deletions retract exactly their own nodes, and re-indexing twice is identical to once.
Cross-repo scope without a corpus pipeline. Classic RAG needs you to define, chunk, and refresh a corpus per app. Heimdall watches working trees continuously — new repos join the graph on their own, and personal context (prompt logs, notes, now even email via
heimdall ingest-email) lands in the same graph your code lives in.What they win back. Fair's fair: mem0/Zep/Letta excel at conversational fact curation across chat products, multi-user serving, and hosted APIs; LLM extraction summarizes messy prose better than regexes. Heimdall is making the opposite bet, that a single developer's machine-wide workspace where the unit of memory is verified file-level knowledge, not chat utterances.
Heimdall is the only one built for the real indie developer workflow: many repos, many months, one agent session at a time, on hardware you already own. Best for people with tons of side projects.
FAQ
Does my code leave my machine? No. Indexing is tree-sitter parsing + local embeddings on CPU. Search runs against your local daemon. Nothing phones home.
Do I need a GPU? No. The embedding model (bge-m3) runs on Apple Silicon / any modern CPU.
How is this different from grep? Grep finds strings you already know exist. Heimdall answers "have I solved anything like this before?" across every project you've touched, ranked and verified against what's actually on disk right now.
What if a file moves or gets deleted? The reconciler notices on its next pass. Moved files are re-homed automatically (REBUILT verdict); deletions retract exactly their own nodes. A stale path never ranks again.
Does it work with my agent? One command wires it into pi, Claude Code, Codex, Cursor, or Windsurf. Anything that can run a CLI can use search/insert directly.
Is it production-ready? It runs daily on this author's machine across ~12,800 live nodes with a 166-test suite guarding the concurrency invariants. v0.2.0. LongMemEval benchmark harness is in bench/ (in progress).
Roadmap
- v0.1 — packaged CLI, five harness adapters, trust verdicts
- v0.2 — single-writer reconciler, depth-ladder indexing (symbols + call edges)
- LongMemEval-S score ≥ 0.90 published from
bench/(baseline S 0.740 reproduced) - Linux daemon packaging
- MCP server mode
Quickstart
npm i -g @arihantdeva/heimdall
heimdall init --harness claude-code # or pi | codex | cursor | windsurf | all
That's it for install + harness wiring (init, insert work immediately).
Ranked search and doctor need the graft backend — one extra step:
# build graft from source, put the binary on PATH, then:
cp "$(npm root -g)/@arihantdeva/heimdall/config/heimdall.yaml.example" ~/.graft/config.yaml
heimdall doctor # should print HEALTHY
heimdall search "excel tracker portfolio optimization"
On macOS the backend runs as the launchd job com.graft.daemon (template: launchd/com.heimdall.backend.plist.example).
Alternative backend: Mnemosyne
Ranked search also supports mnemosyne-oss (SQLite-backed agent memory) as the retrieval backend. Graft remains the zero-config default; select mnemosyne per-invocation or persistently:
pip install mnemosyne-memory # or: uv pip install mnemosyne-memory
HEIMDALL_BACKEND=mnemosyne heimdall search "preferences" # one-off
# persistent:
python3 -c 'import json,pathlib; p=pathlib.Path.home()/".heimdall/config.json"; c=json.loads(p.read_text()) if p.exists() else {}; c["backend"]="mnemosyne"; p.write_text(json.dumps(c,indent=2))'
heimdall search "preferences"
Resolution order: $HEIMDALL_BACKEND > ~/.heimdall/config.json backend key > graft. Pin a non-PATH binary with MNEMOSYNE=/path/to/mnemosyne. Mnemosyne results flow through the same ranked/verified output as graft hits.
Demo

Real session output (abridged):
$ kb_search "portfolio optimization jam optimizer"
== retrieve (hybrid ranked): portfolio optimization jam optimizer
1. [STRONG] cov83% portfolio optimizer — ~/work/quant-bot/src
EV-optimizer entry point, edited 2026-08-14
2. [WEAK] excel report builder — ~/work/reports/excel
Every hit carries a trust verdict computed against the live filesystem — not a cached embedding score.
Design history
v0.1.0 hooks inferred graph mutations by regex-parsing bash commands and writing the graph from every hook process. It collapsed: writes it didn't recognize were invisible, concurrent hook processes raced delete+insert, and a misparse wrote wrong data as fact. v0.2.0 replaced all of it with a single-writer, level-triggered reconciler: nothing ever tells the graph what changed, only that a path might have. The reconciler reads the file from disk and makes the graph match.
The trust verdict layer came from the same lesson one level up: even a perfect graph lies if its anchors rot (directories get reorganized aggressively). So search results are re-verified against the filesystem at query time, with self-healing (rehome) for moved files.
Architecture / how it works
┌─────────────────────────── harness integrations ───────────────────────────┐
│ Pi: extensions/kb-*.ts Claude Code: PostToolUse hook │
│ (kb-tools, kb-autosync, kb-orient, kb-search-guard) Codex/Cursor/Windsurf │
└─���─────────────┬──────────────────────────────┬─────────────────────────────┘
│ only ever appends hints │
▼ ▼
~/.heimdall/hints.jsonl heimdall CLI (bin/heimdall.js)
│ init / insert / hint / verify / depth
▼ │
┌───────────────────────── THE SINGLE WRITER (holds O_EXCL lock) ───┘
│ bin/heimdall-reconciler.mjs (daemon: fs watch + hint ingest + drain loop + audit timer)
│ │ drain() reads hint queue → reconcilePath() per path
│ ▼
│ bin/lib/reconcile.mjs — level-triggered convergence
│ reads file from disk → desiredState() → compares hash/depth
│ → sink projection (graft insert/delete) BEFORE journal commit
│ → journal.commit() in one transaction, generation-guarded (ABA)
│ ▼
│ bin/lib/journal.mjs — AUTHORITATIVE index (~/.heimdall/journal.db, node:sqlite, WAL)
│ paths (hash/size/mtime/depth/generation/state) · owned_nodes · owned_edges
│ pending_edges (order-independent cross-file edges) · queue (dedup)
│ ▼
└──────────► bin/lib/sink.mjs — projection target (pluggable backend)
GraftSink (graft CLI) | MemorySink (tests, --dry-run)
▼
~/.graft daemon (vendor/graft, built from source) — sqlite + bge-m3 embeddings + graph edges
SEARCH PATH (read side, no lock):
kb-search.sh → graft retrieve (hybrid ranked) → kb_search_verify.py (verdicts + stale heal)
→ graft explore (graph walk of related work) → printed, ranked, verified
Core invariants (each has a test):
- Level-triggered, not event-driven. Hooks/watchers/scripts can only say "look at this path". They are never believed about what happened. A missed, duplicated, or flatly wrong hint costs one
stat— nothing more. This is what makes 40 concurrent agents editing one file harmless: they produce N cheap appends that collapse to one queue row (primary-key dedup). - Single writer by construction. Every graph/journal mutation passes through
Lock— anO_EXCLcreate with stale-PID liveness reclamation (Node has no portable flock). Two writers cannot exist, so the old delete-then-insert race (file briefly vanished from the graph) is unrepresentable. The daemon holds the lock for its lifetime; one-shotheimdall reconciletakes it too and serializes against the daemon. - Exact ownership. Every node and edge belongs to exactly one path. A commit deletes and re-inserts all of that path's rows in one transaction — a deleted symbol cannot survive as a ghost, and one path's retraction never touches another's rows.
- Content hash is the oracle. Desired state = pure function of (bytes, depth). Reconciling twice is byte-identical to reconciling once. The
generationcounter is the ABA guard: a commit whose generation moved since the reconcile started is rejected (actionstale) and the path re-queued, so a late reconcile can never resurrect a node a newer one deleted. - Order independence. A cross-file edge to a not-yet-indexed symbol parks as a pending edge, owned by the source file, and resolves in both directions once the target file reconciles. The final graph does not depend on which file was reconciled first.
- Audit as backstop.
heimdall verifycompares the journal against the filesystem (stat-only: size/mtime, plus a depth-upgrade check) and exits 1 on drift;--deepre-hashes everything and catches even a same-size/same-mtime rewrite. This is the accuracy claim as a CI-able command. The daemon runs the same audit on a timer (default 15 min) and re-queues drift. - Journal before sink, sink before commit. Sink writes happen BEFORE the journal commit: a crash in between leaves the path dirty, so it is redone — safe because reconcile is idempotent. The reverse order could mark a path clean that was never projected, which is the one failure the audit cannot detect later.
- The honest guarantee is bounded-staleness convergence, not instantaneous correctness: between an edit and the next reconcile pass the graph is behind. It is never wrong in a way that survives a pass.
Depth levels
Nodes are indexed at a depth. The default and the recommendation is maximum — the graph knows not just that a file exists but which functions and classes live in it, at which lines, and what calls what.
| Depth | Node knows |
|---|---|
path (L0) |
the file exists |
file (L1) |
+ language, size, description |
symbol (L2) |
+ one node per top-level definition, with file:line and signature |
graph (L3) |
+ imports / calls / inherits / uses edges, intra- and cross-file |
max resolves to whatever the machine can actually do: L2/L3 need tree-sitter, so a box without it degrades to L1 rather than failing — and the next audit automatically re-indexes at the deeper level once tree-sitter appears (a depth upgrade shows up as drift). Asking for a depth above the machine's capability is honored as far as possible and reported as clamped:
heimdall depth src/server.ts # requested vs effective depth for one path
Extraction is tree-sitter AST parsing via a Python bridge, not an LLM call: depth costs CPU, never tokens. The bridge (bin/lib/heimdall_extract.py) deliberately calls graphify's per-language extractors directly rather than graphify.extract(), because the latter writes a graphify-out/cache/ directory next to the files it reads (Heimdall indexes the whole home tree) and adds a second cache with its own invalidation rules that could disagree with reality.
Components (map)
| Component | File | Job |
|---|---|---|
| CLI | bin/heimdall.js + bin/lib/cli-main.mjs |
init / search / insert / doctor / daemon / reconcile / verify / depth / hint — thin wrappers over the bin/ scripts, never a rewrite |
| Reconciler daemon | bin/heimdall-reconciler.mjs |
the single writer: recursive fs watch + hint ingest + drain loop + audit timer, holds the lock for its lifetime |
| Reconcile | bin/lib/reconcile.mjs |
level-triggered convergence: read disk, make graph match; audit() = drift detector |
| Journal | bin/lib/journal.mjs |
authoritative index: ownership, hashes, generations, dedup queue, pending edges |
| Extraction | bin/lib/extract.mjs + bin/lib/heimdall_extract.py |
desired state per file: hash + L0-L3 nodes/edges, path-namespaced node ids |
| Depth ladder | bin/lib/depth.mjs |
levels, capability probe, per-root overrides, config |
| Single-writer lock | bin/lib/lock.mjs |
O_EXCL lock every graph mutation passes through |
| Hints | bin/lib/hints.mjs |
the one channel a non-writer may use (append-only, atomic, torn-line tolerant) |
| Sink | bin/lib/sink.mjs |
projection targets: GraftSink (CLI) and MemorySink (tests/dry-run) |
| Ranked search | bin/kb-search.sh |
top-k hybrid ranked + graph walk, --scope filter |
| Trust verification | bin/kb_search_verify.py |
content-aware STRONG/WEAK/STALE/REBUILT/REMOVED verdicts; stale self-heal |
| Stale pruning | bin/kb-stale-scan.py, bin/kb-rehome.sh |
full-graph sweep: deterministic rehome or log+delete |
| Health & telemetry | bin/kb-health.sh, bin/telemetry.sh |
daemon health, index freshness, usage stats (kb_* calls/24h, hit rate, est. time saved) |
| Bootstrap | bin/sync-edits.sh, bin/seed-graft.sh |
replay session edit logs → hints; seed inventory TSV into Graft |
| Full rebuild | bin/kb-rebuild.sh |
backup → wipe → parallel restore → re-seed → prune → verify |
| Pi extension: tools | extensions/kb-tools.ts |
exposes kb_search / kb_insert / kb_sync as agent tools |
| Pi extension: autosync | extensions/kb-autosync.ts |
hook that appends path hints — never writes the graph |
| Pi extension: orient | extensions/kb-orient.ts |
injects prior-work hits into the first user prompt of a session (2.5s cap, silent degrade) |
| Pi extension: guard | extensions/kb-search-guard.ts + extensions/lib/kb-guard-core.mjs |
warns after 3 consecutive grep-style actions without kb_search; agent can self-suspend via the kb_guard_pause tool for 1–20 turns (enforcement resumes clean-slate on expiry) |
| Email ingestion | bin/lib/ingest-email.mjs |
mailbox → graft-style cards (read-only via cli-email list/show), idempotent; retrieval rides the semantic layer |
| Backends | vendor/graft/ (Apache 2.0), vendor/graphify/ (MIT) |
Graft = semantic-memory daemon; graphify = code-graph extractors |
Backends are pluggable. Any store speaking the graft CLI contract works; Graft is the vendored reference. See docs/heimdall_compare.dot/png for the graphify vs Graft vs Heimdall positioning (graphify answers codebase questions, Graft persists notes/facts across projects, Heimdall ties them together with trust verdicts and self-healing).
Harness integration
One command wires the full enforcement stack into any supported harness: memory rules in the agent's instruction file, an MCP server (kb_search / kb_insert / kb_sync), and a guard hook that warns when the agent falls back to ls/grep/find chains instead of searching memory:
heimdall init --harness claude-code # or codex, cursor, pi, opencode, gemini-cli, deepseek
heimdall init --detect # list harnesses found on this machine
heimdall mcp # raw stdio MCP server (for anything else)
HEIMDALL_NO_AUTOINIT=1 npm i -g @arihantdeva/heimdall # skip auto-setup
npm i -g runs a best-effort postinstall that auto-wires every detected harness (never fails the install; opt out with HEIMDALL_NO_AUTOINIT=1).
| Harness | Command | Rules file | MCP | Guard hook |
|---|---|---|---|---|
| Claude Code | init --harness claude-code |
~/.claude/CLAUDE.md |
✅ settings.json |
✅ PostToolUse hook |
| Codex CLI | init --harness codex |
~/AGENTS.md |
✅ config.toml |
rules only |
| Cursor | init --harness cursor |
.cursor/rules/heimdall.mdc |
✅ mcp.json |
rules only |
| Pi | init --harness pi |
~/.pi/agent/AGENTS.md |
native tools | ✅ kb-search-guard extension |
| OpenCode | init --harness opencode |
AGENTS.md plugin dir | ✅ opencode.json |
✅ plugin hook |
| Gemini CLI | init --harness gemini-cli |
~/.gemini/GEMINI.md |
✅ settings.json |
rules only |
| DeepSeek (experimental) | init --harness deepseek |
~/.deepseek/AGENTS.md |
✅ settings.json |
✅ PostToolUse hook |
| Anything else | heimdall mcp |
your rules | ✅ stdio server | — |
Every adapter merges into existing config files (never clobbers your keys), is idempotent on re-run, and embeds the same canonical rule block — no per-harness rule drift. The DeepSeek adapter is experimental: DeepSeek's published harness config format may differ; verify paths after running init.
Usage
heimdall search "excel tracker portfolio optimization" # ranked + verified knowledge search
heimdall insert --title "portfolio optimizer" \
--body "~/work/quant-bot/src — EV-optimizer entry point" \
--keywords portfolio,optimize # record reusable work
heimdall init --harness claude-code # wire into your harness (idempotent)
heimdall doctor # daemon + index health
Self-healing surface:
heimdall daemon # the single writer: watch, reconcile, audit on a timer
heimdall reconcile # converge now (one-shot; takes the same lock)
heimdall reconcile --all # deep audit + repair everything
heimdall verify --deep # read-only drift report; exit 1 if any. CI-safe
heimdall hint PATH ... # mark paths dirty — no lock needed, any process
heimdall hint --stdin # harness hooks hand tool-call JSON on stdin
Email ingestion (CPU-only, read-only)
Index your own mailbox into the knowledge base. Reads mail exclusively
through the local cli-email binary's read-only
subcommands (list, show — never send/mark/move); renders one markdown
card per message; retrieval rides the existing semantic layer (CPU bge-m3,
no GPU, no cloud calls).
heimdall ingest-email --accounts a1,a2 --limit 50 # cards → ~/Repos/email-archive/graft/mail/<account>/<uid>.md
~/.heimdall/venv/bin/python3 bin/embed-index.py build # embed new cards (CPU-only)
heimdall search "subject or sender words" # emails now rank like any other knowledge
Re-runs are idempotent (byte-compare per card — unchanged mail is not
rewritten). Point --root at any directory under ~/Repos to change where
the card tree lives.
Testing
npm test # full suite (166 tests)
npm run typecheck # extensions typecheck
Suites:
tests/reconcile.test.mjs— the point. Invariant tests: 40 racing writers converge to one node set; separate OS processes hinting one file collapse to one queue row; reconciling twice is byte-identical; ABA generation guard rejects stale commits; deletion retracts exactly its own nodes; cross-file edges converge regardless of reconcile order; depth clamping; parse-failure degrades to L1; audit catches behind-our-back edits incl. same-size-same-mtime rewrites (--deep); git checkout picked up (the old command-regex path could not see it); lock admits exactly one writer + stale-PID reclamation; node-id path namespacing; garbage hints dropped. The L3 end-to-end test self-skips without tree-sitter.tests/guard.test.mjs— kb-search-guard contract (warn on 3rd consecutive grep action, reset on kb_search/kb_sync/graft, interleaved reads do NOT reset; agent-callablesuspend(N)/tickTurn()pause: silences all enforcement for N model turns, clamped 1–20, expiry restores clean-slate).tests/init.test.mjs+tests/adapters.test.mjs— CLI contract and per-harness config-writer smoke tests against temp HOMEs.tests/kb-verify.test.mjs— content-aware verdict contract viaselftest:node ids (no graft daemon needed): content mismatch downgrades STRONG, content match upgrades to STRONG, binary files degrade gracefully,extract_pathshome-anchor regression (the tilde-form bug).
The concurrency tests are the point: if the single-writer or idempotency properties ever break, those are the tests that go red.
Requirements
- Node ≥ 22.5 (the journal uses the built-in
node:sqlite),bash,python3 - macOS today (launchd daemon management); Linux works with a manual daemon
- tree-sitter-capable python for L2/L3 (
HEIMDALL_PYTHONenv, or~/.heimdall/venv/bin/python3, orpython3in PATH) - Runtime npm deps: zero —
typebox/typescript/@types/nodeare dev-only - Graft backend for
search/doctor(built fromvendor/graft/)
Operations
bash bin/kb-health.sh # heimdall doctor — daemon up, index fresh
bash bin/sync-edits.sh # one-shot: replay session edit logs → hints → reconcile
bash bin/telemetry.sh view # last snapshots: nodes/day, sync age, kb_* calls, hit rate
python3 bin/kb-stale-scan.py # full-graph stale sweep (rehome or log+delete)
bash bin/kb-rebuild.sh # last-resort full rebuild (backup → wipe → restore)
Status
v0.2.0 — adds the reconciler: a single-writer, level-triggered convergence loop with a content-hash oracle, exact per-path ownership, and a depth ladder that indexes symbols and call edges by line. Published on npm as @arihantdeva/heimdall.
Runtime state
~/.heimdall/— journal, lock, hint queue,config.json(harness selection), adapter install records.~/.graft/— backend config (config.yaml), sqlite profile DB,graftd.log,.last-sync(sync-edits watermark).
These are data, not build artifacts — never rm/mv over them blindly.
Development notes / gotchas
- The bridge must not use
graphify.extract()— it writes a cache dir next to indexed files and adds a second invalidation source. - The L3 test self-skips without tree-sitter — a green suite can silently mean "L1-only machine"; check
heimdall depth <file>output for the effective level.
Comments