Skip to content

Latest commit

 

History

1,698 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Lunaris

CI recall-ratchet crates.io docs.rs PyPI npm MSRV License

Sub-25 ms recall at 100,000 documents per scope — measured, not projected — with provable atomicity and a graph that's opt-in.

p50 19.2–22.4 ms / p99 23.4–24.4 ms, engine-side, single-shard Moon v0.8.5 on an Apple M4 Pro — full envelope and method. Beyond 100k the contract is unvalidated; do not read "millions".

A production-grade agent-memory engine in Rust, with first-class Python and TypeScript SDKs and a zero-Rust MCP server for coding agents. Raw observations in; structured, bi-temporal facts out. Backed by Moon, the high-performance Redis-compatible substrate — and, as of 0.7.0, only Moon. The Postgres and SQLite backends were removed; see 0.6 → 0.7 if you are on one.

Lunaris layered architecture

Documentation: the full guide lives in the Lunaris Book, or run mdbook serve docs/book to read it locally. First time here? docs/POSITIONING.md is the one-page pitch + honest "use a different tool when…" criteria. How it works: docs/ARCHITECTURE.md — the layered design and the Moon advantage map, every claim proof-anchored.

Try it without installing anything

No database, no container, no account, no config file. lunaris try starts a private in-process Moon on a loopback port, writes six sample memories, asks the store a question and shuts down when it exits.

$ git clone https://github.com/pilotspace/lunaris && cd lunaris
$ cargo build --release -p lunaris-cli --features embedded-moon
$ ./target/release/lunaris try
  ✓ store          embedded Moon on 127.0.0.1:52350
  ✓ corpus         6 sample memories

  ? why did we pick Moon instead of Postgres

 1. [0.914] sample/decisions  01M0H9HYYMTDZB9VJK4S6Y8CFS
    We chose Moon over Postgres for the memory substrate because recall has to
    stay under 25 ms at a hundred thousand documents per scope, and Moon answers
    vector search, BM25 and graph traversal in one round trip instead of three.

recalled 5 of 6 memories in 1 ms

It refuses to touch a store it did not start — every LUNARIS_* store variable in your environment is ignored, and ports 6379/6380/6381/6399 are rejected outright. First run downloads a 253 MB embedder (once per machine); after that it is a few seconds. This needs a clonelunaris-cli is not published to crates.io and has no prebuilt release binary yet. Full walkthrough →

Pick your path

You are… Do this Time
Giving your AI agent memory (Claude Code, Codex) Run the agent installer — MCP + hooks + Moon, no Rust toolchain needed 2 min
Building an app in Python / TypeScript / Rust Install an SDK 5 min
Just looking — want to see it recall something lunaris try — needs a clone and a cargo build 3 min
Evaluating against Mem0 / Zep / Cognee Read POSITIONING.md, then the migration doc for your tool 10 min

1. Give your AI agent memory (MCP)

The MCP server gives any MCP-capable agent 20 memory tools — nine durable-memory tools (memory.ingest, memory.recall, memory.forget, memory.list_scopes, memory.record_decision, memory.record_edit, memory.feedback, memory.status, memory.remember), four working-memory scratchpad tools (memory.scratchpad_write, memory.scratchpad_read, memory.scratchpad_grep, memory.scratchpad_consolidate), and five curation tools that are the reason to pick Lunaris over a vector store — memory.verify_agenda (what the store thinks may be stale), memory.resolve (retire a memory that is superseded), memory.dream_agenda (clusters of raw episodes ripe for distillation) and memory.distill (write the distilled prose back durably) and memory.profile (render what the scope actually knows, as a readable page), and two retention tools (memory.retention to set how long this scope keeps memories, memory.retention_enforce to run a pass — previewing by default, and never on a timer). The roster is pinned by crates/lunaris-mcp/tests/server_boot.rs::server_boots_and_lists_all_tools, which drives the real binary through tools/list.

Recommended: one script, both agents, Moon included

git clone https://github.com/pilotspace/lunaris && cd lunaris
scripts/setup-lunaris-agents.py --agent both --runner npx   # or: uvx | local

This is the only install path that ends with a working store. It installs Moon if none is found, starts it on moon://127.0.0.1:6381, probes the endpoint with FT._LIST to prove it is really Moon and not some other Redis-speaking service on that port, writes the MCP server entry for Claude Code and Codex, and installs the lifecycle hooks that capture and inject context automatically. Preview every file it would touch with --dry-run; check an existing install with --verify.

