The build proceeds inside-out: start at the storage layer, port the indexer to match the Python prototype's behaviour exactly, then wire transports, then live mode. Each milestone has a concrete acceptance check.
Crates: escurel-storage, escurel-md, escurel-index, escurel-embed (without
runtime — stub embeddings first).
LaneStoretrait +FsStoreimplementation (dev-only).S3Storeimplementation — required for the substrate deployment target (S3 is the production default per locked decision 7). The substrate-target cut-line ships S3 as GA, not as a feature flag.- Markdown parser (regex-based wikilink parser).
- Frontmatter parser (YAML; tolerant of the prototype's conventions).
- DuckDB schema migrations (
pages,links,blocks,crdt_ops,crdt_snapshots,frontmatter_index). vssandftsextension load + HNSW + BM25 index creation onblocks.- Pre-deployment retrieval-quality spike per
../adr/0001-duckdb-only-storage.md: build the 460-block evaluation harness against DuckDBvss+fts+ RRF fusion; run the 10,120-block synonym-mutant stress corpus; meet the acceptance table in the ADR's Pre-deployment gate section. Outcome gates M2 work. update_page,rebuild,auditrunning end-to-end.
Acceptance. Port the 28-assertion e2e test from the Python prototype to Rust. All 28 assertions PASS with timings within 2× of the prototype's.
Crates: escurel-embed proper, plus search / resolve / expand
/ neighbours / list_skills / list_instances /
run_stored_query over a direct Rust API (no transport yet).
- EmbeddingGemma loading via candle (CPU first; CUDA/Metal as feature flags).
- Gemini API adapter behind
embedding.provider = gemini. - Embed worker pool with the per-tenant queue.
- All seven read tools functional from a unit-test harness.
Acceptance. Agent-reproduction harness ports to Rust and produces the same 3/3 task success at ≤ 1.5 k tokens. EmbeddingGemma vs. the prototype's BGE-large quality difference logged but not gating.
Crates: escurel-server gateway, escurel-auth, escurel-quota.
- axum HTTP server with MCP/JSON-RPC framing.
- WebSocket endpoint (without live mode yet — just presence and search subscriptions stubbed out).
- OIDC verification with JWKS caching.
- Token-bucket quotas wired in.
escurelCLI as a thin MCP-over-HTTP client.
Acceptance. Run the cold-start verification through the CLI against a real running server. 8/8 queries correct under both policies. Validate OIDC against a Keycloak test instance.
Crates: escurel-crdt, plus the admin half of escurel-server.
- Loro adapter,
LiveDocactor per page, op log + snapshot persistence. - WebSocket op streaming.
open_session/apply_op/close_sessionover HTTP and the WebSocket op stream.- Admin endpoints: tenant CRUD, export/import, attach_external, audit/rebuild streaming.
- Two-stage reconciler for external markdown edits.
Acceptance. Two CLI processes concurrently edit the same
page over WS; merged state is consistent on both sides.
Kill the server mid-session; on restart the op log replays
cleanly and the next apply_op succeeds.
Crates: escurel-obs. Plus substrate-binding artefacts ship in this
milestone (S3Store has moved to M1).
- OTel traces + metrics + JSON logs wired everywhere.
/metricsPrometheus endpoint.- Failure injection tests covering the recovery matrix from
storage.md, including the cattle-node-loss → auto-rebuild-from-markdown path. - License audit re-run; deps frozen.
- End-to-end deploy doc.
- Substrate-target artefacts (per
../deploy/substrate.md): the repo container image (Dockerfile→ ghcr); the substrate-repo Kamal deploy contract (kamal/dz-escurel/deploy.yml— host-1 pin, STOP-FIRST,/dataVolume) +apps/registry.ymlrow (external build, exposure, Secret Manager secrets); inclusion in the substrate Volume backup.
Acceptance. End-to-end smoke against three deploys:
single-binary on a laptop (FS, no OTLP); systemd unit on a VM
(FS, OTLP to local Tempo/Prometheus); substrate nonprod
(S3 LaneStore = Hetzner Object Storage, OTLP to substrate
collector, cattle-node loss → auto-rebuild). All three pass the
prototype's e2e verification suite.
Cut a release. Tag v1.0.0. Publish operator docs.
escurel models memory as a triad — Events · Skills · Instances — where the current state of an instance is the projection of its event sequence, mediated by the skills that describe how to process each event. v1 ships the Skills + Instances legs and the event log via existing primitives; M7 makes the Event leg and the projection first-class, deliberately extending the v1 contract (each change ships with its spec/contract/ADR update):
- Events / inbox store. A dedicated
eventstable (a real inbox queue) tightly bound to the page model: each event'slabel_skilllinks to the skill that knows how to process it, andinstance_page_idlinks to the instance it belongs to once processed. Tools:capture_event,list_inbox,list_events,assign_event. (This is an intentional break of the v1 "events are ordinary instances; no new storage shape" invariant.) - Frontmatter-link indexing so an instance's frontmatter relations
(
about:/derived_from:) are real backlinks. - Historical state via CRDT snapshots:
expand(as_of=T | version)re-materializes an instance's frontmatter+body at a past instant (extends the v1 rule that markdown instances ignore@version). - Outbound webhook on new inbox items (the v1 transports are all inbound; this is a new platform surface).
- External-agent projection. The fold event→state is performed by an
external agent (using the event's
label_skillas context); the server stays automation-free, consistent with the v1 contract. The reference implementation is theescurel-demo-agentcrate — it reads the inbox (notified by the capture webhook, or polling), routes each event to its instance (pre-flag, else alabel_skill → instancetable), and folds it in viaassign_event. v1.5's in-server projection rules-engine remains out (see below).
The reference consumer is the escurel-explore event/instance workspace;
the reference processor is the escurel-demo-agent crate.
escurel evolves from a generic agent knowledge base into a persistent project-memory for data scientists/analysts, per ADR-0010. A long-running project holds two evolving networks — a knowledge graph (data → analysis → results → hypotheses) and an expectation graph (goals → priorities → constraints → success-criteria) — and most lost context comes from the expectation side. The contribution is a hypothesis-centric, expectation-aware memory that records why decisions were made, why paths were abandoned, and how expectations drifted.
Shipped as a non-breaking, opt-in layer:
- The
project-memoryskill pack — eleven first-class entity skills (Stakeholder, Goal, Expectation, Constraint, Priority, Success-Criterion, Hypothesis, Dataset, Analysis, Result, Decision) + an overview skill, distributed via the existing signed base-layer pack mechanism. Entities are ordinaryskillpages (thePageTypeenum is untouched); provenance relations are typed frontmatter wikilinks (derived_from,motivated_by,supersedes, …) whose kind is carried bylinks.src_field. - The
resolved_linksview — one derived DuckDB VIEW that resolves the slug-valuedlinks.dst_pageto a realpage_id(dropping danglers) and projects the relation kind. Rebuilt on every open; fully derivable from markdown per ADR-0001. - Four bounded read tools —
provenance_ancestry(multi-hop "everything this rests on / derives from it"),provenance_path(shortest path / reachability),expectation_drift(the cross-graph "decisions resting on a since-superseded expectation" query), andabandoned_paths(dead-ended branches). Parameterized (no agent SQL), depth-bounded, cycle-guarded, and fail-closed on ACL. - Automation-free (v1 contract preserved). escurel serves these queries; synthesising "the most promising next steps" is an external agent's job, not a server-side rules engine.
Backend. The engine runs on stock DuckDB recursive CTEs — zero new
dependency, the full query surface. A DuckPGQ (MATCH) backend sits behind
a reserved GraphBackend seam, gated on a go/no-go spike: DuckPGQ is a
per-DuckDB-version community extension, and no build exists for the pinned
DuckDB v1.5.3 (the spike 404s), so the MATCH backend is not built and
the feature is complete on CTEs. The #[ignore] spike re-checks on each
DuckDB bump; see
../notes/discovered/2026-07-31-duckpgq-unavailable-on-1.5.3.md.
In:
- All 12 agent tools from
../contract/agent-interface.md - Live CRDT mode + whole-page fallback on all three transports
- S3 LaneStore is the production default (Hetzner Object Storage as the reference substrate target); local FS retained as a dev-only convenience
- Gemini (
gemini-embedding-001) is the default embedder (binary ships thegeminifeature; keyless → zero-vector fallback so dev/CI/air-gapped boots stay clean) - EmbeddingGemma in candle (CPU; CUDA/Metal flags) as the air-gapped/local
provider (
provider = embeddinggemma) — what the substrate jobspec pins - Three quota dimensions (queries, writes+embeds, concurrent sessions)
- Admin API: tenant CRUD, export/import, rebuild, audit, attach_external, embedding_reload, compact_db, quota_get, health
- OTel + JSON logs +
/metrics escurelCLI (operator + agent-style)- Mandatory
escurelmeta-skill shipped with every new tenant - Event-typed skills supported via existing primitives;
at:denormalised to an indexed column on both the DuckDBpagesandblockstables;list_instancesacceptsorder_byand the operator-wrappedFilterClausesyntax - External instance backends — the
InstanceBackendtrait with three impls (markdown|sql_view|document).sql_view: read-only DuckDB views over postgres/mysql/sqlite/erpl/json_dir/parquet_dir, server-side credential registry,validate_bindings, ACL-before-fusion search.document: PDF/DOCX/PPTX/XLSX + text uploaded via/ingest//ingest/upload, extracted in-process by kreuzberg (default-on), chunked + embedded. Every external instance keeps a markdown overlay page (seeprotocol.md). Deferred to a future change request: row-grain SQL instances + write-back CRDT, multi-document instances, LLM-driven document processing, the LanceDB retrieval hatch - Remote (proxy) instance backends —
openapi+mcp: an instance is a live window onto a remote object, fetched live onexpand(no DuckDB copy) with optional write-back viawrite_instance.openapiproxies a REST/OpenAPI endpoint;mcpproxies an upstream MCP server (escurel is the client). Base URL + auth held in an adminexternal_endpointsregistry referenced by name (SSRF / secrets-in-markdown guard); remote data feeds no search lane (capabilities.search: "none"). Seeprotocol.md
Out (deferred):
- Federation across tenants
- Live cursors; v1 has presence badges only
- Sidecar embedding adapter (Ollama / TEI / vLLM) — the trait exists; concrete impl is post-v1
- Web admin UI — CLI only in v1
- Reranker beyond the small CE head bundled with
EmbeddingGemma; bge-reranker-large as a
--features rerankoption only - Auto-provision-on-first-request tenant flow
- Live search subscriptions (
search_subscribeover WS) — schema reserved, off behind a feature flag - Event-derived state projection (rules engine that maps events to state mutations automatically) — see below
- Direct measurement at 1 M instances; v1 extrapolates from 100 k measured
- Multi-model variance run of agent harness benchmarks across GPT and Gemini
Event-derived state projection. v1 records events and state
side-by-side without deriving one from the other. An author or
agent who creates a meeting instance with a
follow_ups: [[decision-record::expand-phoenix-scope]] link is
expected to also create the decision-record. Automatic
projection (a rule: "when meeting commits with follow_ups X,
upsert decision-record X with caused_by: meeting::Y") is on
the roadmap as v1.5. The design implication today: events and
state are both recorded, not derived. A future projection
rules layer would sit between the indexer and the live mode and
emit synthetic writes.
FTS retrieval quality. FTS is the most stress-sensitive
retrieval path. A synonym-mutant stress corpus is part of the
pre-deployment gate in
../adr/0001-duckdb-only-storage.md,
with a declared 0.60 nDCG target. If tokenizer tuning (stemmer
/ k1 / b) cannot close the gap, the fallback is not to
revert the consolidation; it is to keep DuckDB for vector +
relational + CRDT and attach a separate external FTS engine
(e.g. Tantivy) for the lexical column only. v1 ships DuckDB
FTS as the default with a config flag (retrieval.fts_backend = "duckdb" | "external") that switches to the external-engine
path.
FTS tokenizer tuning. Stemming + k1 + b tuning on a more
realistic distractor distribution is the prerequisite to
making fts_backend = "duckdb" viable for all tenants.
Tracked as a research item under the consolidation gate.
The v1 dep set (Rust crates):
| crate | license | notes |
|---|---|---|
object_store |
Apache-2.0 / MIT | from the Apache Arrow project |
duckdb (Rust bindings) |
MIT | bindings; DuckDB itself MIT; the vss and fts extensions are part of the DuckDB extension ecosystem under MIT |
loro |
MIT | CRDT |
candle-core, candle-nn, candle-transformers |
MIT/Apache-2.0 | HF inference runtime |
axum, hyper, tokio |
MIT | HTTP + runtime |
tower, tower-http |
MIT | middleware |
tracing, tracing-opentelemetry |
MIT | logs + traces |
opentelemetry, opentelemetry-otlp |
Apache-2.0 | OTel SDK |
prometheus |
Apache-2.0 | scrape endpoint |
jsonwebtoken |
MIT | JWT verification |
reqwest |
MIT/Apache-2.0 | HTTP client (Gemini adapter) |
serde, serde_json, serde_yaml, toml |
MIT/Apache-2.0 | (de)serialisation |
clap |
MIT/Apache-2.0 | CLI |
dashmap, lru |
MIT | data structures |
ulid |
MIT | page id generation |
regex, aho-corasick |
MIT/Apache-2.0 | wikilink parsing |
Permissive across the board. No GPL surface. Closed-source SaaS
operators can ship this without surfacing source. Embedding
model weights (google/embeddinggemma-300m) are governed by
the Gemma license, which permits commercial use including
hosted services subject to the prohibited-use policy — operators
should review independently if their use case is unusual.
The shape of the v1.5 / v2 directions:
- Event-derived state projection — a rules-engine layer
that materialises supersession / chain links from event
follow_upsfields. Optional per-tenant; off by default (the absence of automation is part of the v1 contract). - Live cursors and OT-grade collaboration — once the dependent libraries stabilise.
- Federation — query routing across tenants for an admin asking cross-tenant questions; behind a permission layer.
- Sidecar embedding adapter — TEI / vLLM / Ollama; gives GPU-resource-constrained operators a way to pool inference capacity.
- Reranker upgrade — bge-reranker-large by default; the CE head from EmbeddingGemma stays as the fast-path.
- Web admin UI — replaces the CLI for everyday operator tasks. The CLI stays for scripting.
- Multi-region S3 backends — read-replica datasets for geographic distribution; the writer remains single-region.
- Substrate-target package — a published container image +
reusable Kamal deploy/registry fragments, so substrate operators
consume
escurel-serveras a turnkey workload. Per-target binding docs grow as new substrate targets are adopted (managed-K8s, single-VM, etc.).
None of these break the v1 contract. The Rust crate layout was chosen with these extensions in mind: each lives in a new crate that depends on the existing ones, not in a refactor.