Context Forge
A local-first persistent memory library for LLM applications. turso (async SQLite) + standalone Tantivy BM25 retrieval, recency-decay scoring, and token-budget-aware context assembly — no cloud dependency, fully async.
Embed it in a bot, agent runtime, or MCP server that needs durable, searchable memory across sessions.
Installation
Pre-release notice: the current release is a
0.5.0-betapre-release (see crates.io for the latest beta). The public API may still change between betas based on integration feedback. Pin the exact version — caret ranges and the bare version string Cargo writes by default never match pre-release versions, so an exact pin is required. Replace<latest-beta>below with the current version:= "=0.5.0-<latest-beta>"
or in Cargo.toml:
[]
= "=0.5.0-<latest-beta>"
Quick start
use ;
use PathBuf;
async
Run the full version with cargo run --example basic (see
examples/basic.rs).
The default db_path is :memory: — an in-memory database that disappears
when the ContextForge instance is dropped. Set a real filesystem path for
durable storage.
Feature flags
| Feature | Default | Pulls in | Status |
|---|---|---|---|
analysis |
yes | stop-words |
Importance-detection pipeline — tokenizer, lexicon, n-grams, recurrence, classification, scoring. |
parallel |
no | rayon |
Opt-in rayon parallelism for the analysis pipeline (per-session term maps, classification, scoring). The library never configures the global rayon pool. |
distill-http |
no | reqwest |
OpenAI-compatible local-LLM distillation (Ollama/llama-server). |
Chunked distillation
ChunkingDistiller wraps any Distiller and bounds the size of the prompt
sent to the model on each call. A long transcript is split into
budget-sized pieces, each piece is distilled independently, and the partial
results are merged into one DistilledMemory:
use ;
let distiller = new
.with_reduce_strategy; // the default
max_chunk_chars is caller policy — this crate has no opinion on what a
safe prompt size is for any particular model or host; it only knows how to
split, map, and reduce once given a budget. ChunkingDistiller is
model-agnostic (it wraps any Distiller, including a hand-rolled one) and
needs no feature flags — it works the same with or without distill-http.
merge_distilled and split_on_budget, the pieces ChunkingDistiller is
built from, are also exported directly for callers who want custom
split/merge logic.
See examples/chunked_distill.rs for a
runnable, no-network example.
Runtime requirement
This crate is fully async — all public methods on ContextForge return
futures and must be .awaited. A tokio runtime is required. The
distill-http feature additionally requires the multi-thread flavor
(#[tokio::main] or tokio::runtime::Builder::new_multi_thread()) because
distill_and_save uses tokio::task::block_in_place internally.
ContextForge is Send + Sync and can be shared across tasks directly:
use Arc;
let cf = new;
// share across tokio tasks — no spawn_blocking needed
let hits = cf.query.await?;
Security
Save-time secret scrubbing
ContextForge::save passes content through scrub_secrets before it is
persisted, using the ScrubConfig in Config::scrub. This redacts common
credential formats — cloud provider keys, GitHub/Slack/Discord tokens,
Anthropic/OpenAI keys, PEM private key blocks, JWTs, and bearer tokens — with
[REDACTED:<label>] placeholders before they reach the database or the
search index.
Scrubbing is on by default. Disable it via:
use ;
let config = Config ;
This is an explicit, non-silent opt-out — you are asserting that content
will never contain secrets, or that you have your own scrubbing in place.
Note:
SaveOptions::metadatais stored verbatim and is not scrubbed. Do not place untrusted or secret-bearing text there.- Scrubbing happens only in
ContextForge::save. The lower-levelContextEngine::save_snapshotand theContextStoragetrait persistcontentas-is — callers who write through those paths directly are responsible for scrubbing first.
Untrusted-memory doctrine
Retrieved entries are untrusted text. Anything saved into the store —
including conversation history, tool output, or text from another user — can
contain adversarial instructions (stored prompt injection), and comes back
out verbatim from ContextForge::query (aside from save-time secret
scrubbing above).
Callers MUST present retrieved memory to models as quoted data — e.g. inside a fenced or otherwise clearly delimited block labeled as history — never as system-level instructions, and MUST NOT execute or evaluate anything found in it.
Architecture
engine—ContextEngine::assemble: BM25 search via theSearchertrait, then recency decay (score * 0.5^(age_seconds / half_life), default half-life 259,200s / 72h, configurable viaConfig), then sort by weighted score descending, then greedy bin-pack into the token budget. Oversized entries are skipped, not aborting. Also ownssave_snapshot. No I/O.storage— turso (async SQLite) for persistence, standalone Tantivy for in-memory BM25 indexing. Dual-write on save: turso commits to disk, tantivy updates the in-memory index. On open, the tantivy index is rebuilt from turso (linear startup cost, negligible for small corpora). turso is the source of truth; tantivy is a derived index.analysis(featureanalysis) — importance-detection pipeline (tokenizer, lexicon, n-grams, scoring). Pure computation, no I/O.scrub— secret-scrubbing patterns andscrub_secrets. Pure, no I/O.
Entries carry a scope field (e.g. "discord:thread:42",
"project:homelab-rs") for namespace partitioning; scope = None is global.
ContextForge::query(query, scope, token_budget) restricts the search to
scope when given, or searches everything when scope is None.
Status
All features implemented and tested: single-crate layout, scoped data model,
the ContextForge async public API facade, real BM25 scoring via standalone
Tantivy, save-time secret scrubbing, optional rayon parallelism (parallel),
and local-LLM distillation via an OpenAI-compatible endpoint (distill-http).
Live-validated against a Discord bot (Husk) across save/recall, BM25 ranking, restart persistence, scope isolation, and secret-scrubbing test scenarios.
Storage is turso (async SQLite) + standalone Tantivy — rusqlite, r2d2, and
FTS5 have been removed. This is a breaking API change from 0.5.x: all public
methods are now async.