The FT._LIST probe is not ceremony. A plain PING is answered happily by any Redis-compatible process, so a proxy or cache sitting on the port you named accepts every write and silently swallows your memory — which is exactly what happened on 2026-07-14. Setup refuses to wire a store that cannot prove it is Moon.

Without a clone: MCP only, bring your own Moon

The commands below wire the MCP server and nothing else — no hooks, and no store. LUNARIS_MCP_STORAGE is not optional and Lunaris will not guess a value, so you must already have a Moon listening at the URL you pass, or the server refuses to boot. Install one with scripts/install-moon.sh first. Both runners download a prebuilt native binary on first run — no Rust toolchain required (linux-x64/arm64, darwin-x64/arm64, win32-x64).

Claude Code — one command, either runner:

claude mcp add --transport stdio lunaris \
  -e LUNARIS_MCP_STORAGE=moon://127.0.0.1:6381 \
  -- npx -y @pilotspace/lunaris-mcp
# or
claude mcp add --transport stdio lunaris \
  -e LUNARIS_MCP_STORAGE=moon://127.0.0.1:6381 \
  -- uvx lunaris-mcp

Any MCP client — JSON config:

{
  "mcpServers": {
    "lunaris": {
      "command": "npx",
      "args": ["-y", "@pilotspace/lunaris-mcp"],
      "env": { "LUNARIS_MCP_STORAGE": "moon://127.0.0.1:6381" }
    }
  }
}

Without a package manager — build from source:

cargo install --git https://github.com/pilotspace/lunaris lunaris-mcp

lunaris-mcp is not on crates.io — plain cargo install lunaris-mcp will not work. It links lunaris-memory-service, which carries a vendor/ path dependency and is therefore publish = false; a crate cannot be published to crates.io while any of its dependencies are unpublished. The --git form above builds the same source (needs a Rust 1.94 toolchain, cmake, and a C++ compiler for llama.cpp). From 0.6.1 onward, prebuilt lunaris-mcp-<target>.tar.gz binaries are also attached to each GitHub release.

