This repository is a working exploration of Datalevin as a backend for coding-agent memory exposed through an MCP server. The prototype focuses on a thin, honest vertical slice:
- structured EAV/Datalog memory with provenance
- MCP tools/resources that operate on high-value coding memory actions
- ingestion + normalization from realistic coding artifacts
- reproducible evaluation scenarios and report generation
The goal is to answer one question with evidence: should Datalevin be used for coding-agent memory behind MCP?
Datalevin gives:
- ACID local transactions on LMDB
- Datalog queries for relationship-heavy retrieval
- full-text search over normalized memory bodies
- flexible EAV schema evolution for a prototype phase
That combination is promising for coding memory where facts and relationships matter more than document blobs.
src/datalaga/memoryschema, transaction logic, query logicsrc/datalaga/mcp/server.cljMCP stdio server (tools + resources)src/datalaga/cli.cljstandalone CLI — all tools without MCP protocolsrc/datalaga/ingest.cljingestion and normalization pipelinesrc/datalaga/eval.cljevaluation harness + MCP smoke testsrc/datalaga/inspect.cljinspection CLI for debugging memorysrc/datalaga/maintenance.cljmaintenance CLI for normalization/backfillsrc/datalaga/memory/maintenance.cljshared normalization enginebuild.cljuberjar build script for native-image compilationexamples/seed-data.ednsynthetic but realistic linked coding-memory dataseteval/report.mdgenerated evaluation reportdocs/architecture.mdarchitecture details
- Clojure CLI
- Java 21+
- On macOS x86_64 only:
libomp(for Datalevin native dependency)
Use the wrapper script so Datalevin native loading works consistently:
./bin/clojure-with-dtlv ...
The wrapper:
- ensures local Maven cache under
.m2/repository - extracts required Datalevin native libraries into
.native/dtlvon macOS x86_64 - sets
DYLD_LIBRARY_PATHappropriately
If you run clojure directly and the native libraries are not configured, startup and tests can fail with errors like UnsatisfiedLinkError: no jniDTLV in java.library.path or Library not loaded: 'libdtlv.dylib'. In that case, rerun the command through ./bin/clojure-with-dtlv ... or another entrypoint that uses the wrapper.
The datalaga CLI exposes every MCP tool as a direct command. No JSON-RPC protocol — just command-line arguments and structured output.
# List all available tools
./bin/datalaga tools
# Get help for a specific tool
./bin/datalaga help ensure_project
# Create a project
./bin/datalaga ensure_project --project_id project:myapp --name "My App"
# List projects
./bin/datalaga list_projects
# Search entities
./bin/datalaga search_entities --project_id project:myapp --query "auth"
# Record a tool run
./bin/datalaga record_tool_run \
--run_id run:001 \
--project_id project:myapp \
--command "npm test" \
--exit_code 0
# Remember a fact
./bin/datalaga remember_fact \
--entity_id note:finding-1 \
--entity_type note \
--summary "Auth flow needs refactor" \
--project_id project:myapp
# Run a raw Datalog query
./bin/datalaga memory_query \
--query_edn '[:find [?id ...] :where [?e :entity/id ?id]]'
# Summarize project memory
./bin/datalaga summarize_project_memory --project_id project:myapp
# Output as JSON instead of pretty-printed EDN
./bin/datalaga list_projects -f json
# Pass arguments as a JSON object
./bin/datalaga ensure_project --json '{"project_id":"project:myapp","name":"My App"}'
# Read arguments from stdin
echo '{"project_id":"project:myapp"}' | ./bin/datalaga list_projects -Global options:
-d, --db-path PATH— Datalevin database path (default:.data/memory)-s, --seed-file FILE— Seed data file (default:examples/seed-data.edn)--seed-on-start— Seed the database before running the command-f, --format FORMAT— Output format:pretty(default),json,edn--json JSON— Pass tool arguments as a JSON object-h, --help
Hyphens and underscores are interchangeable in tool names (list-projects = list_projects).
Start MCP server (JSON-RPC over stdio):
./bin/start-mcp --db-path .data/memory --seed-file examples/seed-data.ednSeed on startup only when requested:
./bin/start-mcp --db-path .data/memory --seed-file examples/seed-data.edn --seed-on-startRun local demo/evaluation:
./bin/run-evalInspect memory directly:
./bin/inspect-memory summary project:phoenix-auth
./bin/inspect-memory task task:AUTH-142
./bin/inspect-memory symbol symbol:src/auth/session.clj#refresh-session
./bin/inspect-memory prior-decisions task:AUTH-160
./bin/inspect-memory --seed-before-run summary project:phoenix-authRun housekeeping normalization:
./bin/normalize-memory --project-id project:yoyo-evolve --mode dry_run
./bin/normalize-memory --project-id project:yoyo-evolve --mode apply --migration-id migration:v1
./bin/normalize-memory --seed-before-run --project-id project:yoyo-evolve --mode dry_runBuild a standalone native binary (requires GraalVM 25+ with native-image):
# For native-image builds, replace datalevin/datalevin with
# org.clojars.huahaiy/datalevin-native in deps.edn first.
./bin/build-nativeThis produces target/datalaga — a single binary that starts instantly, no JVM required.
# Run the native binary
./target/datalaga list_projects
./target/datalaga search_entities --project_id project:myapp --query "auth" -f jsonTo build just the uberjar (for JVM deployment without native-image):
clj -T:build uber
java -jar target/datalaga.jar list_projectsImplemented tools:
ensure_projectensure_tasklist_projectsupsert_code_entitysearch_entitiesremember_factrecord_eventrecord_tool_runrecord_errorlink_entitiessearch_notesfind_related_contextget_symbol_memoryget_task_timelinesummarize_project_memorymemory_query(raw Datalevin Datalog query via EDN payload)memory_pull(entity pull with optional EDN pull pattern)normalize_project_memory(admin maintenance)
Notable behavior:
record_tool_runinfers a readable run name fromcommandwhennameis omitted, setsentity/statusfromexit_code(successorfailed), and can track replacement lineage viasupersedes_run_ids/retries_of_run_ids.record_tool_runrejects compound shell commands (&&,||,;,|) so each run record maps to one executable command.record_tool_runacceptstouched_file_pathsin addition totouched_file_ids, auto-upsertsfile:*entities for those paths, and returnsnormalized_touched_file_refs.record_eventacceptssubject_file_paths, auto-upserts file entities for those paths, and returnsnormalized_subject_file_refs.record_erroracceptsrelated_file_paths, auto-upserts file entities for those paths, and returnsnormalized_related_file_refs.- path-based file normalization now defaults to project-scoped file ids:
file:<project_id>:<path>. search_entitiesapplies path-intent ranking for path-like queries (for examplescripts/evolve.sh), biasing file entities to the top when available.search_entities,search_notes, andfind_related_contextsupport pagination viaoffsetand boundedlimitdefaults to keep responses predictable for agent loops.search_entitiesreturns file-onboarding guidance on path-like file misses (suggested_file_id,suggested_action=upsert_code_entity,suggested_arguments) whenentity_type=fileand no file match exists.- when a path-like file miss corresponds to an existing global
file:*entity,search_entitiesalso returnsexisting_file_idandexisting_file_project_idto explain the miss and guide re-association/upsert. - when
record_tool_runis called without lineage fields, the server auto-links to the most recent matching run in the same project/task/session (auto_lineage_inferredin response). - write tools enforce strict argument validation; unsupported fields are rejected with remediation data.
- unsupported-argument rejections may include
did_you_meanremediation hints for common mistakes (for examplerecord_tool_run.status-> useexit_code). - tool errors include
error_typeandretry_bootstrap_recommendedto separate validation fixes from true bootstrap retries. - tool calls also enforce required argument presence (missing required args are rejected early).
- write transactions now prevalidate references before commit, preventing partial entity writes on missing refs.
ensure_tasknormalizes statuses (completed->done,in-progress->in_progress) and active task views treatdone|completed|closed|cancelledas non-active.list_projectsis sorted by recent project activity (latest entity update/create), and each project brief includesproject/last-activity-at.record_errordefaults toentity/status = openunless provided explicitly.link_entitieswithlink_type = resolved_byauto-marks the source error asresolvedand attaches the resolver run as a reference.link_entitiescan also model cross-project service topology explicitly (for examplelink_type = depends_onorlink_type = calls_apibetweenproject:*entities).summarize_project_memoryandmemory://project/{project_id}/recent-failuresexclude errors already markedresolved/closed.remember_factauto-normalizesattributes.files/attributes.file_pathsintofile:*refs, upserts missing file entities, and returnsnormalized_file_refs;external_refs(orattributes.external_refs) are normalized intoentity/external-refs.search_notessupportsproject_idsfor cross-project recall, andfind_related_contextacceptsproject_idsto broaden graph traversal beyond the anchor entity's project.memory_querynow supports boundedtimeout_ms,max_results, andoffset, returningtotal_count/truncatedmetadata for collection results.normalize_project_memorysupports project-scopeddry_run|applyhousekeeping with operation filters:normalize_entity_typesbackfill_error_resolutionlink_run_supersessionscope_file_ids_by_project(backfill project-scopedfile:<project_id>:<path>IDs and refs) and writes a maintenance event on apply for auditability.
Exposed resources:
memory://project/{project_id}/summarymemory://project/{project_id}/recent-failuresmemory://project/{project_id}/active-tasksmemory://session/{session_id}/timelinememory://task/{task_id}/timelinememory://symbol/{symbol_id}/dossier
Seed data includes linked entities for:
Project,File,SymbolTask,Session,ToolRun,Error,ObservationDecision,Patch,Note,Event,Link
Example relation chains in the dataset:
- failing test tool run -> error -> observation -> decision -> patch -> follow-up notes
- task touched files -> prior decisions related files -> context recommendation
- file contains symbol -> symbol related failures/patches/decisions/notes
Custom entity type policy:
- core types are stored as-is (for example
project,task,error) - non-core types with no namespace are promoted to
custom/<type>(for examplefinding->custom/finding) - non-core types with a namespace are kept as provided
- original custom type is retained in
entity/kind
Code entity onboarding flow (for non-seeded repos):
upsert_code_entityfor file/symbol anchors as you discover code structure.search_entitiesto discover valid IDs before creating refs inrecord_error/link_entities.find_related_context/get_symbol_memorybecome useful once anchors exist.
- exact entity lookup
- graph/EAV traversal around files/symbols/tasks
- full-text note/body search
- hybrid text + graph context retrieval
High-signal example:
- “What prior decisions touched files that this task is about to modify?”
- implemented in
prior-decisions-for-taskvia joins over task touched files and decision related files
- implemented in
Strengths:
- graph/EAV retrieval materially outperforms plain text for anchored coding workflows
- provenance is explicit and inspectable
- memory model is readable and extensible
Pain points:
- integration glue is still required between MCP payloads and normalized entities
- full-text alone underperforms graph traversal for relationship-heavy coding questions
Run ./bin/run-eval to regenerate:
eval/report.mdwith retrieval metrics by scenario- MCP smoke test evidence (tools/resources reachable via stdio protocol)
- recommendation and rationale
Use this snippet in a repo-level AGENTS.md to enforce consistent memory behavior:
## Required MCP Memory Policy (`datalaga`)
For all non-trivial coding tasks in this repository, you MUST use the `datalaga` MCP server.
### 1) Bootstrap
- At the start of work, call `list_projects`.
- Do not assume demo seed data exists; the server may run without `--seed-on-start`.
- If the current repo project is missing, call `ensure_project` with `project_id` and a short summary.
- If you will write task-scoped records (`task_id` on runs/events/errors/facts), call `ensure_task` first.
- `ensure_task` also updates an existing task’s mutable fields (`status`, `summary`, `priority`, `description`) when reused.
### 2) Read Before Acting
- Before edits or major analysis, call `summarize_project_memory`.
- When touching a symbol/file/task, load context first with:
- `get_symbol_memory` (symbol-focused work),
- `find_related_context` (file/entity neighborhood),
- `get_task_timeline` (task-focused work), as applicable.
- Use `memory_pull` for targeted entity inspection when high-level tools are insufficient.
- Use `memory_query` for advanced recall/debugging (EDN Datalog), and prefer high-level read tools by default.
### 3) Write During/After Work
- Record command executions with `record_tool_run` (build/test/lint/tool output).
- Use one command per `record_tool_run` entry; do not combine commands with `&&` in a single run record.
- Do not send unknown or incomplete tool arguments. MCP calls with unsupported fields or missing required fields are rejected.
- If you rerun the same command for a task/session, keep `command` stable so lineage inference can chain retries automatically (or pass explicit `supersedes_run_ids` / `retries_of_run_ids`).
- Prefer `touched_file_paths` when file ids are unknown; the server will normalize and upsert file refs for run logging.
- Prefer `subject_file_paths` / `related_file_paths` when event/error references include files but file ids are unknown.
- Record failures with `record_error` and link to related runs/symbols.
- Persist key findings/decisions with `remember_fact` (or `record_event` for timeline milestones).
- When a fact references files, include `attributes.files` or `attributes.file_paths`; the server will auto-upsert `file:*` entities and attach them via `entity/refs`.
- Create explicit causality with `link_entities` when useful (e.g. error -> decision -> patch).
### 4) End-of-Task Memory Writeback
- Before final response, store a concise evaluation event containing:
- what was checked,
- what failed/passed,
- risks,
- follow-up opportunities.
- If memory write fails, inspect `error_type` / `retry_bootstrap_recommended` in the error data:
- when `retry_bootstrap_recommended=true`, retry once after `list_projects`/`ensure_project` (and `ensure_task` if needed),
- when `false` (for example validation errors), fix arguments and retry directly without bootstrap steps.
### 5) Scope and Quality
- Keep `project_id` consistent for all writes in a session.
- Prefer structured fields over blob text.
- Do not skip memory logging for convenience.MIT. See LICENSE.