Context Forge
A local-first persistent memory library for LLM applications. SQLite + FTS5 BM25 retrieval, recency-decay scoring, and token-budget-aware context assembly — no network calls, no async runtime, no cloud dependency.
Embed it in a bot, agent runtime, or MCP server that needs durable, searchable memory across sessions.
Installation
Pre-release notice:
0.5.0-beta.1is a pre-release. The public API may still change between betas based on integration feedback. Pin the exact version — caret ranges (^0.5.0-beta.1, or the bare"0.5.0-beta.1"Cargo writes by default) never match pre-release versions across betas, so an exact pin is required either way:= "=0.5.0-beta.1"
or in Cargo.toml:
[]
= "=0.5.0-beta.1"
Quick start
use ;
use PathBuf;
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, scoring). Used internally for future ranking work. |
parallel |
no | rayon |
Reserved for Phase 4 (parallel scoring). Not yet implemented. |
distill-http |
no | reqwest |
OpenAI-compatible local-LLM distillation (Ollama/llama-server). |
Async callers
This crate is synchronous by design — it performs blocking SQLite I/O and
never spawns its own threads or runtime. Callers using an async runtime
(e.g. Tokio) should wrap calls in
spawn_blocking
and share a single ContextForge instance behind an Arc:
use Arc;
let cf = new;
// in an async context:
let hits = spawn_blocking.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— all SQL: rusqlite + r2d2 connection pool, WAL mode, FTS5 virtual table kept in sync via triggers, forward-only migrations (schema.rs). Current schema version is v3.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
This crate is mid-refactor from a Claude Code compaction-memory plugin into a
general-purpose library. Phases 0–5 are complete: single-crate layout, data
model generalization, public API facade, save-time secret scrubbing,
parallel (rayon-based parallel scoring), and distill-http (local-LLM
thread distillation via an OpenAI-compatible endpoint).
Published as 0.5.0-beta.1 — a pre-release for downstream integration
testing. Final 0.5.0 will follow once the API has proven itself in a real
downstream consumer; any breaking changes that integration surfaces will land
as additional betas (0.5.0-beta.2, ...) before the 0.5.0 cut.