LUNARIS_MCP_STORAGE is required. Through 0.6.x an unset value opened a per-scope SQLite file; 0.7.0 deleted that backend, and the server now refuses to boot rather than guess a store — a mis-routed memory is harder to notice than a process that will not start. Point it at a Moon (moon://127.0.0.1:6381 — the port scripts/setup-lunaris-agents.py uses, and deliberately not 6380, which is an ai-proxy Redis on some boxes and answers RESP PING happily), started with --shards 1; install Moon with scripts/install-moon.sh (agent setup runs it for you when no Moon is found). A source build with --features embedded-moon auto-launches an in-process Moon — an opt-in for development, not the published-binary default. First ingest stages the embedder weights once (lazy GGUF download).

Full guides: docs/integration/claude-code.md · docs/integration/codex.md · docs/integration/hooks.md

Tell your AI about Lunaris

Paste this into your CLAUDE.md / AGENTS.md so your agent uses the memory deliberately:

## Memory (Lunaris MCP)
- When you learn something a future session would need and could not
  re-derive from the code, write it down with `memory.remember`. Four
  kinds: `decision` (a choice + its rationale), `fix` (what broke and what
  actually fixed it), `preference` (how this user wants to work),
  `constraint` (project state or an invariant). Always fill `why` when there
  is one. Do this on your own judgement, as it happens — nothing else in the
  system distils raw activity into knowledge.
- `memory.ingest` for raw observations; `memory.record_decision` /
  `memory.record_edit` remain for structured decision and edit records.
- Before answering questions about prior work, query `memory.recall`.
- Use `memory.scratchpad_write`/`scratchpad_read`/`scratchpad_grep` for
  transient working notes within a task (drafts, plans, in-progress state);
  promote the durable ones with `memory.scratchpad_consolidate`.
- Memory is partitioned by scope — never mix scopes; list with
  `memory.list_scopes`. Use `memory.forget` when asked to delete — it
  previews by default; show the match count, then re-issue with
  `dry_run: false` to actually delete.
- Check backend health with `memory.status` if recall returns nothing.
- `memory.profile` renders what this scope actually knows as a readable page —
  use it when starting on unfamiliar work, or to check anything is being
  captured at all.

2. Build with an SDK

# Python (3.11+)
pip install lunaris

# TypeScript (Node 20+)
npm install @pilotspace/lunaris

# Rust — published as `lunaris-memory`; import as `lunaris`
cargo add lunaris-memory --rename lunaris

Python — ingest + recall in one file (from the SDK guide):

import asyncio, lunaris, ulid

async def main():
    handle = await lunaris.open("moon://127.0.0.1:6380")

    lsn = await handle.ingest({
        "id": str(ulid.ULID()), "scope": "_dev_", "source": "quickstart",
        "content": "Alice loves chocolate.", "metadata": {}, "t_ref": None,
        "bt": {"valid": [{"wall_ms": 0, "counter": 0, "node_id": 0}, None],
               "sys":   [{"wall_ms": 0, "counter": 0, "node_id": 0}, None]},
    })

    hits = await handle.recall().query("what does Alice like?").top(5).execute()
    print(lsn, [h["text"] for h in hits])

asyncio.run(main())

TypeScript — same shape (SDK guide):

import { open, RetrievalBuilder } from "@pilotspace/lunaris";

const handle = await open("moon://127.0.0.1:6380");
const lsn = await handle.ingest(episode);           // same episode shape as Python
const hits = await new RetrievalBuilder()
  .bind(handle)
  .query("what does Alice like?")
  .top(5)
  .execute();

Rust — the typed surface:

use lunaris::{EpisodeBuilder, Lunaris, Scope};

let lunaris = Lunaris::open("moon://127.0.0.1:6380").await?;
let scoped  = lunaris.scoped(Scope::new("acme.agent-1")?);

let lsn = scoped.ingest(EpisodeBuilder::new("user-msg", "Alice loves chocolate.")).await?;

moon://host:port is the only connection scheme — every retired spelling (postgres://, memory://, sqlite:///path) was removed in 0.7.0 and now returns an error naming the migration guide. Embedding and reranking run in-process via llama.cpp (granite-embedding-311m Q4_K_M + bge-reranker-v2-m3 Q5_K_M GGUF) — no embedding API, no network on the hot path; GPU offload is a build-time metal/cuda/vulkan feature. Air-gapped options: configuration reference. Memory budgets per build tier (Tier-0 no-inference → full cross-encoder): deployment tiers.

Runnable examples: examples/quickstart-py/ · examples/quickstart-ts/ · examples/quickstart-rs/ · examples/multi-agent-rs/

Why Lunaris

Three properties define what Lunaris IS. Every commit is reviewed against them; any feature that weakens any of the three is rejected.

Moat What it means Where enforced
Sub-25 ms p50 recall No LLM on the recall hot path. Measured at the 100k-documents-per-scope target corpus: p50 19.2–22.4 ms · p95 22.3–24.1 ms · p99 23.4–24.4 ms, engine-side (query embedding excluded), graph OFF, rerank OFF, k=30, single-shard Moon v0.8.5 on an Apple M4 Pro; 500 timed queries after 50 warmup, run-to-run p50 drift ± 3 ms (envelope + method, raw samples). scripts/bench/perf/recall_latency.sh all (local gate, ~10 min, live Moon)
Single atomic_write per ingest All-or-nothing commit across vector, KV, BM25, graph, audit, queue. Fan-out architectures (Mem0, Zep) can't make this guarantee. tests/ingest_pipeline.rs::single_atomic_write_call + CI grep gate
Bi-temporal MVCC + HLC BiTemporal { valid, sys } on every primitive, on every backend — forget and supersession close intervals instead of destroying rows. As-of reads are search-side and graph-side (FT.SEARCH AS_OF, GRAPH.QUERY VALID_AT); historical KV reads are not available on Moon and read_as_of refuses rather than answering with today's data — the Postgres/SQLite version chains that served them were removed in 0.7.0 (limits). Required field on Episode, Chunk, Entity, Fact, Relation, Community

Architecture at a glance

Surface (SDKs / HTTP / MCP / hooks) → engine pipelines (ingest, retrieval DSL, opt-in graph + consolidation + verification) → one storage trait → one backend, Moon (the trait is still the seam that kept Postgres and SQLite honest until 0.7.0 removed them). The retrieval DSL fuses vector, keyword (BM25), and graph lanes with RRF in a single typed expression — and on Moon, the fusion and the time-travel cut execute inside the substrate.

Full tour with diagrams: docs/ARCHITECTURE.md and the book's Architecture at a Glance.

Why Moon — the substrate advantage

The conventional agent-memory stack is three databases and a broker: a vector DB, a graph DB, a relational store, and a queue — four failure domains with no transaction spanning them. Moon collapses all four lanes into one process, so each Lunaris feature maps onto something the substrate does natively instead of a layer bolted on top:

What Moon does natively, feature by feature

  • Atomic memoryTXN.BEGIN / TXN.COMMIT commit every lane at once; no half-written memory.
  • Hybrid recallFT.SEARCH + native RRF fuse vector + keyword in one round trip.
  • Time-travelFT.SEARCH AS_OF / GRAPH.QUERY VALID_AT make "what did the agent know at T?" a query, not a rebuild (search + graph lanes only; a historical KV read has no version chain to walk on Moon, so read_as_of refuses explicitly rather than answering with today's data).
  • Opt-in graph — per-scope GRAPH.QUERY (Cypher): relationships without running Neo4j.
  • GDPR forgetFT.INVALIDATE_RANGE erases a whole time range, no scan-and-delete loop.
  • Background work — a native queue + pub/sub run consolidation without an external broker.

Same job as Mem0, Zep, and Cognee — different guarantees. The single substrate is why several rows below are a ✓ for Lunaris where the fan-out tools manage only a partial or an ✗:

Lunaris vs Mem0 / Zep / Cognee, feature by feature

Every cell is sourced from the comparison table in Why Lunaris; the full advantage map — each claim anchored to a code path — lives in docs/ARCHITECTURE.md. One honest caveat: plain key-value point reads aren't natively temporal and index schemas are fixed at creation, so the architecture page lists every limit beside every win.

Recall quality — LongMemEval-S

Lunaris currently publishes no LongMemEval headline. The former 85.4% (427/500) J-score and its 98.2% / 93.0% retrieval companions were retracted on 2026-08-21: the driver that produced them (tmp/full500.sh) was never in git, no raw 500-question artifact set survives, and the surviving drivers contradict the published methodology (50 questions per process and a gpt-4o judge, against a page claiming one-process-per-question with a MiniMax judge). Full reasoning: docs/benchmarks/v0.7-longmemeval-jscore-validation.md.

We would rather ship an empty row than an unreproducible one. What exists instead, today:

  • The harness is in the repositoryscripts/bench/lme/, one process per question, config-fingerprinted, resume-safe, with its own reserved-port guards. Ported into version control in 0.6.2 precisely so that a number can never again outlive its runner.
  • A judge-free retrieval ratchet runs in CI on every recall-affecting push to main (LongMemEval-S evidence-recall any-gold, deterministic, no API key), gated against a checked-in baseline that records the exact retrieval config it was measured under. Its scope, its current fail floor and the work to give it teeth are documented — including the parts that do not yet work — in scripts/bench/lme/baselines/README.md.
  • A full N=125 A/B re-run on the committed harness is pending, blocked on a provider key. When it lands, its raw per-question artifacts get committed under docs/benchmarks/lme-raw/ in the same shape as the latency envelope's — because a published number without a committed raw artifact is not publishable here.

Competitor LongMemEval figures previously listed in this table (Zep, Mem0) were removed at the same time: they carried no citation, and one of them was a LoCoMo score mislabelled as LongMemEval. See Competitor figures.

Persona tracking — PersonaMem (32k)

Full 32k split (589 questions, 37 shared contexts), production hybrid recall path on the quality operating point (rerank ON — not the shipped default; see operating points), exact letter-match scoring (no LLM judge), zero errors:

Configuration Accuracy
Lunaris + claude-sonnet-5 reader (single reader, quality path) 75.0% (442/589)
No-memory floor (same reader, options only) 41.9% (247/589)
TencentDB-Agent-Memory (published; split/reader unstated) 76% / 48%
Two-reader oracle cascade — an upper bound, not a system result¹ 81.8% (482/589)

Memory lift: +33.1 points with the identical reader — larger than Tencent's published +28, from a lower floor. Fact-recall questions go from 2.3% without memory to 78.3% with it, single reader.

Two caveats we state rather than bury:

¹ 81.8% is an oracle bound, not a measurement of Lunaris. claude-opus-5 re-answered only the questions the Sonnet arm missed, and gold labels decided which questions those were. A deployable cascade would need a gold-free routing rule. The headline is the clean single-reader 75.0%.

² One of the seven categories does not measure memory. In suggest_new_ideas (93 of the 589 questions) the gold answer is essentially always the shortest option — a classifier that reads nothing and picks the shortest candidate scores 98.9% there against 0–15.5% in every other category. Its published "memory net-harms this category" reading is an artifact of that, not a finding, and we deliberately do not optimise against it. The 75.0% is understated by the presence of 93 questions that cannot be won on merit. Root cause and the regression test that pins it: issue #141.

Full methodology, per-category table, caveats, and reproduction commands: scripts/bench/pm/RESULTS.md and the book write-up.

Multi-agent isolation

Every Lunaris operation is partitioned by Scope — a validated newtype enforced at compile time and at the storage boundary (per-scope Moon keyspaces + per-scope indices). Cross-scope reads are a type error. See RFC 0001.

let scope_a = Scope::new("acme.agent-1")?;
let scope_b = Scope::new("acme.agent-2")?;

// Same ULID, different scopes — two distinct rows. No leak.
lunaris.scoped(scope_a).ingest(builder.clone()).await?;
lunaris.scoped(scope_b).ingest(builder).await?;

Operating in production

External Moon is the supported deployment (the embedded server is dev/test-only):

Status

Current release: 0.7.0 (2026-08-18). Newest first; every row is a git tag. CHANGELOG.md is the authority — this table is a summary.

Release Date What landed
0.7.0 — Moon-only, and the GA cut 2026-08-18 Every storage backend except Moon deleted; one production_root recall plan across HTTP / SDK / MCP / hook; opt-in rerank stage; recall-ratchet CI gate (replacing the Eval Gauntlet, which had 20 startup failures and 0 completed runs); measured 100k-doc capacity envelope with committed raw artifacts; rehearsed upgrade/rollback
0.6.2 — operability 2026-08-15 Last release shipping Postgres + SQLite. Historical read_as_of / scan_range on Moon now fail loudly instead of quietly returning present-time data
0.6.0-rc.1 / rc.2 2026-07-15 / 07-17 llama.cpp-only cutover (candle deleted); adaptive chunking + RAPTOR tree retrieval; Moon v0.8.0 bump
0.5.0 — adapters + memory convergence 2026-06-16 LangGraph / CrewAI / Letta reference adapters, write-time dedup + cross-episode supersede, relicensed Apache-2.0
0.4.0 — MCP surface 2026-06-13 lunaris-mcp + the memory.scratchpad_* tools, RAPTOR tree retrieval, recall fan-out p50 12 → 6 ms, hybrid filter push-down
0.3.0 — proactive capture + packaging 2026-06-05 lunaris-hook lifecycle capture, MCP polish, npx / uvx distribution
0.2.1 — multi-agent partitioning 2026-05-12 The Scope newtype partition key
0.1.x 2026-04 → 05 First engine cut: bi-temporal store, retrieval DSL, single-atomic_write ingest

RELEASES.md carries the per-release gate evidence.

Contributing

CONTRIBUTING.md has the build + test recipe (including the MOON_TEST_BINARY that storage-backed tests need), what CI checks, and the grep-pinned invariants a PR must not break. Participation is governed by our Code of Conduct. Security issues go to SECURITY.md, never a public issue.

Coming from another agent-memory tool?

  • docs/MIGRATING-FROM-MEM0.md — code-side comparisons (ingest, recall, time-travel, forget), a 5-step incremental migration plan, honest "stay on Mem0 if…" criteria.
  • docs/MIGRATING-FROM-ZEP.md — Zep already has bi-temporal facts; the conversation is latency + substrate simplification.
  • docs/MIGRATING-FROM-COGNEE.md — pipeline-vs-DSL tradeoff: if your custom logic lives at ingest time, Cognee's Task model maps cleaner; at recall time, Lunaris's operator DSL is simpler.

For contributors

License

Licensed under the Apache License, Version 2.0. See LICENSE for the full text.

About

lunaris - Agent-memory engine in Rust

Resources

Code of conduct

Contributing

Security policy

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages