mcp server
aegisdb
A fast, single-binary C database that gives AI agents durable, searchable long-term memory — with a Claude Code integration.
Set up
Runs on your computer. Pick your agent and operating system.
This project does not publish an install method we can turn into a config. Follow the README instead.
README
synced Oct 21, 2026AegisDB
Self-hosted memory for your AI agents. One small C binary — multi-tenant, encrypted, with backups, read replicas, and a one-command Prometheus + Grafana stack. Your agents' memory stays on your box; nothing ships to a SaaS.
AI agents forget everything between sessions. AegisDB gives them durable, searchable long-term memory — episodic history, semantic facts with vector search, and volatile working memory — behind a dead-simple JSON-over-TCP protocol, with a first-class Claude Code integration. It's a single dependency-free binary you run yourself: your data, your box, no third party in the loop.
A memory can also carry a machine-readable fact ({s, p, o}) beside its
prose. Declare the vocabulary and the server draws the conclusions those facts
entail, links each one to the premises it rests on, and withdraws it when a
premise goes away — so it can answer questions whose answer lives in no single
record, and show its working. All opt-in, off by default, and never on the
recall path.
Quickstart
Run the server — no clone, no toolchain (prebuilt multi-arch image on GHCR):
docker run -d --name aegisdb -p 9470:9470 -v aegis-data:/data \
ghcr.io/d4n-larsson/aegisdb:latest
Talk to it — the same binary is also the client:
docker exec aegisdb aegisdb client ping
docker exec aegisdb aegisdb client put --type semantic --tags user "prefers dark mode"
docker exec aegisdb aegisdb client search --tags user --top-k 5
Want the whole observability stack (server + Prometheus + a pre-built Grafana dashboard) in one command? Clone this repo and:
docker compose --profile monitoring up # dashboard on http://127.0.0.1:3000
Giving Claude Code a persistent memory is a one-liner (with a server
running): uvx --from aegisdb-mcp aegisdb-init — see
Use as Claude Code memory.
Why AegisDB
- Self-hosted & private. Your agents' memory never leaves your infrastructure — no SaaS, no per-token billing, no data-sharing. Encrypt it at rest with one flag.
- One binary, no dependencies. Written in C; the only vendored code is cJSON and the crypto. No JVM, no Python runtime, no external database to babysit — and the Python client keeps the promise, with an empty dependency list of its own.
- Built for teams. Multi-tenant auth (per-namespace, scoped tokens), per-tenant quotas + rate limits, online backups, read replicas, and turnkey Prometheus/Grafana observability.
- Claude Code native. Ships an MCP server + hooks so Claude remembers across sessions — installable with a single command.
- Production-minded. Corruption-resilient append-only log, crash recovery, a documented security review, and CI that runs ASan/UBSan/TSan plus continuous fuzzing.
What teams use it for
The flagship case: one shared memory pool per repo. Everyone on a team re-teaches their agent the same things — why the auth middleware is bypassed in staging, that the build needs a clean rebuild after a header change, which migration approach was rejected last quarter and why. That knowledge dies at the end of each session, per person, and the next agent re-derives it from scratch.
Point every teammate's Claude Code at one AegisDB with a token in a shared
namespace, and a decision captured on Tuesday is recalled into someone else's
session on Thursday — automatically, ranked and size-capped, a relevant slice
rather than a context dump. A new hire's agent starts on day one already knowing
the conventions, the gotchas, and the shape of the system.
docs/tutorial-team-server.md is the end-to-end
setup; integrations/claude-code/README.md
covers the client side.
Self-hosting is load-bearing here, not a preference. The memories a coding agent accumulates are your internal engineering knowledge — architecture rationale, security posture, incident history, unreleased plans. That is exactly the category most teams can't hand to a third-party SaaS, which is why this is a binary you run, on your box, with a key only you hold.
Three more that fall out of the same deployment:
- Isolated tenants instead of a shared pool. One namespace per person or per
client project, enforced by the server — a consultancy can run a single
instance across engagements with no cross-contamination. Per-tenant quotas and
rate limits (
--tenant-max-records,--tenant-rate-qps) stop one runaway agent from filling the disk or monopolizing the box, which is the failure mode that actually kills shared internal services. - An auditable memory layer, not a black box.
searchwithexplain:trueshows why each memory ranked; the inspector UI browses and deletes bad ones;historyandgetwithas_ofanswer "what did the agent believe when it made that recommendation?" When agents influence real decisions, being able to inspect and correct their memory is what makes them trustworthy — andexport/purgecover the "erase everything about X" request when it arrives. - Read-only tokens for automation. A
ro-scoped token lets CI, a review bot, or a docs generator read team memory without writing to it, keeping the write path limited to humans' interactive sessions.
Where it doesn't pay off. This earns its keep for a team with agents in the
daily loop that already feels the re-explaining tax. If you use Claude Code
occasionally, a CLAUDE.md checked into the repo does most of the job for free —
the value starts when the knowledge is too large, too per-developer, or too
fast-moving to hand-maintain in a file. A shared pool also wants a little hygiene
(consolidate, forget) so it doesn't silt up with stale facts, and make eval
tells you whether recall quality is holding.
Features
- Durable episodic memory — append-only log with magic + CRC32 framing, corruption-resilient recovery, and legacy-log migration
- Semantic facts — updateable records (latest version wins)
- Working memory — volatile per-session ring buffer with TTL and promotion
- Retrieval — lookup by ID, time-range search, tag search (
all/any), semantic (embedding) search ranked by cosine similarity weighted by importance × confidence, pluscountover the same filters - Keyword + hybrid search — BM25 over payloads that keeps identifiers intact,
so
--tenant-max-records,hnsw.c:214andAEGIS_RECALL_TOP_Kare findable by their exact spelling — the rare tokens embeddings average away. Passqueryalongsideembeddingand the two ranked lists fuse by reciprocal rank; pass it alone and a server with no embedding provider still answers content queries - Explainable ranking —
searchwithexplain:truereturns a per-hit breakdown (relevance × weight × recency = score, plus which retrieval path found it), so retrieval is inspectable, not a black box - Semantic scale — exact cosine while small; past
--ann-thresholdan HNSW graph for sublinear approximate top-K, built off the write path and sharded so the build parallelizes (--ann-shard-target), optionally int8-quantized - Relationships — directed, typed edges between records and a graph walk
that follows only the kinds you name, forwards or backwards (
traversewithkinds/direction), so "what supersedes this?" and "what depends on this?" are one call each rather than a scan. Every hit reports the hop that reached it, so a walk reads as a path. Agent-namespace isolation throughout - Typed facts —
inserttakes an optionalfact: {s, p, o}alongside the prose, andsearch/counttake apattern({"s": 42, "p": "prefers", "o": "*"}) answered from a purpose-built index.--predicate-registrydeclares the vocabulary and a fact naming an undeclared predicate is refused at insert. A record with no fact encodes exactly as before, on disk and on the wire - Deterministic inference —
--inferencematerializes the transitive, symmetric and inverse-of closures the registry declares, on a background tick and per namespace, never on the write path. Every conclusion carries aderivation(rule, depth, premise ids) thatexplainreturns, written server-side so a client cannot manufacture one — and it is withdrawn once every justification has lost a premise. Two live values for a single-valued predicate raise aconflicts_withedge: reported, never silently resolved. Graded bymake eval-multihop, where the symbolic path answers 100% at recall@5 on questions semantic, keyword and fused retrieval all score 0% on - Prose in, symbols out (Claude Code) — capture can distil a transcript into
triples prompted against the registry as a controlled vocabulary, grounding
each mention to an existing entity by cosine + BM25 before minting a new one;
a triple that doesn't fit is dropped and counted, never bent onto the nearest
predicate. A question can become a
pattern, and a derivation can be read back in English — the model reads the proof, it never produces it. Measured bymake eval-extraction(in-vocabulary rate, with conflation and fragmentation counted apart) - Memory that curates itself —
consolidatecollapses near-duplicate semantic facts (recording asupersedesprovenance link, not silent loss);forgetages out low-value records so a long-running corpus and its RAM plateau — both measurable viamake eval - Forgetting informed by what you actually use — every record carries a
recall count and a last-recalled time, so
forgetscores on evidence rather than a write-time importance guess: a fact recalled yesterday survives even if it was written a year ago, and one nothing ever retrieves ages out - Time travel —
historyreturns every version of a record with validity intervals, andgetwithas_ofreconstructs it as of a past time ("what did the agent know at T?") from the append-only log - Right to be forgotten —
exportdumps a subject's records (per-namespace, paginated) andpurgehard-deletes a namespace and compacts, so the payloads actually leave the on-disk log — the compliance answer to "erase everything about X" - Memory inspector — a local browser UI to browse/search memories, see why
each hit ranked, and edit or delete them (
docker compose --profile inspector up) - Measurable recall —
make evalscores retrieval quality (recall@k / MRR) against a labelled corpus, with modes for the dedup and decay policies, so memory-quality changes are gated on numbers, not vibes;make eval-multihopandmake eval-extractiondo the same for the symbolic path and for prose→triple extraction - Smarter capture (Claude Code) — the MCP integration can distil sessions
into durable facts with an LLM (
extract_mode), deduping and superseding contradictions on write — behind a provider seam (claude-code/anthropic/openai) - Multi-tenant auth — optional bearer tokens (constant-time check;
pingexempt), each bound to a namespace + scope (ro/rw/admin) so one server safely isolates many tenants - Per-tenant limits — optional storage quotas (records/bytes) and a request rate limit per namespace, so one team member's runaway agent can't fill the disk or monopolize the shared server
- Encryption at rest — optional XChaCha20-Poly1305 (vendored, no crypto dependency) over the log + checkpoints; opt-in via
--encryption-key-file, with an offline migrator and encrypted backups/replicas - Observability —
statsop (incl. memory-quality counters: merged / forgotten / purged) plus a drop-in Prometheus exporter + Grafana dashboard (docker compose --profile monitoring up) - Operations — online
snapshot/restore backups and read replicas - Concurrency — sharded
poll()event-loop threads (--io-threads); selectablefsyncdurability (sync/batch/interval)
Requirements
- Linux (primary target) with GCC 11+ or Clang 14+
- One of: CMake 3.20+ or GNU Make
- Python 3.8+ (optional, for the example client below)
Build
With CMake (canonical)
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build --output-on-failure # runs the unit test suite
With Make (no CMake required)
make # builds build/aegisdb
make test # builds and runs the C unit tests
make integration # wire-protocol contract tests (launches the server)
make check # unit + integration
make clean
The server binary is produced at build/aegisdb.
With Docker
Prebuilt multi-arch images (linux/amd64, linux/arm64) are published to GitHub
Container Registry on every push to main and every release tag — no clone or
toolchain needed:
docker run -p 9470:9470 -v aegis-data:/data ghcr.io/d4n-larsson/aegisdb:latest
# or pin a specific release, e.g. ghcr.io/d4n-larsson/aegisdb:0.4.11 (see Releases)
To build it yourself instead, a multi-stage Dockerfile (Debian-slim) compiles
the server and ships a minimal runtime image. Data persists in a named volume at
/data.
# Build and run with Docker Compose
docker compose up --build # serves on localhost:9470
# Or build and run the image directly
docker build -t aegisdb .
docker run -p 127.0.0.1:9470:9470 -v aegis-data:/data aegisdb
Compose is configured by an optional .env file — copy the template and edit:
cp .env.example .env # then tweak port, durability, tenant limits, …
docker compose up --build
Every setting has a default, so .env is optional. It exposes the common flags
as named vars (AEGIS_PORT, AEGIS_EMBEDDING_DIM, AEGIS_DURABILITY,
AEGIS_TENANT_MAX_RECORDS, …) plus AEGIS_EXTRA_ARGS for anything else
(--auth-token-file, --io-threads, ANN tuning, …). See
.env.example for the full list.
To skip building, point
docker-compose.ymlat the published image: replacebuild: .withimage: ghcr.io/d4n-larsson/aegisdb:latest.
The image ships a HEALTHCHECK that uses the binary's built-in --health-check
probe (no extra tooling in the image), so docker ps and Compose
depends_on: condition: service_healthy reflect real server liveness.
The container runs as an unprivileged user. The server listens on 0.0.0.0:9470
inside the container, but Compose publishes that port on the host's loopback
(127.0.0.1) only by default — because the wire protocol is unauthenticated and
plaintext out of the box, it must not be reachable off-box until you secure it.
To expose it deliberately, set AEGIS_BIND=0.0.0.0 (or a specific host IP) in
.env, and first enable authentication: mount a token file into /data and add
--auth-token-file /data/tokens.txt (to AEGIS_EXTRA_ARGS under Compose; see
Authentication). Even with auth, tokens travel in plaintext,
so terminate TLS at a trusted proxy for any non-loopback exposure. Override other
flags by appending them to the run command, e.g. docker run aegisdb --embedding-dim 1024, or (with Compose) via .env.
Run
./build/aegisdb --data-dir ./data --port 9470
Expected startup output:
2026-06-28 12:00:00.000 INFO [aegisdb] AegisDB 0.1.0 starting (log level: info)
2026-06-28 12:00:00.000 WARN [aegisdb] no auth tokens configured; ...
2026-06-28 12:00:00.000 INFO [aegisdb] recovery complete: N records loaded
2026-06-28 12:00:00.000 INFO [aegisdb] listening on 0.0.0.0:9470
2026-06-28 12:00:00.000 INFO [aegisdb] data directory: ./data
Logs go to stderr as <timestamp> <LEVEL> [aegisdb] <message>. Control the
verbosity with --log-level error|warn|info|debug (default info) or the
AEGISDB_LOG_LEVEL environment variable — the flag takes precedence. At
debug, the server logs every accepted connection and dispatched operation.
The WARN line appears only when the server is started without
--auth-token/--auth-token-file (see Authentication).
Talk to it
The same binary is also a client — no nc, no hand-written JSON:
aegisdb client ping
aegisdb client put --type semantic --tags user "prefers dark mode"
aegisdb client get 1
aegisdb client search --tags user --top-k 5
aegisdb client stats
Host, port, and token default to $AEGIS_HOST / $AEGIS_PORT / $AEGIS_TOKEN
(127.0.0.1 / 9470 / none) or --host/--port/--token. The exit code is
0 on an ok response, so it scripts cleanly. Inside Docker:
docker exec aegisdb aegisdb client stats.
To create a tenant token, gen-token prints a ready token-file line (hashed)
and the one-time plaintext token:
$ aegisdb gen-token --namespace acme --scope rw
sha256$… acme rw # paste into your --auth-token-file
token: 9f3c… # give to the client (AEGIS_TOKEN); not recoverable
Configuration flags
| Flag | Default | Description |
|---|---|---|
--data-dir <path> | ./data | Persistence directory |
--port <n> | 9470 | TCP listen port |
--phase <1-4> | 4 | Highest enabled feature phase (gates operations) |
--io-threads <n> | 2× CPUs (8–64) | poll() event-loop threads for dispatch parallelism (does not cap concurrent connections). Alias: --workers |
--max-payload <bytes> | 1048576 | Max data size (1 MiB) |
--embedding-dim <n> | 384 | Expected embedding vector length |
--ann-threshold <n> | 10000 | Live vectors before semantic search switches from exact scan to the HNSW graph |
--ann-ef-search <n> | HNSW default | HNSW query beam width (recall/latency knob) |
--ann-shard-target <n> | 25000 | Target vectors per HNSW shard; the graph splits into ~count/n shards (capped by CPUs) so the build parallelizes |
--ann-quantize | off | Store HNSW vectors as int8 (~4× less memory, small recall cost) |
--durability <mode> | interval | sync (fsync per write), batch (per --fsync-batch records), or interval (per --fsync-interval-ms) |
--fsync-batch <n> | 1000 | Records between fsync calls in batch mode |
--fsync-interval-ms <n> | 1000 | Flush cadence in interval mode (floored at the ~1s maintenance tick) |
--checkpoint-sec <n> | 60 | Index checkpoint cadence so recovery replays only the tail; 0 disables |
--compact-sec <n> | 300 | Log-compaction check cadence; compacts only when enough of the log is dead; 0 disables |
--tenant-max-records <n> | 0 | Per-namespace live-record cap (0 = unlimited); enforced only when auth is enabled |
--tenant-max-bytes <n> | 0 | Per-namespace live-byte cap (0 = unlimited) |
--tenant-rate-qps <n> | 0 | Per-namespace request rate limit in req/s, burst = 1s (0 = unlimited) |
--max-index-bytes <n> | 0 | Soft cap on in-RAM index size; inserts return MEMORY_LIMIT past it so a growing dataset backpressures instead of getting OOM-killed (accepts K/M/G; 0 = unlimited). Watch stats.memory. |
--replication-port <n> | — | Serve the read-replica log stream on this port (primary; requires --replication-token) |
--replication-token <t> | — | Token to subscribe to / follow the replication stream |
--replicate-from <h:p> | — | Follow this primary's replication port as a read-only replica (implies --read-only) |
--read-only | off | Refuse client writes (READ_ONLY) |
--working-capacity <n> | 256 | Working-memory ring buffer size |
--query-scan-cap <n> | 100000 | Records a broad/filterless search/count will load, bounding the cost of an unselective query |
--max-connections <n> | 0 | Hard cap on concurrent client connections (0 = unlimited) |
--idle-timeout-sec <n> | 60 | Reap a connection making no byte progress for this long (0 disables) |
--no-lexical-index | off (index on) | Disable the BM25 keyword index; a query then returns NOT_READY |
--no-edge-index | off (index on) | Disable the reverse edge index; a backward traverse then returns NOT_READY |
--no-fact-index | off (indexes on) | Disable the typed-fact indexes; a pattern then returns NOT_READY |
--no-usage-feedback | off (tracking on) | Stop tracking recall counts, so forget scores on write-time importance alone |
--predicate-registry <path> | — | Declare the typed-fact vocabulary (cardinality, symmetry, transitivity, inverse_of, mutex_with). A fact naming an undeclared predicate is refused; a malformed registry refuses to start. predicates.example.json is a starter vocabulary — see tools/facts/ |
--inference | off | Materialize the declared closures on a background tick, per namespace, and flag contradictions. Off by default because it grows the log rather than RAM |
--inference-interval-sec <n> | 30 | Cadence of the inference pass |
--inference-max-depth <n> | 4 | Longest derivation chain a conclusion may rest on |
--inference-max-derived <n> | 1000 | Conclusions written per pass (shared across tenants, visited round-robin) |
--inference-max-candidates <n> | 1000000 | Conclusions considered per pass |
--inference-max-subsume <n> | 256 | Descendants a subsume expansion may walk before it reports truncation |
--inference-confidence-floor <f> | 0.1 | Floor for confidence propagated as a product along a derivation chain |
--restore <dir> | — | One-shot: install the snapshot at <dir> into an empty --data-dir, then exit |
--log-level <level> | info | error, warn, info, or debug (also $AEGISDB_LOG_LEVEL) |
--auth-token <token> | — | Accept this global admin token (repeatable) |
--auth-token-file <path> | — | Accept tokens, one per line: <token> [namespace] [ro|rw|admin]; a token may be sha256$<hex> (hashed at rest) |
--hash-token <token> | Print the token's sha256$<hex> form and exit (paste into the token file) | |
--encryption-key-file <path> | — | Encrypt the log + checkpoints at rest with the 32-byte key (64 hex chars) in <path> (Encryption at rest) |
--encrypt-migrate | Rewrite --data-dir's plaintext log encrypted (needs --encryption-key-file) and exit | |
--health-check | Probe a local server on --port, print nothing, exit 0 if healthy / 1 otherwise | |
--help | Show usage |
Authentication & multi-tenancy
Setting up a shared server for a team? Follow the step-by-step team server tutorial instead of assembling the flags below by hand.
With no --auth-token/--auth-token-file, the server runs without
authentication and every request is served with unrestricted access. When
tokens are configured, each request must carry a matching "token" field
(except ping, which is always exempt) or the server returns UNAUTHORIZED.
Tokens are compared in constant time.
Each token in the token file is bound to a namespace and a scope:
admin-key # global admin: any namespace, all operations
acme-key acme rw # tenant "acme", read+write
acme-view acme ro # tenant "acme", read-only
beta-key beta rw # tenant "beta"
A namespaced token can only write into its own namespace (agent_id is pinned
automatically) and only read its own records — another tenant's records read
back as NOT_FOUND. Read-only tokens are refused writes with FORBIDDEN, and
stats is admin-only. This lets one server back many isolated tenants/agents.
Tokens can also be managed at runtime (no restart) by an admin token via the
token_list / token_add / token_revoke operations — a revoked token stops
authenticating immediately, and changes persist back to --auth-token-file
(rewritten hashed). Tokens are referenced by a fingerprint id, so they can be
listed and revoked without exposing the secret. See
docs/wire-protocol.md.
Tokens can be stored hashed so a leaked token file reveals nothing usable.
Run aegisdb --hash-token <tok> to get its sha256$<hex> form and put that in
the token file; clients still send the plaintext token, which the server hashes
and compares in constant time. Use high-entropy tokens (openssl rand -hex 32).
Tokens are sent in plaintext, so run the server behind an encrypted channel — a
TLS-terminating reverse proxy (nginx/Caddy), stunnel, or a private network.
TLS is intentionally kept out of the binary to preserve the single,
dependency-free build.
Encryption at rest
The log and index checkpoints can be encrypted on disk with XChaCha20-Poly1305 (vendored — no crypto dependency added), so a stolen disk, volume snapshot, or backup tarball reveals nothing without the key. Mint a key and start with it:
aegisdb gen-key > key.hex # 32-byte key, 64 hex chars; store it 0600
aegisdb --data-dir ./data --encryption-key-file key.hex
- Opt-in and per-directory. A data dir created without a key stays
plaintext. On a new dir the key encrypts from the first write; to convert an
existing plaintext dir, run the offline one-shot
aegisdb --encrypt-migrate --data-dir ./data --encryption-key-file key.hex. - Fail-closed. The server refuses to start if the key does not match the dir (wrong key, or a key given for a plaintext dir / no key for an encrypted one).
- Backups stay encrypted;
--restorerequires the same key (the snapshot manifest records a non-secret key fingerprint and is checked before restoring). - Replicas must be configured with the same
--encryption-key-file; the subscribe handshake rejects a key mismatch. Each node encrypts its own log with the key. - Scope. This protects data at rest. It is not a transport control (the wire, including replication, is still plaintext — front it with a proxy as above) and does not protect a running process's memory. Keep the key safe and separate from the data dir; without it the data is unrecoverable.
Wire Protocol
Newline-delimited JSON (NDJSON) over TCP — one JSON object per line per
request/response. See docs/wire-protocol.md
for the full contract.
# Health check
echo '{"operation":"ping"}' | nc -q1 localhost 9470
# Insert an episodic memory
echo '{"operation":"insert","type":"episodic","tags":["user","preference"],"data":"User likes coffee"}' | nc -q1 localhost 9470
# Retrieve by ID
echo '{"operation":"get","id":1}' | nc -q1 localhost 9470
# Time-range search
echo '{"operation":"search","start_time":0,"end_time":9999999999999,"top_k":10}' | nc -q1 localhost 9470
# Tag search
echo '{"operation":"search","tags":["user"],"match":"all","top_k":10}' | nc -q1 localhost 9470
# Keyword search — finds the exact spelling embeddings average away
echo '{"operation":"search","query":"--tenant-max-records","top_k":5}' | nc -q1 localhost 9470
# A memory that also asserts a typed fact. `s` and `o` are the record ids of the
# two things being related; needs --predicate-registry declaring `part_of`
echo '{"operation":"insert","type":"semantic","data":"hnsw.c is part of the storage layer","fact":{"s":1,"p":"part_of","o":{"id":2}}}' | nc -q1 localhost 9470
# ...and finding it by the fact rather than the words
echo '{"operation":"search","pattern":{"s":1,"p":"part_of"},"explain":true}' | nc -q1 localhost 9470
Supported operations: ping, insert (episodic/semantic/working, single or
batch, optionally carrying a fact), get, update (semantic), delete (by
id or query), search (time/tags/embedding/query/pattern), count,
consolidate, forget, export, purge, history (+ point-in-time get via
as_of), promote, relate, traverse (with kinds/direction),
conflicts, predicates, stats, snapshot, and token administration
(token_list/token_add/token_revoke).
Python client
pip install aegisdb — a dependency-free client with one method per operation
and an exception per wire error code (clients/python/):
from aegisdb import AegisClient, NotFound
with AegisClient(host="127.0.0.1", port=9470) as db:
db.insert("prefers dark mode", type="semantic", tags=["user"])
print(db.search(query="dark mode", top_k=5)["records"])
try:
db.get(999)
except NotFound as exc:
print(exc.code, exc.message)
Raw socket example
import socket, json
def request(payload: dict) -> dict:
with socket.create_connection(("localhost", 9470)) as s:
s.sendall((json.dumps(payload) + "\n").encode())
return json.loads(s.recv(65536).decode())
print(request({"operation": "ping"}))
print(request({"operation": "insert", "type": "episodic",
"tags": ["demo"], "data": "Hello from quickstart"}))
Project Layout
src/
├── main.c # Entry point, CLI args, client subcommands
├── server/ # TCP NDJSON server, sharded poll() event loops
├── protocol/ # JSON request parsing / response building
├── query/ # Operation router / query engine
├── storage/ # Append-only log, hash/time/tag/semantic/lexical/
│ # edge/fact/usage indexes, compaction, recovery
├── memory/ # MemoryRecord encode/decode, working buffer
└── util/ # CRC32, SHA-256, config, health check, client, logging
include/aegisdb/ # Public headers
clients/python/ # `aegisdb` — the dependency-free client SDK
predicates.example.json # starter typed-fact vocabulary (see tools/facts/)
integrations/ # claude-code (MCP), langgraph (store), prometheus-exporter
tests/ # unit/, integration/, contract/
third_party/ # Vendored cJSON and Unity
data/ # Runtime data (gitignored)
Crash Recovery
AegisDB persists episodic and semantic records to an append-only
memory.log with per-frame header and payload CRC32 checksums. On startup it
loads the index checkpoint (memory.index) and replays only the log tail written
since — falling back to a full scan if the checkpoint is missing or corrupt —
then rebuilds the in-memory indexes. A torn tail from a mid-write crash is
trimmed; interior corruption is skipped frame by frame so the surrounding records
still load. To verify:
- Insert several records.
kill -9 <pid>the server.- Restart it — startup logs
recovery complete: N records loaded. geteach ID; all records return intact.
Backups & disaster recovery
Recovery + a durable volume + restart: unless-stopped already survive a
process crash. To survive losing the host or disk, take backups off the box.
AegisDB is single-node by design (no built-in replication), and because the log
is append-only a backup is just a consistent snapshot of its durable prefix. The
admin snapshot op writes one online (no downtime); --restore installs it into
an empty data dir. scripts/aegis-backup.sh automates
the loop — snapshot → tarball → ship off-box via a transport you supply (S3,
rclone, rsync, …) → local retention:
# host cron (daily), shipping to S3:
AEGIS_BACKUP_UPLOAD_CMD='aws s3 cp {} s3://my-bucket/aegis/' \
scripts/aegis-backup.sh
Or run it on a schedule as the opt-in compose sidecar:
docker compose --profile backup up -d # loops the script (default: daily)
Restore a shipped tarball into a fresh server:
tar -xzf aegis-20260709T....tar.gz -C /tmp/snap
aegisdb --restore /tmp/snap/aegis-20260709T... --data-dir ./restored --embedding-dim 384
aegisdb --data-dir ./restored --embedding-dim 384 # start; recovery rebuilds indexes
See .env.example for the backup knobs (AEGIS_BACKUP_UPLOAD_CMD,
AEGIS_BACKUP_INTERVAL, AEGIS_BACKUP_RETAIN). The transport is pluggable so no
cloud SDK is baked into the image — the same "bring your own edge" stance as TLS.
Do not scale the server with
deploy: replicas: Nagainst the shared volume: AegisDB is single-writer (one append-only log, one id allocator), so multiple writers would corrupt the data. Backups and read replicas (below) are the supported resilience path.
Read replicas
For read availability and read scaling, a read-only replica follows a primary
by streaming its append-only log and replaying it — always the same frames in the
same order, so the replica's log is byte-identical and offsets line up. Replicas
are asynchronous (eventually consistent, bounded by lag) and read-only; promotion
after a primary failure is a manual, operator-fenced step. Full design and the
promotion runbook: docs/read-replica-design.md.
# primary: serve the replication stream (needs a token)
aegisdb --data-dir ./p --port 9470 \
--replication-port 9480 --replication-token "$TOKEN"
# replica: follow the primary, serve read-only on its own port
aegisdb --data-dir ./r --port 9471 \
--replicate-from 127.0.0.1:9480 --replication-token "$TOKEN"
Send writes to the primary and spread reads across either; a write to a replica
returns READ_ONLY. stats reports the replication posture (role,
lag_bytes, connected replicas). Compaction on the primary rewrites offsets, so
a replica automatically re-bootstraps when it detects the change. Every node
must use the same --embedding-dim. The stream is authenticated by the token but
not encrypted — keep it on a trusted network / behind a TLS proxy, like the
client protocol.
Use as a LangGraph store
pip install aegisdb-langgraph makes AegisDB the long-term memory behind a
LangGraph agent — BaseStore's get/put/search/delete/list_namespaces,
with the namespace hierarchy carried in tags so a prefix search stays an index
lookup:
from aegisdb_langgraph import AegisStore
graph = builder.compile(store=AegisStore(namespace="my-agent"))
Three things it deliberately does not do — TTL, vector indexing, and
server-side filter — are spelled out with their reasons in
integrations/langgraph/README.md.
Use as Claude Code memory
AegisDB can act as the persistent long-term memory of Claude Code
via the integration in integrations/claude-code/:
an MCP server exposing memory tools plus hooks for automatic recall and capture.
It is published to PyPI as aegisdb-mcp,
so uvx aegisdb-mcp runs it with no clone. Fastest setup: with a server
running, uvx --from aegisdb-mcp aegisdb-init scaffolds .mcp.json and the hooks
for you (or install the /aegis-setup skill and let Claude do it). See
integrations/claude-code/README.md for
that and the manual step-by-step.
Documentation
- Team server tutorial (guided golden path — auth, tenants, encryption, backups):
docs/tutorial-team-server.md - Wire protocol:
docs/wire-protocol.md - Quickstart (solo / local):
docs/quickstart.md - Architecture:
docs/architecture.md - Read-replica design & promotion runbook:
docs/read-replica-design.md - Roadmap (where the leverage is, and what shipped):
docs/ROADMAP.md - The symbolic layer — typed facts, inference, and the model seam:
docs/symbolic-layer-design.md,docs/typed-facts-design.md,docs/inference-design.md,docs/neuro-symbolic-design.md - Recall-quality eval harness:
eval/README.md - Python client SDK:
clients/python/README.md - Starter predicate vocabulary + a worked fact corpus:
tools/facts/README.md - LangGraph store adapter:
integrations/langgraph/README.md