Agent-first memory for Beater. It is a local Rust app and library that turns ledgered agent traces into typed, temporal memory and answers queries with provenance, contradiction warnings, and a token budget.
The design follows research/agent-memory.md:
memory is a projection over append-only traces, not a plain vector database.
See ARCHITECTURE.md for the layer boundaries.
ledgered agent traces
-> offline distiller
-> typed temporal graph and substores
-> tiered read path
-> compact answer with provenance, contradictions, and token budget
The current implementation includes:
- SQLite ledger/projection store
- deterministic
ADD / UPDATE / INVALIDATE / NOOPdistiller - provider-safe distillation boundary with strict JSON schema parsing, validation, bounded repair attempts, rejection metrics, and no provider calls inside projection write transactions
- opt-in command-backed provider distillation for CLI and HTTP manage paths, with a per-call timeout and bounded repair attempts
- provider distillation can emit typed relation edges to scoped neighbor
memories; projected edges carry source-event provenance so bitemporal
known_at_unix_msreads traverse only relationships known at that time - opt-in command-backed active reconstruction for CLI and HTTP query paths, with strict decision parsing, candidate validation, and provider metrics
- memory economics telemetry for projection source/stored-token estimates, active stored-token totals, active node counts by kind, and query tier latency/token counters
- distiller output validation before graph projection writes
- typed nodes and edges for facts, episodes, procedures, state, gotchas, and anti-memory
- deterministic typed-substore read routing with conservative fallback and response/audit diagnostics
- lexical cue seeding plus graph activation ranking
beater.jsjournal import from.beater/journal.db- canonical span JSONL import aligned with
beater-agentsspan kinds - atomic imports that roll back malformed batches and report the bad source row
- CLI commands for
init,remember,manage,project,query, and import flows - authenticated HTTP API for service deployments
- optional idempotency keys for retry-safe direct
rememberwrites - production operations for schema/integrity health checks and SQLite maintenance, backup, and restore
- graph projection integrity checks and orphan repair for edges, citations, and cue index entries
- bitemporal query windows:
as_of_unix_msfilters observed validity, whileknown_at_unix_msfilters what the ledger had ingested by that transaction time - optional Tier 2 active reconstruction for hard or forced read-time graph exploration, with bounded steps, token budget, and a diagnostic report
- ledger validation that rejects malformed events before they enter the append-only log
- query validation that rejects malformed scopes, empty questions, and unusable token budgets before retrieval
- HTTP query requests are validated through the same
MemoryQuerycontract used by the engine before any retrieval task runs - projection limit validation that rejects zero-sized project and rebuild batches before mutating derived graph state
- checked SQLite limit binding so oversized embedded read or retention limits cannot become unbounded queries
- audit record validation and non-negative audit-retention cutoffs before writing or pruning the durable operational trail
- guarded projection rebuild from the append-only ledger
- explicit audit retention pruning by age or newest-row count
- database identity checks that reject unrelated SQLite files instead of silently migrating them
- service metrics, persisted audit events, fixed-window request limiting, and bounded DB work concurrency
x-request-idpropagation for HTTP responses and durable audit rows- deterministic evaluation harness for LongMemEval-style ability cases, expectation-based judging, context-saturation gap placeholders, and read/write economics reports; cases are isolated by default unless the suite opts into a shared haystack, and eval runs can opt into the same provider distillation and active reconstruction adapters as CLI/HTTP runtime paths
- answer-shaped
MemoryAnswerwith citations, stale assumptions, contradictions, suggested follow-up queries, and token estimates
cargo run -p beater-memory -- init
cargo run -p beater-memory -- remember \
--idempotency-key demo-checkout-db-gotcha \
--tenant local --project demo --kind gotcha \
"Checkout fails when DATABASE_URL is missing. Fix by setting DATABASE_URL."
cargo run -p beater-memory -- remember --no-project \
--tenant local --project demo --kind fact \
"Append this observation without managing projections yet."
cargo run -p beater-memory -- manage --limit 100
cargo run -p beater-memory -- query \
--tenant local --project demo \
"How do I fix checkout database failures?"
cargo run -p beater-memory -- query \
--tenant local --project demo \
--as-of-unix-ms 1782864000000 \
--known-at-unix-ms 1782867600000 \
"How did checkout look at that time?"
cargo run -p beater-memory -- query \
--tenant local --project demo \
--reconstruction-mode auto \
"why did checkout database failures recover?"
cargo run -p beater-memory -- \
--reconstructor provider-command \
--reconstructor-command ./reconstruct-provider \
query --tenant local --project demo \
--reconstruction-mode force \
"why did checkout database failures recover?"
cargo run -p beater-memory -- health --jsonRun the HTTP API:
export BEATER_MEMORY_TOKEN='dev-secret'
cargo run -p beater-memory -- serve --bind 127.0.0.1:8765The server refuses to start without a bearer token unless --allow-no-auth is
passed, trims configured bearer tokens, and rejects blank tokens. Public request
limits for body size, projection batch size, query token budget, and audit page
size must be greater than zero. It drains with Axum graceful shutdown on Ctrl-C
and on SIGTERM for Unix process managers. All /v1/* routes require
Authorization: Bearer <token>; /livez is the unauthenticated liveness
endpoint, and /readyz is the unauthenticated readiness endpoint for DB-backed
traffic. The service defaults to 600 authenticated requests per actor per
minute; use
--max-requests-per-minute 0 to disable the fixed-window limiter for a trusted
local deployment. Every response includes x-request-id; client-supplied valid
request IDs are echoed, and generated IDs are written into durable audit detail.
DB-backed HTTP requests are also capped at 32 concurrent blocking SQLite tasks
by default; use --max-concurrent-db-tasks to tune this. When saturated,
DB-backed routes return 503 service_busy with Retry-After, while /livez,
JSON /v1/metrics, and Prometheus /v1/metrics/prometheus remain available;
/readyz returns 503 until the database work queue is available and health
checks pass. Each DB-backed task also has a 30s wall-time budget by default;
adjust it with --db-task-timeout-ms. Timed-out DB routes return
504 service_timeout and increment db_timeout_requests.
Import a beater.js journal:
cargo run -p beater-memory -- import-beater-js \
--journal ../beater.js/path/to/app/.beater/journal.db \
--tenant local --project my-appImport canonical span JSONL, useful for beater-agents exports:
cargo run -p beater-memory -- import-jsonl \
--path ./spans.jsonl --tenant local --project observed-agentImport commands project by default. Pass --no-project to append imported
ledger events only, then run manage when you want to build projections.
Run a deterministic memory evaluation suite:
cargo run -p beater-memory -- eval --suite ./memory-eval.jsonProvider-backed evals use the same global adapter flags as manage/query:
cargo run -p beater-memory -- \
--distiller provider-command \
--distiller-command ./distill-provider \
--reconstructor provider-command \
--reconstructor-command ./reconstruct-provider \
eval --suite ./memory-eval.json --reconstruction-mode forceeval emits machine-readable JSON on stdout by default. Passing suites keep
stderr empty; failing suites still print the JSON report to stdout and then emit
a human error on stderr with a nonzero exit code.
The default DB path is .beater-memory/memory.db; override with --db.
For retryable direct writes, pass an idempotency key. Reusing the same key in
the same tenant, project, environment, and memory kind maps the write to the same
ledger event; repeated submissions return ingested: false over HTTP instead of
appending duplicate ledger work.
CLI remember and HTTP /v1/remember keep their compatibility default of
managing projections immediately; pass --no-project on the CLI or
"project": false over HTTP for a write-only append, then run manage or
POST /v1/manage later.
Projection commands use the deterministic heuristic distiller by default. To run a local provider adapter at manage time, pass global distiller flags before the subcommand:
cargo run -p beater-memory -- \
--distiller provider-command \
--distiller-command ./distill-provider \
--distiller-timeout-ms 25000 \
remember --no-project --tenant local --project demo --kind fact \
"Append this first, then manage it with the provider."
cargo run -p beater-memory -- \
--distiller provider-command \
--distiller-command ./distill-provider \
--distiller-arg --model \
--distiller-arg gpt-4.1-mini \
manage --limit 100The same flags work for serve:
BEATER_MEMORY_TOKEN=dev-secret cargo run -p beater-memory -- \
--distiller provider-command \
--distiller-command ./distill-provider \
serve --bind 127.0.0.1:8765--distiller-arg can pass provider flags that start with -; repeating the
flag passes multiple command arguments. The provider command timeout defaults to
25s and must be lower than the server DB task timeout when used with serve.
Increase --db-task-timeout-ms if a provider, repair budget, or manage batch
needs more wall-clock time.
The command receives one JSON request on stdin with request: "distill" or
request: "repair", the ledger event, neighbors, and repair fields for
malformed output. It must write provider JSON on stdout:
{
"memories": [
{
"op": "add",
"node_kind": "fact",
"text": "Distilled memory text",
"target_node_id": null,
"relation_edges": [
{"kind": "fixes", "target_node_id": "existing-scoped-neighbor-id"}
],
"cited_spans": [
{"tenant_id": "local", "project_id": "demo", "trace_id": "trace", "span_id": "span", "seq": 1}
]
}
]
}Provider projection reports, HTTP metrics, Prometheus metrics, and audit detail
include provider calls, provider errors, schema errors, repairs, rejections,
durable replay batches, token estimates, and elapsed milliseconds. Accepted
command-provider batches are stored with the projected ledger event, so
rebuild-projection can replay those batches without calling the provider when
the same command replay fingerprint is selected. Rebuild fails before clearing
projections if any already projected event is missing a matching durable batch.
Replay-safe provider batches may use add and noop freely; update and
invalidate must include an explicit target_node_id after validation so
rebuild does not re-resolve targets against a different neighbor set.
relation_edges may point from the emitted memory to an existing scoped
neighbor by target_node_id. Providers may emit domain relation kinds
caused_by, fixes, before, after, part_of, blocks, and enables;
revision and projection-scaffolding kinds such as supersedes, contradicts,
mentions, derived_from, and observed_in are reserved for the engine.
Projected relation edges record the source ledger event, allowing
known_at_unix_ms reads and active reconstruction to use only relationships
that were known by the query's transaction-time boundary.
The current command fingerprint includes the command path string, JSON command
arguments, and effective repair budget; it does not include provider file
contents, environment variables, cwd-dependent behavior, timeout, or model
settings unless those settings are encoded in the command arguments.
Queries use the deterministic active reconstructor by default. To run a local
provider adapter during forced or auto Tier 2 reads, pass global reconstructor
flags before query or serve:
cargo run -p beater-memory -- \
--reconstructor provider-command \
--reconstructor-command ./reconstruct-provider \
--reconstructor-timeout-ms 25000 \
query --tenant local --project demo \
--reconstruction-mode force \
"incident alpha"
BEATER_MEMORY_TOKEN=dev-secret cargo run -p beater-memory -- \
--reconstructor provider-command \
--reconstructor-command ./reconstruct-provider \
--reconstructor-timeout-ms 5000 \
serve --bind 127.0.0.1:8765--reconstructor-arg repeats like --distiller-arg and can pass values that
start with -. The command timeout defaults to 25s and must be lower than the
server DB task timeout when used with serve. CLI query and server startup
reject missing, non-file, or non-executable command paths. HTTP query requests
using provider reconstruction are rejected when
--reconstructor-timeout-ms * max_reconstruction_steps is greater than or equal
to --db-task-timeout-ms, so long-running providers should either use a lower
per-call timeout, fewer reconstruction steps, or a larger DB task timeout.
The command receives one ReconstructionStep JSON document on stdin with
question, step_index, expanded_node_id, remaining_tokens, and
candidates. It must write one decision JSON document on stdout:
{"decision":"accept","node_id":"node-id-from-candidates"}Valid decisions are accept, prune, and stop. accept and prune must
name a candidate node ID from the same step; unknown fields, unknown decisions,
and non-candidate node IDs are treated as schema failures. Provider transport or
schema failures stop active reconstruction for that query and are reported in
the ReconstructionReport; they do not fail the whole query. Query responses,
HTTP metrics, Prometheus metrics, and audit detail include reconstruction
provider calls, provider errors, schema errors, input/output token estimates,
and elapsed milliseconds.
Eval fixtures use contract_version: 1; omitted versions default to v1 for
older local fixtures, while unsupported future versions are rejected. A fixture
can also include source metadata with name, uri, and revision; CLI
reports echo that metadata plus the suite file path.
By default each case runs in an isolated per-case project. Set
"shared_haystack": true only for chronological shared-haystack benchmarks:
the runner ingests, projects, and queries each case in order, so later cases can
see earlier observations but earlier cases cannot see future observations.
Report score is effective_expectation_pass_rate: content expectations are
matched against the answer, evidence, stale assumptions, and contradictions,
then hard gates such as expected_tier force the case score to 0.0 when they
fail. content_score preserves the raw content-only match rate for debugging.
Top-level passed/failed count full case pass/fail, checks_* count content
expectations, and context_saturation_gap is the clamped shortfall versus the
average full-context baseline over cases that supply one. Each case report
includes effective scope, modes, routing/reconstruction telemetry, expectation
match rows, compact answer/evidence excerpts, and write/read economics.
Manage/projection is atomic per ledger event. The engine uses an immediate
SQLite transaction, rechecks that the event is still pending inside the
transaction, then commits the memory nodes, edges, cue index, citations, and
projected marker together. New databases are stamped with the Beater Memory SQLite
application_id; existing databases must already carry that identity before
schema migration runs.
cargo run -p beater-memory -- health
cargo run -p beater-memory -- maintenance
cargo run -p beater-memory -- maintenance --vacuum
cargo run -p beater-memory -- maintenance --repair-orphans
cargo run -p beater-memory -- maintenance --retain-audit-events 10000
cargo run -p beater-memory -- maintenance --prune-audit-before-unix-ms 1782864000000
cargo run -p beater-memory -- rebuild-projection --yes-clear-projections
cargo run -p beater-memory -- backup --path ./backups/memory.db
cargo run -p beater-memory -- restore --path ./backups/memory.db --yes-replace-current-dbBackups use SQLite's online backup API and refuse to overwrite an existing
backup path. Restore replaces the active database and requires the explicit
--yes-replace-current-db flag. Health reports graph projection orphan counts;
maintenance reports graph integrity before and after the pass, and only removes
orphan projection rows when --repair-orphans or HTTP repair_orphans: true is
set. Projection rebuild clears only derived memory nodes, edges, citations, cue
indexes, and projection markers, then replays the append-only ledger; audit rows
and ledger events remain intact. Schema upgrades that introduce source-event
edge provenance reset legacy source-less projection rows when needed so the next
manage/rebuild pass regenerates edges with bitemporal provenance. Audit
retention is explicit: maintenance can
drop rows older than a Unix millisecond cutoff and/or keep only the newest N
audit rows. If both are set, age pruning runs first and newest-row retention is
applied to the remaining audit trail.
HTTP equivalents:
curl -H "Authorization: Bearer $BEATER_MEMORY_TOKEN" \
http://127.0.0.1:8765/v1/health
curl http://127.0.0.1:8765/readyz
curl -H "Authorization: Bearer $BEATER_MEMORY_TOKEN" \
http://127.0.0.1:8765/v1/metrics
curl -H "Authorization: Bearer $BEATER_MEMORY_TOKEN" \
http://127.0.0.1:8765/v1/metrics/prometheus
curl -H "Authorization: Bearer $BEATER_MEMORY_TOKEN" \
'http://127.0.0.1:8765/v1/audit?limit=50'
curl -H "Authorization: Bearer $BEATER_MEMORY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"repair_orphans":true,"retain_latest_audit_events":10000}' \
http://127.0.0.1:8765/v1/maintenance
curl -H "Authorization: Bearer $BEATER_MEMORY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"tenant_id":"local","project_id":"demo","kind":"gotcha","idempotency_key":"demo-checkout-db-gotcha","text":"Checkout fails when DATABASE_URL is missing."}' \
http://127.0.0.1:8765/v1/remember
curl -H "Authorization: Bearer $BEATER_MEMORY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"question":"checkout database failure","scope":{"tenant_id":"local","project_id":"demo","environment_id":null,"as_of_unix_ms":null,"known_at_unix_ms":null},"reconstruction_mode":"auto","max_reconstruction_steps":4,"max_reconstruction_tokens":2000}' \
http://127.0.0.1:8765/v1/queryKey public API exports include:
MemoryEngineProjectReportandProjectionRebuildReportSqliteMemoryStoreMemoryServerConfig,try_memory_router,memory_router,serve, andserve_with_shutdownStoreHealth,StoreStats,MaintenanceOptions,MaintenanceReport,GraphIntegrityReport,GraphRepairReport,AuditPruneReport,ProjectionResetReport,BackupReport,RestoreReport,AuditRecord, andAuditEventLiveResponse,ReadyResponse,ServiceMetricsSnapshot, andQueryTierMetricsLedgerEventDistiller,HeuristicDistiller,ProviderDistiller,DistillationProvider,DistillationPrompt,DistillationRepairPrompt,DistillationReplayKey,DistillOutcome, andDistillMetricsActiveReconstructor,DeterministicReconstructor,ProviderReconstructor,CommandReconstructionProvider,CommandReconstructionProviderConfig,ReconstructionProvider,ReconstructorConfig,RuntimeReconstructor,ReconstructionCandidate,ReconstructionDecision,ReconstructionDecisionOutcome,ReconstructionMetrics, andReconstructionStepMemoryQueryandMemoryAnswerMemoryTier,MemoryMode,MemoryNodeKind,MemoryEdgeKind,BeliefRevisionOp,DistilledEdge,RoutingReason,RoutingReport,ReconstructionMode,ReconstructionOptions,ReconstructionReason, andReconstructionReportEVAL_CONTRACT_VERSION,EvalSuite,EvalSuiteSource,EvalCase,EvalEvent,EvalOptions,EvalRuntimeOptions,EvalReport,EvalReportSource,EvalExpectationReport,EvalAbility,EvalScoreKind,EvalAbilitySummary,EvalTierSummary,run_eval_suite, andrun_eval_suite_with_source, andrun_eval_suite_with_source_and_runtime- import helpers for
beater.jsjournals and canonical JSONL - evidence token budgeting helpers
Run checks:
cargo fmt --all --check
cargo test
scripts/e2e-memory.sh
cargo clippy --workspace --all-targets -- -D warnings- Memory is
write / manage / read, not justretrieve. - The write hot path should stay append-only and robust.
projectremains a compatibility alias for manage-time projection; new code should prefermanagewhen it is intentionally distilling pending ledger observations.- Provider-backed distillation must happen outside projection write
transactions, use constrained schemas with snake_case JSON values such as
"add"and"invalidate", repair or reject malformed output, and emit provider/repair/rejection counters before writing projections. Provider relation edges must target scoped neighbor memories and are stored with source event provenance for bitemporal graph traversal. - Retrieval returns an answer-shaped evidence bundle, not raw chunks.
- Token cost, read latency, write amplification, and context pollution are first-class metrics.
- Evaluation reports use versioned deterministic content expectation checks or future calibrated judges; they do not use F1/BLEU as the correctness signal. Retrieval-tier expectations are hard gates that zero the effective case score when they fail, context-saturation gap is the clamped full-context shortfall, and selected evidence tokens are reported as the answer-context token load.
- Contradictions are graph edges and stale assumptions, not silently overwritten summaries.
For each coherent feature slice: implement, self-review the diff, run focused tests plus the workspace checks, commit only intended files, open a PR, and merge only after local checks and GitHub CI are clean.
beater-memory is part of the ecosystem — a family of Rust-first, local-first agent-infrastructure projects. It is fully standalone: a local Rust app and library that turns any ledgered agent traces into typed temporal memory with provenance. Within the family its shipped protocol boundary is:
- importing beater.js journals (
.beater/journal.db) and canonical span JSONL — this integration exists today
It is designed to serve as a memory backend for agents running in beater.js or under beaterOS once a real consumer lands on both sides; that future connection stays over protocol/API boundaries, not shared internal crates.