DOCUMENTATION · v0.3
Everything is a fact on an interval.
One Rust binary, embedded SQLite, µs point queries. This page covers setup, the SDKs, the HTTP API, and the temporal model: everything you need to put a memory that admits staleness inside a control loop.
Setup: build, serve, connect.
No runtime, no cluster, no ops. Build the binary, then pick a surface: HTTP for services, MCP for LLM agents.
# build from source: one Rust binary, no runtime
git clone https://github.com/difrasai/erubus
cd erubus
cargo build --release
# self-check: temporal, bitemporal, clock, merge, decay
./target/release/erubusSTORAGE
One SQLite file per namespace: erubus.db, erubus-<ns>.db
NAMESPACES
Header X-Erubus-Ns or query ?ns= · ^[a-z0-9_-]{1,64}$
CLOCKS
All times are UTC UNIX epoch seconds; far-future writes rejected
Quickstart: first fact in 30 seconds.
- 01
Start the server
erubus servelistens on 127.0.0.1:8080; auth off in dev, with a loud warning
- 02
Teach it how fast the camera goes stale
curl -X POST localhost:8080/decay -d '{"source_prefix":"vision:","half_life_secs":5}'confidence halves every 5s of silence from any vision: source
- 03
Assert a fact
curl -X POST localhost:8080/fact -d '{"subject":"pallet-7","rel":"at","object":"bay-3","confidence":0.9,"source":"vision:cam1"}'202 accepted: enqueued to the single-writer channel
- 04
Query it, decay included
curl 'localhost:8080/value?subject=pallet-7&rel=at'effective_confidence shrinks as the observation ages
SDKs: Python, C++, raw HTTP, MCP.
Both clients are dependency-free on purpose: the Python client is stdlib-only, the C++ client is a single header that drops into a ROS 2 node with nothing to vendor.
# clients/python: stdlib only, no pip dependencies
from erubus import Erubus
kg = Erubus()
kg.set_decay("vision:", half_life_secs=5)
kg.assert_fact("pallet-7", "at", "bay-3",
confidence=0.9, source="vision:cam1")
kg.value_at("pallet-7", "at")
# {'value': 'bay-3', 'effective_confidence': 0.87}
with kg.batch() as b: # one request, many facts
for det in detections:
b.add(det.id, "at", det.bay, confidence=det.score)
for fact in kg.watch("pallet-7", reconnect=True):
print(fact) # live SSE streamHTTP API: fourteen endpoints, no surprises.
| METHOD | PATH | WHAT IT DOES | SCOPE |
|---|---|---|---|
| POST | /fact | assert one fact; t defaults to now (UTC) | rw |
| POST | /facts | batch ingest, all-or-nothing (max 10k / 32 MiB) | rw |
| GET | /value | belief at T using only facts learned by K | ro |
| GET | /context | neighbourhood of a root as of T (depth ≤ 4) | ro |
| GET | /watch | SSE stream of facts touching a subject | ro |
| POST | /decay | per-source-prefix half-life | rw |
| POST | /merge | declare same entity; rewrites history | rw |
| GET | /suggest_merge | fuzzy merge candidates (never auto-merges) | ro |
| GET | /backup | consistent snapshot (VACUUM INTO) | ro |
| POST | /retention | delete closed edges older than N (N ≥ 3600) | rw |
| GET | /schema | list registered relations | ro |
| POST | /schema | upsert relation; ERUBUS_STRICT=1 enforces | rw |
| GET | /health | liveness; minimal body without a token | n/a |
| GET | /metrics | Prometheus text format | ro |
Data model: bitemporal edges, decay, merges.
t_start / t_end record when a fact was true in the world; t_txn records when the system learned it. A supersession not yet learned by known_atleaves the prior edge open for that read, so “what did the robot believe when it decided” is answered exactly, not approximately.
Contradiction closes, never overwrites
Functional relations (at, held_by, status, assigned_to) supersede in time; others accumulate. History stays queryable forever; retention is operator-driven only.
Decay clocks from last_seen
effective = stored × 0.5^((t − last_seen)/half_life). Re-seeing the same object refreshes the clock and keeps the stronger confidence: confirmation of presence, not a new measurement.
Entity merges rewrite history
POST /merge folds "bay 3" into "Bay-03": edges rewritten, supersession repaired, aliases stay flat. Reads and writes both resolve aliases, so either name answers.
Flap rejection guards the live edge
A lower-confidence contradiction within 2s of the live edge's start is dropped when the gap exceeds 0.15. Far-past backfill is accepted and spliced into history.
Auth & TLS: bearer tokens, per-namespace scopes.
# comma-separated token=scope; ro|rw, optionally @namespace
ERUBUS_TOKENS=abc123=rw,readonly1=ro@prod erubus serve
# writes need rw for the request namespace
# reads need any valid token
# /health stays callable without credentials (minimal body)
# unset ERUBUS_TOKENS → auth off (dev mode, loud warning)Operations: backup, metrics, retention.
Backup
GET /backup returns a consistent VACUUM INTO snapshot of the live WAL database. Restore = stop, replace the .db (delete -wal/-shm), start.
erubus backup src.db dst.dbMetrics
Prometheus text at /metrics: ingest/reject counters, writer_alive, queue depth, edge count, db bytes: all labeled by namespace.
erubus_facts_rejected_total{ns=…}Retention
History is never deleted by default. POST /retention trims closed edges older than N seconds (N ≥ 3600); live edges are never touched.
{"older_than_secs": 86400}CLI: one binary, every mode.
- $ erubusself-check (temporal, bitemporal, clock, merge, decay)
- $ erubus demoa robot picks the wrong bay, and the graph proves why
- $ erubus serveHTTP(S) service on 127.0.0.1:8080
- $ erubus mcpMCP server over stdio
- $ erubus observestream this machine's processes in as facts
- $ erubus evalmeasured: does temporal context reduce errors?
- $ erubus benchingest + query latency at 10k / 100k / 1M facts
- $ erubus fsck <path>verify temporal invariants on a database
- $ erubus backup <src> <dst>consistent snapshot, prints sizes
- $ erubus suggest <db>offline fuzzy merge candidates (JSON)
- $ erubus crashtest30× kill -9 mid-write; invariants must survive
Benchmarks: measured, reproducible.
Windows, release build, synthetic churn. Self-measured; rerun with cargo run --release -- bench.
Query latency stays inside a control loop at 1M facts. Past ~10k/s sustained ingest at that size, shard by namespace.
Measured against Mem0: same trace, same harness.
The 8-seed warehouse trace (8,085 decisions) replayed through Mem0's OSS build (mem0ai 2.0.13, FastEmbed + FAISS, offline, no cloud key; 2026-07-24). Mem0's relevance score stands in for confidence at the same gate 0.70. Reproduce with benchmarks/per_seed.py.
| policy | wrong picks | re-scan | cost/decision |
|---|---|---|---|
| Mem0: latest belief | 15.9% | n/a | 4.78 |
| Mem0: gate on relevance @0.70 | 10.3% | 10.2% | 3.19 |
| Erubus: gate on decayed confidence @0.70 | 0.3% | 76.4% | 0.86 |
Mem0's latest-belief error (15.9%) matches the modelled baseline band; the workload is scored identically for both. Gating on Mem0's relevance still leaves 10.3% wrong picks: that score measures semantic similarity, not staleness, so a stale-but-similar memory keeps a high score. Erubus gates on decayed confidence and reaches 0.3%: ~30× fewer wrong picks on identical input. Mem0 is a fine agent-memory store; it just doesn't model time, which is the whole job here.
Two more worlds: and where the advantage does not show.
The warehouse is one regime. The Embodied Belief Benchmark adds two more, run the same way (Erubus + Mem0, 8 seeds, gate 0.70). We publish the ties: decayed confidence beats static confidence decisively only when high-confidence beliefs go stale faster than they are refreshed and a persistently-wrong source is present.
| world | Erubus wrong | Mem0 wrong | why |
|---|---|---|---|
| warehouse | 0.3% | 10.3% | stale high-conf + never-decaying-but-wrong source → decay wins ~30× |
| inspection | 3.5% | 3.7% | tie: truth drifts too slowly for staleness to bite |
| shared-zone | 21.0% | 21.5% | tie: wrong beliefs are freshly written; no gate catches them |
One offline command; no vendor can dispute a run you did yourself. Reproduce with benchmarks/worlds.py.