| description | The vocabulary of the TinyCortex crate — memory entries, namespaces, categories, the security taint model, the Memory trait, and interaction-aware scoring and decay. |
|---|
This page defines the vocabulary of the TinyCortex crate: the shape of a stored
memory, how memories are partitioned by namespace and category, the
security taint model that travels with every entry, and how
interaction-aware scoring and decay shape retrieval. Every concept here is
grounded in real types from src/memory/types.rs, src/memory/traits.rs, and
src/memory/config.rs.
TinyCortex is a Rust library. The types below are the stable public contract shared across all layers (storage, ingest, retrieval, RPC). They are pure data with no storage side effects, and are ported faithfully from OpenHuman so the serialized wire formats (snake_case enum strings, serde defaults) stay byte-compatible.
The canonical record returned by recall and lookups is
MemoryEntry (src/memory/types.rs):
pub struct MemoryEntry {
pub id: String, // unique id (usually a UUID)
pub key: String, // key or title
pub content: String, // the memory body
pub namespace: Option<String>, // logical partition; None => global
pub category: MemoryCategory, // organizational category
pub timestamp: String, // ISO 8601 create / last-update
pub session_id: Option<String>, // optional session scope
pub score: Option<f64>, // optional relevance/confidence, ~0.0–1.0
pub taint: MemoryTaint, // provenance / trust signal
}MemoryEntry is the unit the Memory trait stores and
recalls. Deeper engine layers carry richer records for documents, KV rows, graph
edges, and ranked hits — StoredMemoryDocument, MemoryKvRecord,
GraphRelationRecord, and NamespaceMemoryHit — but MemoryEntry is the
high-level entry every backend speaks.
A retrieval hit can resolve to different underlying storage shapes. The
MemoryItemKind discriminator (wire strings in the table) tells you which:
| Variant | Wire string | Refers to |
|---|---|---|
Document |
document |
A namespace-scoped memory document (memory_docs row) |
Kv |
kv |
A key/value record |
Episodic |
episodic |
An episodic / conversational memory |
Event |
event |
A discrete event entry |
There are two timestamp conventions in the type surface, and the distinction matters when porting or serializing:
MemoryEntry.timestampis an ISO 8601 string for create / last-update.- Lower-level persisted records (
StoredMemoryDocument,MemoryKvRecord,GraphRelationRecord,NamespaceMemoryHit) carrycreated_at/updated_atasf64Unix timestamps in seconds. NamespaceSummary.last_updatedis anOption<String>RFC3339 timestamp for agent-side discovery.
updated_at feeds the freshness signal during ranking (see
Scoring and decay).
A namespace is a logical partition — think folders. The same key can exist
in multiple namespaces without collision, and recall is normally scoped to one
namespace to cut noise.
preferences → "User prefers dark mode", "Timezone is PST"
conversation-history → "Discussed Q3 roadmap on March 5"
user-facts → "Works at Acme Corp", "Based in Austin"
Namespaces give you:
- Separation of concerns — keep preferences apart from conversation logs apart from domain knowledge.
- Scoped queries — recall only what is relevant to the current task.
- Scoped deletion — clean up an entire category of memories without touching others.
When no explicit namespace is supplied, TinyCortex falls back to a single well-known default exported as a constant:
pub const GLOBAL_NAMESPACE: &str = "global";RecallOpts.namespace is Option<&str>; None falls back to
GLOBAL_NAMESPACE. At the storage layer, a None namespace on a
MemoryKvRecord or GraphRelationRecord denotes a global row / relation
rather than a namespace-scoped one — i.e. Option<String> namespaces encode
"global" as None.
Agents can enumerate what exists without scanning content via
Memory::namespace_summaries(), which returns NamespaceSummary rows:
pub struct NamespaceSummary {
pub namespace: String,
pub count: usize, // entries currently stored
pub last_updated: Option<String>, // RFC3339 of most recent update
}Every entry is filed under a MemoryCategory that captures its nature and
lifecycle (src/memory/types.rs):
| Variant | Wire / Display string |
Meaning |
|---|---|---|
Core |
core |
Long-term foundational facts, user preferences, permanent decisions |
Daily |
daily |
Temporal logs — daily activities or ephemeral state |
Conversation |
conversation |
Context derived from active conversations |
Custom(String) |
the inner name | A user- or system-defined custom category |
MemoryCategory serializes rename_all = "snake_case" and implements Display
(e.g. Custom("travel") renders as travel). Categories are a filter axis for
both Memory::list(..) and RecallOpts.category, orthogonal to namespaces:
namespaces partition where a memory lives, categories classify what kind it
is.
Every memory entry carries a provenance / trust signal called its taint. This is a security primitive, not just metadata: it drives downstream policy on whether automation whose context contains this content may invoke external-effect tools.
pub enum MemoryTaint {
Internal, // wire: "internal" — user-driven content
ExternalSync, // wire: "external_sync" — ingested from a third-party source
}Rules baked into the type (src/memory/types.rs):
- Default is
Internal. Legacy rows with no persisted taint column and all in-memory defaults are conservatively trusted as user-driven content. - Sync paths MUST set
ExternalSyncat write time. Any path that ingests text from third-party services (Gmail, Slack, Notion, Composio, MCP, …) is required to mark the content tainted so callers can refuse external-effect tools on tainted context. The trait exposesstore_with_taint(..)precisely for this. - Unknown decodes fail closed.
MemoryTaint::from_db_strmaps"internal"→Internal,"external_sync"→ExternalSync, and anything else →ExternalSync(the more restrictive value), so content of unknown provenance is treated as untrusted. - Serialization split. JSON uses snake_case (
internal/external_sync) via serde; the SQLitememory_docs.taintcolumn usesas_db_str()/from_db_str(), which happen to share the same strings.
Taint rides along on nearly every record in the type surface — MemoryEntry,
NamespaceDocumentInput, NamespaceQueryResult, StoredMemoryDocument, and
NamespaceMemoryHit all carry a #[serde(default)] taint field — so the
trust signal is preserved end-to-end from ingest through retrieval.
{% hint style="info" %}
TinyCortex defines and propagates the taint; it does not itself decide tool
policy. The host (OpenHuman or your application) reads the taint on recalled
context and decides whether to allow external-effect tools. See
Sources for where ExternalSync originates.
{% endhint %}
The core contract every backend implements is Memory
(src/memory/traits.rs), an async_trait. The methods most relevant to these
concepts:
#[async_trait]
pub trait Memory: Send + Sync {
fn name(&self) -> &str; // "sqlite", "vector", "in_memory", …
async fn store(&self, namespace: &str, key: &str, content: &str,
category: MemoryCategory, session_id: Option<&str>) -> Result<()>;
async fn store_with_taint(&self, namespace: &str, key: &str, content: &str,
category: MemoryCategory, session_id: Option<&str>,
taint: MemoryTaint) -> Result<()>; // default degrades to store()
async fn recall(&self, query: &str, limit: usize,
opts: RecallOpts<'_>) -> Result<Vec<MemoryEntry>>;
async fn get(&self, namespace: &str, key: &str) -> Result<Option<MemoryEntry>>;
async fn list(&self, namespace: Option<&str>, category: Option<&MemoryCategory>,
session_id: Option<&str>) -> Result<Vec<MemoryEntry>>;
async fn forget(&self, namespace: &str, key: &str) -> Result<bool>;
async fn namespace_summaries(&self) -> Result<Vec<NamespaceSummary>>;
async fn count(&self) -> Result<usize>;
async fn health_check(&self) -> bool;
}store_with_taint has a default that degrades to store for backends that do
not yet persist taint; recall_relevant_by_vector defaults to empty so
keyword-only or mock backends opt out cleanly. InMemoryMemoryStore is the
reliably runnable implementation today — see Getting Started.
recall takes a RecallOpts<'a> filter struct:
pub struct RecallOpts<'a> {
pub namespace: Option<&'a str>, // None => GLOBAL_NAMESPACE
pub category: Option<MemoryCategory>,
pub session_id: Option<&'a str>,
pub min_score: Option<f64>, // drop hits below this (~0.0–1.0)
pub cross_session: bool, // include other sessions' conversational hits
}min_score thresholds out weak hits; cross_session = true widens
conversational recall to other sessions in the same workspace alongside the
namespace recall.
Recall ranks candidate memories by combining several signals. The per-hit
explanation is captured in RetrievalScoreBreakdown (src/memory/types.rs),
which makes every score auditable:
pub struct RetrievalScoreBreakdown {
pub keyword_relevance: f64, // lexical / keyword match
pub vector_similarity: f64, // dense cosine similarity
pub graph_relevance: f64, // graph-proximity / co-occurrence
pub episodic_relevance: f64, // episodic recall
pub freshness: f64, // recency
pub final_score: f64, // weighted combination used for ranking
}NamespaceMemoryHit.score mirrors final_score, and each hit also carries the
full breakdown plus any supporting_relations that reinforced its ranking — so
retrieval is explainable rather than a black box.
How the signals combine is config-driven. WeightProfile
(src/memory/config.rs) weights graph / vector / keyword / freshness, and four
named presets ship as constants:
Profile (by_name) |
graph | vector | keyword | freshness |
|---|---|---|---|---|
balanced (default) |
0.35 | 0.35 | 0.15 | 0.15 |
semantic |
0.15 | 0.65 | 0.20 | 0.00 |
lexical |
0.25 | 0.15 | 0.60 | 0.00 |
graph_first |
0.55 | 0.30 | 0.15 | 0.00 |
WeightProfile::by_name resolves a wire name and falls back to BALANCED
for unknown names. RetrievalConfig.default_profile (default BALANCED) is
applied when a query specifies no profile. A freshness weight of 0.0
disables recency boosting for that profile. See Retrieval for how
profiles are selected per query.
TinyCortex applies a time-decay model inspired by the Ebbinghaus Forgetting Curve:
- New memories rank high — the freshness signal starts at 1.0.
- Aging memories decay — freshness halves every
half_life_days(7 by default), so importance at query time decreases with age. - Updated memories are refreshed — decay is computed from
updated_at, so writing to a memory resets its freshness clock. - Decayed memories effectively drop out of recall, keeping results lean without manual cleanup.
Note the mechanism precisely: decay is a stateless, query-time function of
updated_at — there is no persisted retention score that is written down over
time, and merely recalling a memory does not reinforce it. Interaction
weighting (authored/reply/dm/mention signals) shapes a memory's admission score
once, at ingest. In the type surface all of this appears as the freshness
signal and the freshness weight in the active profile.
The gitbook concept docs describe higher-level product behaviors —
prompt-driven "conscious recall" that returns an LLM-ready context string
alongside structured chunks. Within this crate, the closest real type is
NamespaceRetrievalContext, which pairs a rendered context_text with the
ranked hits that back it:
pub struct NamespaceRetrievalContext {
pub namespace: String,
pub query: Option<String>,
pub context_text: String, // ready-to-inject rendered context
pub hits: Vec<NamespaceMemoryHit>, // ranked hits backing the context
}{% hint style="info" %}
The turnkey "conscious recall" experience, managed APIs, the billion-token /
cost-per-user figures, and any hosted client SDK are part of the hosted
OpenHuman platform, not this open-source crate. TinyCortex provides the Rust
primitives (the Memory trait, scoring breakdowns, taint, namespaces) those
products are built on.
{% endhint %}
