use base64::Engine;
use crate::billing::{self, BillingCache};
use crate::crypto::{self, DerivedKeys};
use crate::embedding::{self, EmbeddingMode, EmbeddingProvider};
use crate::hotcache::HotCache;
use crate::lsh::LshHasher;
use crate::reranker::{self, Candidate, RerankerConfig};
use crate::relay::{RelayClient, RelayConfig};
use crate::search;
use crate::store;
use crate::wallet;
use crate::Result;
use totalreclaw_core::claims::MemorySource;
const DEFAULT_RELAY_URL: &str = "https://api.totalreclaw.xyz";
struct DecryptedEnvelope {
text: String,
category: MemoryCategory,
v1_source: Option<MemorySource>,
}
fn parse_decrypted_envelope(decrypted: &str) -> DecryptedEnvelope {
if let Ok(obj) = serde_json::from_str::<serde_json::Value>(decrypted) {
let is_v1 = obj.get("text").is_some() && obj.get("type").is_some();
if is_v1 {
let text = obj
.get("text")
.and_then(|v| v.as_str())
.unwrap_or(decrypted)
.to_string();
let v1_source = obj
.get("source")
.and_then(|v| v.as_str())
.and_then(parse_v1_source);
let v1_type = obj
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("claim");
let category = v1_type_to_category(v1_type);
return DecryptedEnvelope {
text,
category,
v1_source,
};
}
let text = obj
.get("t")
.and_then(|v| v.as_str())
.unwrap_or(decrypted)
.to_string();
let source = obj.get("s").and_then(|v| v.as_str()).unwrap_or("");
let category = category_from_source(source);
return DecryptedEnvelope {
text,
category,
v1_source: None,
};
}
DecryptedEnvelope {
text: decrypted.to_string(),
category: MemoryCategory::Core,
v1_source: None,
}
}
fn parse_v1_source(raw: &str) -> Option<MemorySource> {
match raw {
"user" => Some(MemorySource::User),
"user-inferred" => Some(MemorySource::UserInferred),
"assistant" => Some(MemorySource::Assistant),
"external" => Some(MemorySource::External),
"derived" => Some(MemorySource::Derived),
_ => None,
}
}
fn v1_type_to_category(v1_type: &str) -> MemoryCategory {
match v1_type {
"episode" => MemoryCategory::Conversation,
"claim" | "preference" | "directive" | "commitment" | "summary" => {
MemoryCategory::Core
}
_ => MemoryCategory::Core,
}
}
fn category_from_source(source: &str) -> MemoryCategory {
let lower = source.to_lowercase();
if lower.contains("conversation") || lower.contains("episodic") {
MemoryCategory::Conversation
} else if lower.contains("daily") || lower.contains("context") {
MemoryCategory::Daily
} else {
MemoryCategory::Core
}
}
const AUTO_RECALL_TOP_K: usize = 8;
#[derive(Debug, Clone, PartialEq)]
pub enum MemoryCategory {
Core,
Daily,
Conversation,
Custom(String),
}
impl std::fmt::Display for MemoryCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MemoryCategory::Core => write!(f, "core"),
MemoryCategory::Daily => write!(f, "daily"),
MemoryCategory::Conversation => write!(f, "conversation"),
MemoryCategory::Custom(s) => write!(f, "{}", s),
}
}
}
#[derive(Debug, Clone)]
pub struct MemoryEntry {
pub id: String,
pub key: String,
pub content: String,
pub category: MemoryCategory,
pub timestamp: String,
pub session_id: Option<String>,
pub score: Option<f64>,
}
pub struct TotalReclawMemory {
keys: DerivedKeys,
lsh_hasher: LshHasher,
embedding_provider: Box<dyn EmbeddingProvider>,
relay: RelayClient,
private_key: [u8; 32],
hot_cache: std::sync::Mutex<HotCache>,
}
pub struct TotalReclawConfig {
pub mnemonic: String,
pub embedding_mode: EmbeddingMode,
pub embedding_dims: usize,
pub relay_url: String,
pub is_test: bool,
}
impl Default for TotalReclawConfig {
fn default() -> Self {
Self {
mnemonic: String::new(),
embedding_mode: EmbeddingMode::Ollama {
base_url: "http://localhost:11434".into(),
model: "nomic-embed-text".into(),
},
embedding_dims: 640,
relay_url: DEFAULT_RELAY_URL.into(),
is_test: false,
}
}
}
impl TotalReclawMemory {
pub async fn new(config: TotalReclawConfig) -> Result<Self> {
let keys = crypto::derive_keys_from_mnemonic(&config.mnemonic)?;
let lsh_seed = crypto::derive_lsh_seed(&config.mnemonic, &keys.salt)?;
let lsh_hasher = LshHasher::new(&lsh_seed, config.embedding_dims)?;
let embedding_provider =
embedding::create_provider(config.embedding_mode, config.embedding_dims)?;
let eth_wallet = wallet::derive_eoa(&config.mnemonic)?;
let private_key = eth_wallet.private_key;
let wallet_address =
wallet::resolve_smart_account_address(ð_wallet.address, "https://sepolia.base.org")
.await?;
let auth_key_hex = hex::encode(keys.auth_key);
let auth_key_hash = crypto::compute_auth_key_hash(&keys.auth_key);
let salt_hex = hex::encode(keys.salt);
let chain_id = if let Some(cache) = billing::read_cache() {
if cache.is_pro() { 100 } else { 84532 }
} else {
84532
};
let relay_config = RelayConfig {
relay_url: config.relay_url.clone(),
auth_key_hex: auth_key_hex.clone(),
wallet_address: wallet_address.clone(),
is_test: config.is_test,
chain_id,
};
let mut relay = RelayClient::new(relay_config);
let _user_id = relay
.register(&auth_key_hash, &salt_hex)
.await
.ok();
if let Ok(status) = relay.billing_status().await {
if status.tier.as_deref() == Some("pro") {
relay.set_chain_id(100);
}
}
Ok(Self {
keys,
lsh_hasher,
embedding_provider,
relay,
private_key,
hot_cache: std::sync::Mutex::new(HotCache::new()),
})
}
pub fn wallet_address(&self) -> &str {
self.relay.wallet_address()
}
pub fn relay(&self) -> &RelayClient {
&self.relay
}
pub fn keys(&self) -> &DerivedKeys {
&self.keys
}
pub fn private_key(&self) -> &[u8; 32] {
&self.private_key
}
pub fn name(&self) -> &str {
"totalreclaw"
}
pub async fn store(
&self,
_key: &str,
content: &str,
category: MemoryCategory,
_session_id: Option<&str>,
) -> Result<()> {
self.store_with_importance(_key, content, category, _session_id, 10.0)
.await
}
pub async fn store_with_importance(
&self,
_key: &str,
content: &str,
category: MemoryCategory,
_session_id: Option<&str>,
importance: f64,
) -> Result<()> {
let source = format!("zeroclaw_{}", category);
store::store_fact_with_importance(
content,
&source,
importance,
&self.keys,
&self.lsh_hasher,
self.embedding_provider.as_ref(),
&self.relay,
Some(&self.private_key),
)
.await?;
if let Ok(mut cache) = self.hot_cache.lock() {
cache.clear();
}
Ok(())
}
pub async fn store_batch(
&self,
facts: &[(&str, &str)], ) -> Result<Vec<String>> {
let result = store::store_fact_batch(
facts,
&self.keys,
&self.lsh_hasher,
self.embedding_provider.as_ref(),
&self.relay,
&self.private_key,
)
.await?;
if let Ok(mut cache) = self.hot_cache.lock() {
cache.clear();
}
Ok(result)
}
pub async fn recall(
&self,
query: &str,
limit: usize,
_session_id: Option<&str>,
) -> Result<Vec<MemoryEntry>> {
let query_embedding = self.embedding_provider.embed(query).await?;
if let Ok(cache) = self.hot_cache.lock() {
if let Some(cached_results) = cache.lookup(&query_embedding) {
return Ok(cached_results);
}
}
let word_trapdoors = crate::blind::generate_blind_indices(query);
let embedding_f64: Vec<f64> = query_embedding.iter().map(|&f| f as f64).collect();
let lsh_trapdoors = self.lsh_hasher.hash(&embedding_f64)?;
let mut all_trapdoors = word_trapdoors;
all_trapdoors.extend(lsh_trapdoors.into_iter());
let billing_cache = billing::read_cache();
let max_candidates = billing::get_max_candidate_pool(billing_cache.as_ref());
let mut candidates = search::search_candidates(
&self.relay,
self.relay.wallet_address(),
&all_trapdoors,
max_candidates,
)
.await?;
let broadened = search::search_broadened(
&self.relay,
self.relay.wallet_address(),
max_candidates,
)
.await
.unwrap_or_default();
let mut seen: std::collections::HashSet<String> =
candidates.iter().map(|c| c.id.clone()).collect();
for fact in broadened {
if !seen.contains(&fact.id) {
seen.insert(fact.id.clone());
candidates.push(fact);
}
}
let mut rerank_candidates = Vec::new();
for fact in &candidates {
let blob_b64 = match search::hex_blob_to_base64(&fact.encrypted_blob) {
Some(b) => b,
None => continue,
};
let decrypted = match crypto::decrypt(&blob_b64, &self.keys.encryption_key) {
Ok(t) => t,
Err(_) => continue,
};
let envelope = parse_decrypted_envelope(&decrypted);
let text = envelope.text;
let mut emb = fact
.encrypted_embedding
.as_deref()
.and_then(|e| crypto::decrypt(e, &self.keys.encryption_key).ok())
.and_then(|b64| {
base64::engine::general_purpose::STANDARD
.decode(&b64)
.ok()
})
.map(|bytes| {
bytes
.chunks_exact(4)
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect::<Vec<f32>>()
})
.unwrap_or_default();
let expected_dims = self.embedding_provider.dimensions();
if !emb.is_empty() && emb.len() != expected_dims {
match self.embedding_provider.embed(&text).await {
Ok(fresh) => emb = fresh,
Err(_) => emb = Vec::new(),
}
}
rerank_candidates.push(Candidate {
id: fact.id.clone(),
text: text.clone(),
embedding: emb,
timestamp: fact.timestamp.clone().unwrap_or_default(),
source: envelope.v1_source,
});
}
let ranked = reranker::rerank_with_config(
query,
&query_embedding,
&rerank_candidates,
limit,
RerankerConfig {
apply_source_weights: true,
},
)?;
let results: Vec<MemoryEntry> = ranked
.into_iter()
.map(|r| MemoryEntry {
id: r.id.clone(),
key: r.id,
content: r.text,
category: MemoryCategory::Core, timestamp: r.timestamp,
session_id: None,
score: Some(r.score),
})
.collect();
if let Ok(mut cache) = self.hot_cache.lock() {
cache.insert(query_embedding, results.clone());
}
Ok(results)
}
pub async fn auto_recall(&self, query: &str) -> Result<Vec<MemoryEntry>> {
self.recall(query, AUTO_RECALL_TOP_K, None).await
}
pub async fn get(&self, key: &str) -> Result<Option<MemoryEntry>> {
let results = self.recall(key, 1, None).await?;
Ok(results.into_iter().next())
}
pub async fn list(
&self,
_category: Option<&MemoryCategory>,
_session_id: Option<&str>,
) -> Result<Vec<MemoryEntry>> {
let facts = search::fetch_all_facts(&self.relay, self.relay.wallet_address()).await?;
let mut entries = Vec::new();
for fact in facts {
let blob_b64 = match search::hex_blob_to_base64(&fact.encrypted_blob) {
Some(b) => b,
None => continue,
};
let decrypted = match crypto::decrypt(&blob_b64, &self.keys.encryption_key) {
Ok(t) => t,
Err(_) => continue,
};
let envelope = parse_decrypted_envelope(&decrypted);
entries.push(MemoryEntry {
id: fact.id.clone(),
key: fact.id,
content: envelope.text,
category: envelope.category,
timestamp: fact.timestamp.unwrap_or_default(),
session_id: None,
score: None,
});
}
Ok(entries)
}
pub async fn forget(&self, key: &str) -> Result<bool> {
store::store_tombstone_v1(key, &self.relay, Some(&self.private_key)).await?;
Ok(true)
}
pub async fn store_v1(&self, input: &store::V1StoreInput) -> Result<String> {
let fact_id = store::store_fact_v1(
input,
&self.keys,
&self.lsh_hasher,
self.embedding_provider.as_ref(),
&self.relay,
Some(&self.private_key),
)
.await?;
if let Ok(mut cache) = self.hot_cache.lock() {
cache.clear();
}
Ok(fact_id)
}
pub async fn pin(&self, _memory_id: &str) -> Result<()> {
Err(crate::Error::Crypto(
"pin: not yet implemented in ZeroClaw — use the MCP totalreclaw_pin tool \
from your agent. See CLAUDE.md Known Gaps."
.into(),
))
}
pub async fn retype(
&self,
_memory_id: &str,
_new_type: MemorySource,
) -> Result<()> {
Err(crate::Error::Crypto(
"retype: not yet implemented in ZeroClaw — use the MCP \
totalreclaw_retype tool. See CLAUDE.md Known Gaps."
.into(),
))
}
pub async fn set_scope(
&self,
_memory_id: &str,
_new_scope: totalreclaw_core::claims::MemoryScope,
) -> Result<()> {
Err(crate::Error::Crypto(
"set_scope: not yet implemented in ZeroClaw — use the MCP \
totalreclaw_set_scope tool. See CLAUDE.md Known Gaps."
.into(),
))
}
pub async fn count(&self) -> Result<usize> {
search::count_facts(&self.relay, self.relay.wallet_address()).await
}
pub async fn health_check(&self) -> bool {
self.relay.health_check().await.unwrap_or(false)
}
pub async fn status(&self) -> Result<crate::relay::BillingStatus> {
self.relay.billing_status().await
}
pub async fn billing_cache(&self) -> Result<BillingCache> {
billing::fetch_billing_status(&self.relay).await
}
pub async fn quota_warning(&self) -> Option<String> {
let cache = billing::fetch_billing_status(&self.relay).await.ok()?;
cache.quota_warning_message()
}
pub async fn debrief(&self, items: &[crate::debrief::DebriefItem]) -> Result<usize> {
let mut stored = 0;
for item in items.iter().take(crate::debrief::MAX_DEBRIEF_ITEMS) {
let importance = item.importance as f64;
store::store_fact_with_importance(
&item.text,
crate::debrief::DEBRIEF_SOURCE,
importance,
&self.keys,
&self.lsh_hasher,
self.embedding_provider.as_ref(),
&self.relay,
Some(&self.private_key),
)
.await?;
stored += 1;
}
if let Ok(mut cache) = self.hot_cache.lock() {
cache.clear();
}
Ok(stored)
}
pub async fn export(&self) -> Result<Vec<MemoryEntry>> {
self.list(None, None).await
}
pub async fn upgrade(&self) -> Result<String> {
self.relay.create_checkout().await
}
}