A local-first context graph engine. One Rust binary that turns a stream of text into a queryable graph of entities, relationships, and episodes — combining property-graph storage, HNSW vector search, and full-text search in a single embedded service. No database server, no separate vector store, no search cluster: everything runs in one process, on your machine, against files in your workspace.
Originally inspired by the knowledge-graph ideas in graphiti, then deliberately narrowed: instead of a general framework over pluggable backends, liminis-context-graph is a purpose-built engine with one storage layer, one wire protocol, and a local-first design from top to bottom.
- One embedded engine. LadybugDB (the community continuation of KuzuDB) provides the property graph, HNSW vector indices, and full-text search in a single embedded database — no server process, no network hop, data in ordinary files under your workspace.
- The write-ahead log is the source of truth — and it's just JSON. Every mutation is appended to plain JSONL files under
.lcg/wal/before it touches the database. The database is a derived index — delete it andknowledge_rebuild_from_walreconstructs the entire graph from the log. - One database, many graphs.
.lcg/wal/is a WAL root: eachgroup_idowns its own stream in.lcg/wal/<group_id>/, independently replayable and independently discardable. One process can hold several graphs at once without them bleeding into each other, and because a stream is just a directory of JSONL files it can be versioned, shipped, and replayed somewhere else. - Models stay out of process. Embedding and LLM inference are reached through narrow adapters over the
/v1/embeddingsand/v1/chat/completionswire shapes OpenAI's API speaks. Embedding runs fully local out of the box on macOS; extraction can run fully local too, or against the hosted Anthropic API. The embedder also supports Bearer-token auth (LCG_EMBEDDING_API_KEY), so a hosted endpoint speaking that same shape and accepting a Bearer token is reachable too — OpenAI's own/v1/embeddingsis the verified case; see Configuration: Embedder sidecar or the full Embedding Options capability matrix for what to run on your platform.
The result is a context graph you can treat like the rest of your local tooling: a single process, a directory of files, versionable with git, rebuildable from its own log.
┌─────────────────────────────────────────────────┐
text chunks │ liminis-context-graph (one process) │
──────────────────► │ │
JSON-RPC 2.0 │ extraction LLM ──► entities + relations │
over Unix socket │ (out-of-process) dedup + resolution │
│ │ │
search queries │ 1. append ┌────▼───────┐ .lcg/wal/ │
──────────────────► │ ────────► │ WAL (JSONL)│ <group_id>/ │
hybrid results │ └────┬───────┘ one stream │
◄────────────────── │ 2. apply │ per graph, │
│ │ source of │
│ │ truth │
│ ────────► ┌────▼───────┐ │
│ │ LadybugDB │ .lcg/db/ │
│ embedder sidecar │ graph+HNSW │ derived │
│ (out-of-process) │ +FTS │ index │
└─────────────────────┴────────────┴──────────────┘
Ingestion: knowledge_process_chunk sends a chunk of text through the extraction LLM, which returns typed entities and relationships (optionally constrained by your ontology). New facts are deduplicated against the existing graph, appended to the WAL, then written to the database with embeddings from the sidecar. Every chunk becomes a time-stamped episode linked to the facts it produced. Recommended maximum chunk_text size is 8,000 characters (default, overridable via LCG_CHUNK_TEXT_ADVISORY_MAX_CHARS — see Configuration): extraction quality degrades well before any context-window limit is reached, and splitting oversized input into multiple knowledge_process_chunk calls is the caller's responsibility. The call still succeeds above the threshold — nothing is truncated, split, or rejected — but the result gains a warning field naming the actual size and the recommended maximum. Both knowledge_process_chunk and knowledge_add_episode also accept an optional attributes object — arbitrary structured metadata stored directly on the resulting episode, retrievable via knowledge_get_episodes and knowledge_search_passages alongside the facts extracted from the same chunk.
Search is hybrid by default: knowledge_find_entities combines full-text search with two vector similarity signals (an entity's name and its summary, so a query that paraphrases an entity's summary — sharing no vocabulary with it — still finds it) via Reciprocal Rank Fusion; knowledge_find_relationships combines full-text and vector similarity; knowledge_search_passages does semantic passage retrieval; knowledge_get_entity_neighbors and knowledge_query_cypher traverse the graph directly. Entities created before this summary-vector capability existed become semantically retrievable by summary via the knowledge_backfill_summary_embeddings admin tool.
Graphs are separated by group_id, and each one owns its WAL stream. A group_id is a real isolation boundary, not a filter applied after the fact: entity resolution, dedup, merge, and purge are all scoped to it, so one graph's ingest cannot rewrite or delete another's data. Each group's mutations land in .lcg/wal/<group_id>/, which makes a single graph independently replayable (knowledge_rebuild_from_wal), independently disposable (knowledge_delete_by_group), and independently shippable — a stream is a directory of JSONL files, so it can be committed to git, distributed, and hydrated into someone else's database.
That is what makes replication and layering practical. A consumer can hydrate several upstream streams into one local database, each keeping its own group_id, and still query any one of them in isolation or all of them together. A stream carries a generation identity (wal.generation) so a consumer can tell a forward advance from a reset and never mistakes a rebuilt upstream for an extension of the one it already replayed. Because entities in different groups stay distinct at the graph layer, references between graphs are expressed as resolvable pointers (knowledge_add_cross_group_edge, knowledge_rebind_pointers) rather than raw edges — so a layer graph can carry its own group_id and connect entities across two source graphs without either source having to know about it, and without the link dangling when a source is re-ingested.
Two transport surfaces. By default the engine serves the Unix-socket JSON-RPC protocol shown above. It can equally run as a native Model Context Protocol server over stdin/stdout (--mcp-stdio), pointing any MCP client straight at the graph with no app or custom client in between. See the IPC & MCP Reference.
No Rust toolchain required:
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/verveguy/liminis-context-graph/releases/latest/download/lcg-service-installer.sh | shPrebuilt binaries are published for macOS (Apple Silicon), Linux x86_64, and Linux ARM64 on every tagged release. If macOS blocks the binary, clear the quarantine attribute: xattr -d com.apple.quarantine ~/.cargo/bin/liminis-context-graph.
OpenSSL 3 is required at runtime. lcg links it dynamically so your package manager's security updates reach it — we do not bundle it, and neither does LadybugDB upstream (ladybug#681). Most systems already have it.
- macOS:
brew install openssl@3. The published Apple Silicon binary resolves OpenSSL at Homebrew's prefix (/opt/homebrew/opt/openssl@3/lib), so Homebrew specifically is required — a MacPorts install at/opt/localwill not satisfy it. Building from source works with either.- Debian/Ubuntu:
apt install libssl3— normally present already.If it is missing, the binary fails at launch with
Library not loaded: /opt/homebrew/opt/openssl@3/lib/libssl.3.dylib(macOS) orerror while loading shared libraries: libssl.so.3(Linux). See Troubleshooting. Design rationale: ADR-0550.
An embedder is required at runtime — see Configuration: Embedder sidecar.
# start your embedding service first — see Configuration: Embedder sidecar
cd your-workspace/ # the directory whose content you're indexing
liminis-context-graph # creates .lcg/, binds .lcg/service.sockThe service speaks newline-delimited JSON-RPC 2.0 over the socket — from any language:
import socket, json
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(".lcg/service.sock")
f = s.makefile("r", encoding="utf-8")
def call(method, params, id=1):
s.sendall((json.dumps({"jsonrpc": "2.0", "id": id, "method": method, "params": params}) + "\n").encode())
return json.loads(f.readline())["result"]
# ingest a chunk of text
call("knowledge_process_chunk", {
"chunk_text": "Ada Lovelace wrote the first program for Babbage's Analytical Engine.",
"chunk_id": "notes-0001",
"source_file": "notes.md",
})
# hybrid (full-text + vector) entity search
print(call("knowledge_find_entities", {"query": "early computing pioneers", "num_results": 5}, id=2))
# graph + WAL health at a glance
print(call("knowledge_status", {}, id=3))Or run it as a native MCP server instead — add to your client's MCP config:
{
"mcpServers": {
"liminis-context-graph": {
"command": "liminis-context-graph",
"args": ["--mcp-stdio", "--scope=read,write"],
"cwd": "/path/to/your-workspace"
}
}
}Requires Rust/Cargo, a C++20 compiler, and OpenSSL 3. The first build downloads a prebuilt lbug bundle (LadybugDB bindings), so the graph engine itself is never compiled — no cmake build step and no C++ dependency tree. lbug's build.rs does still compile its own small cxx FFI bridge locally at -std=c++2a, which is why a C++20 compiler is needed (GCC 13+ / a recent Clang; Ubuntu 22.04's GCC 11 is too old, as it lacks <format>). The bundle statically ships its other third-party dependencies, but since lbug 0.18.0 it links OpenSSL externally, so you also need openssl@3 (macOS: brew install openssl@3; Debian/Ubuntu: apt install libssl-dev).
Released binaries need OpenSSL 3 too — they link it dynamically rather than bundling it, so the same openssl@3 / libssl3 requirement applies at runtime as well as at build time. See ADR-0550.
cargo build --release # build both crates
cargo test -p lcg-core # integration tests (LadybugDB round-trip)
cargo run --example basic_ingest -p lcg-core # example: ingest 3 docs, search, print
cargo run -p lcg-service # run the service binarySee Getting Started for downstream-app bundling and pinned-release tarball URLs.
On a machine that has never run lcg before, Db::open would otherwise run INSTALL vector /
INSTALL fts, which lbug resolves by downloading those extensions from
extension.ladybugdb.com — a problem on an air-gapped host, behind an egress-restricted proxy,
or during a CDN outage. The published release archive bundles both extension binaries for its
target platform, so a fresh install never needs that download:
- Default (bundled): the release archive ships a
.lbdb/extension/<version>/<platform>/directory alongside the binary.Db::openfinds it automatically (resolved relative to the running executable's own location) and loads each extension directly by its absolute path (LOAD EXTENSION '<path>') — noINSTALL, no network call, no configuration needed. LCG_LBUG_HOME: set this environment variable to a directory containing your own.lbdb/extension/<version>/<platform>/{vector,fts}/lib{vector,fts}.lbug_extensiontree to override the bundled location — useful for a custom packaging layout, a shared read-only mount, or staging extensions somewhere other than next to the binary. Takes precedence over the bundled path.- Neither available: if
LCG_LBUG_HOMEis unset and no bundled directory is found relative to the running executable (e.g. running from acargo builddev binary, or a release archive that wasn't extracted intact), behavior is unchanged from before this feature: lbug installs the extensions via its own defaultINSTALL/download path, which requires network access toextension.ladybugdb.comon first use.
A partial bundle or override directory (one of the two extension files present, the other missing) is treated as a configuration error and fails loudly with the resolved path and the missing file named, rather than silently falling through and reaching the network in what you expect to be an offline deployment.
See ADR-0559 for the full design.
lcg links OpenSSL 3 dynamically rather than bundling it, so that your package manager's security updates reach it — see ADR-0550 for why, and ladybug#681 for upstream's matching position. Both errors mean OpenSSL 3 is not installed where the binary looks for it.
# macOS — Homebrew, required for the published Apple Silicon binary
brew install openssl@3
# Debian / Ubuntu
sudo apt install libssl3Check what the binary actually wants with
otool -L $(which liminis-context-graph). The published Apple Silicon binary
names Homebrew's stable prefix, /opt/homebrew/opt/openssl@3/lib/libssl.3.dylib
— that path is a symlink Homebrew maintains across openssl@3 patch upgrades,
so it keeps working as OpenSSL is updated.
MacPorts, or Homebrew somewhere non-standard? The published binary will not
find OpenSSL there. Either symlink it into place, or build from source
(cargo install --path crates/service), which links against whatever your
package manager provides. Making the published binary relocatable across all
three prefixes is tracked in
#550.
This surfaces most confusingly under an MCP client, which usually reports only "server failed to start" and discards the underlying dyld message. If an MCP server fails to start with no explanation, run the binary directly from a terminal first — the real error appears there.
In scope: a single-user, local-first context graph engine, shipped as a library crate (lcg-core) and an IPC binary (lcg-service) that are peers — embed it in a Rust application, or drive it from any language over the socket.
Multi-graph, not multi-tenant. One process can hold many graphs, each with its own group_id and its own WAL stream, and a graph can be replicated into another database by shipping that stream. That is a data-organisation capability for one user's own workspaces and subscriptions — it is not tenancy. There is no authentication, no authorisation, and no per-tenant resource isolation: anything that can reach the socket can reach every group in the database. Treat the process boundary as the trust boundary.
Out of scope, by design:
- Storage engines other than LadybugDB — the single-engine bet is what keeps the service embedded, fast, and simple to operate.
- In-process ML runtimes (
tch,candle,onnxruntime) — embeddings and extraction stay behind out-of-process adapters. - Hosted or multi-tenant deployment — this is local-first infrastructure: one process, on your machine, serving one user. Groups separate that user's graphs from each other; they do not separate users from each other, and are not a security boundary.
Full reference documentation is published at v3rv.com/liminis-context-graph:
- Getting Started — install, run, build from source, bundle in downstream apps.
- Configuration — every environment variable and CLI flag.
- IPC & MCP Reference — the JSON-RPC and Model Context Protocol method surface.
- Telemetry — structured JSONL events emitted on stderr.
- Ontology — the optional entity/relation type vocabulary.
- Operations — WAL administration, degraded mode, and self-healing recovery.
- Testing & Evaluation — LLM cassettes and the extraction-quality eval harness.
- ADR Index — architecture decision records (historical, not current-state, documentation).
The site documents the version stated on its home page and may lag main between releases; this README's quickstart always works against main.
crates/core/ # lcg-core: library crate — all DB interaction
crates/core/benches/ # performance benchmarks (criterion)
crates/core/examples/ # standalone consumers demonstrating the library API
crates/service/ # lcg-service: binary crate — IPC service (builds `liminis-context-graph`)
crates/eval/ # lcg-eval: binary crate — extraction-quality eval harness
native/local-inference/ # Swift CoreML embedding/LLM sidecar for macOS
docs/ # documentation site source (published at v3rv.com/liminis-context-graph)
docs/adr/ # architecture decision records (index at docs/adr/index.md)
specs/ # feature specifications
| Crate | Version | Role |
|---|---|---|
lbug |
=0.20.1 |
LadybugDB Rust bindings (pinned) |
thiserror |
2 |
Error type generation |
No ML-runtime dependencies (tch, candle, onnxruntime) are permitted — embeddings are produced out-of-process.
See docs/adr/ for recorded architecture decisions (index). The project constitution lives at .specify/memory/constitution.md.
Contributions are welcome. See CONTRIBUTING.md for how to file issues, submit pull requests, and the required pre-commit checks. No CLA or DCO sign-off is required — contributions are accepted under the project's MIT license by inbound=outbound convention.
To report a security vulnerability, please use GitHub's private vulnerability reporting rather than filing a public issue. See SECURITY.md for supported versions, response time, and disclosure policy.