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/erubus

STORAGE

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. 

  1. 01

    Start the server

    erubus serve

    listens on 127.0.0.1:8080; auth off in dev, with a loud warning

  2. 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

  3. 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

  4. 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 stream

HTTP API: fourteen endpoints, no surprises. 

METHODPATHWHAT IT DOESSCOPE
POST/factassert one fact; t defaults to now (UTC)rw
POST/factsbatch ingest, all-or-nothing (max 10k / 32 MiB)rw
GET/valuebelief at T using only facts learned by Kro
GET/contextneighbourhood of a root as of T (depth ≤ 4)ro
GET/watchSSE stream of facts touching a subjectro
POST/decayper-source-prefix half-liferw
POST/mergedeclare same entity; rewrites historyrw
GET/suggest_mergefuzzy merge candidates (never auto-merges)ro
GET/backupconsistent snapshot (VACUUM INTO)ro
POST/retentiondelete closed edges older than N (N ≥ 3600)rw
GET/schemalist registered relationsro
POST/schemaupsert relation; ERUBUS_STRICT=1 enforcesrw
GET/healthliveness; minimal body without a tokenn/a
GET/metricsPrometheus text formatro
GOTCHA · 202 means enqueued, not stored. Writes are validated, pushed onto the single-writer channel, and acknowledged. Flap rejection runs later on the writer thread; a fact can report accepted and never become an edge. The only signal is erubus_facts_rejected_total; alert on it if silent rejection matters.

Data model: bitemporal edges, decay, merges. 

TWO TIMELINES PER EDGEvalid time × transaction time
worldat bay-3 [t=0 → t=12]at bay-9 [t=12 → ?]learnedt_txn=0t_txn=19 (late!)value_at(t=15, known_at=15)→ bay-3: the robot could not have known better

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.db

Metrics

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. 

TERMINAL
  • $ 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.

10k facts
ingest~62k/s
value_at p9928 µs
1.8 MiB
100k facts
ingest~30k/s
value_at p9945 µs
17 MiB
1M facts
ingest~9.1k/s
value_at p99434 µs
170 MiB

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.

policywrong picksre-scancost/decision
Mem0: latest belief15.9%n/a4.78
Mem0: gate on relevance @0.7010.3%10.2%3.19
Erubus: gate on decayed confidence @0.700.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.

worldErubus wrongMem0 wrongwhy
warehouse0.3%10.3%stale high-conf + never-decaying-but-wrong source → decay wins ~30×
inspection3.5%3.7%tie: truth drifts too slowly for staleness to bite
shared-zone21.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.

Ready? One command.

cargo run --release -- demo

← Back to the landing page

Erubus