diff --git a/Cargo.lock b/Cargo.lock
index b04ef90..93d2938 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4840,6 +4840,7 @@ dependencies = [
"opentelemetry",
"opentelemetry-otlp",
"opentelemetry_sdk",
+ "parking_lot",
"pdf-extract",
"probe-rs",
"prometheus",
diff --git a/Cargo.toml b/Cargo.toml
index 2c314c3..cc60b72 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -60,6 +60,9 @@ hex = "0.4"
# CSPRNG for secure token generation
rand = "0.8"
+# Fast mutexes that don't poison on panic
+parking_lot = "0.12"
+
# Landlock (Linux sandbox) - optional dependency
landlock = { version = "0.4", optional = true }
diff --git a/README.md b/README.md
index 1faf4eb..4eb140b 100644
--- a/README.md
+++ b/README.md
@@ -508,6 +508,17 @@ ZeroClaw is an open-source project maintained with passion. If you find it usefu
+### ๐ Special Thanks
+
+A heartfelt thank you to the communities and institutions that inspire and fuel this open-source work:
+
+- **Harvard University** โ for fostering intellectual curiosity and pushing the boundaries of what's possible.
+- **MIT** โ for championing open knowledge, open source, and the belief that technology should be accessible to everyone.
+- **Sundai Club** โ for the community, the energy, and the relentless drive to build things that matter.
+- **The World & Beyond** ๐โจ โ to every contributor, dreamer, and builder out there making open source a force for good. This is for you.
+
+We're building in the open because the best ideas come from everywhere. If you're reading this, you're part of it. Welcome. ๐ฆโค๏ธ
+
## License
MIT โ see [LICENSE](LICENSE)
@@ -524,6 +535,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md). Implement a trait, submit a PR:
- New `Tunnel` โ `src/tunnel/`
- New `Skill` โ `~/.zeroclaw/workspace/skills//`
+
---
**ZeroClaw** โ Zero overhead. Zero compromise. Deploy anywhere. Swap anything. ๐ฆ
diff --git a/src/config/schema.rs b/src/config/schema.rs
index 64548e7..24e510c 100644
--- a/src/config/schema.rs
+++ b/src/config/schema.rs
@@ -743,6 +743,28 @@ pub struct MemoryConfig {
/// Max tokens per chunk for document splitting
#[serde(default = "default_chunk_size")]
pub chunk_max_tokens: usize,
+
+ // โโ Response Cache (saves tokens on repeated prompts) โโโโโโ
+ /// Enable LLM response caching to avoid paying for duplicate prompts
+ #[serde(default)]
+ pub response_cache_enabled: bool,
+ /// TTL in minutes for cached responses (default: 60)
+ #[serde(default = "default_response_cache_ttl")]
+ pub response_cache_ttl_minutes: u32,
+ /// Max number of cached responses before LRU eviction (default: 5000)
+ #[serde(default = "default_response_cache_max")]
+ pub response_cache_max_entries: usize,
+
+ // โโ Memory Snapshot (soul backup to Markdown) โโโโโโโโโโโโโ
+ /// Enable periodic export of core memories to MEMORY_SNAPSHOT.md
+ #[serde(default)]
+ pub snapshot_enabled: bool,
+ /// Run snapshot during hygiene passes (heartbeat-driven)
+ #[serde(default)]
+ pub snapshot_on_hygiene: bool,
+ /// Auto-hydrate from MEMORY_SNAPSHOT.md when brain.db is missing
+ #[serde(default = "default_true")]
+ pub auto_hydrate: bool,
}
fn default_embedding_provider() -> String {
@@ -778,6 +800,12 @@ fn default_cache_size() -> usize {
fn default_chunk_size() -> usize {
512
}
+fn default_response_cache_ttl() -> u32 {
+ 60
+}
+fn default_response_cache_max() -> usize {
+ 5_000
+}
impl Default for MemoryConfig {
fn default() -> Self {
@@ -795,6 +823,12 @@ impl Default for MemoryConfig {
keyword_weight: default_keyword_weight(),
embedding_cache_size: default_cache_size(),
chunk_max_tokens: default_chunk_size(),
+ response_cache_enabled: false,
+ response_cache_ttl_minutes: default_response_cache_ttl(),
+ response_cache_max_entries: default_response_cache_max(),
+ snapshot_enabled: false,
+ snapshot_on_hygiene: false,
+ auto_hydrate: true,
}
}
}
diff --git a/src/cron/mod.rs b/src/cron/mod.rs
index 4fe0c39..cddc134 100644
--- a/src/cron/mod.rs
+++ b/src/cron/mod.rs
@@ -422,6 +422,16 @@ fn with_connection(config: &Config, f: impl FnOnce(&Connection) -> Result)
let conn = Connection::open(&db_path)
.with_context(|| format!("Failed to open cron DB: {}", db_path.display()))?;
+ // โโ Production-grade PRAGMA tuning โโโโโโโโโโโโโโโโโโโโโโ
+ conn.execute_batch(
+ "PRAGMA journal_mode = WAL;
+ PRAGMA synchronous = NORMAL;
+ PRAGMA mmap_size = 8388608;
+ PRAGMA cache_size = -2000;
+ PRAGMA temp_store = MEMORY;",
+ )
+ .context("Failed to set cron DB PRAGMAs")?;
+
conn.execute_batch(
"PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
diff --git a/src/memory/mod.rs b/src/memory/mod.rs
index b04e0df..f012c27 100644
--- a/src/memory/mod.rs
+++ b/src/memory/mod.rs
@@ -5,6 +5,8 @@ pub mod hygiene;
pub mod lucid;
pub mod markdown;
pub mod none;
+pub mod response_cache;
+pub mod snapshot;
pub mod sqlite;
pub mod traits;
pub mod vector;
@@ -17,6 +19,7 @@ pub use backend::{
pub use lucid::LucidMemory;
pub use markdown::MarkdownMemory;
pub use none::NoneMemory;
+pub use response_cache::ResponseCache;
pub use sqlite::SqliteMemory;
pub use traits::Memory;
#[allow(unused_imports)]
@@ -63,6 +66,32 @@ pub fn create_memory(
tracing::warn!("memory hygiene skipped: {e}");
}
+ // If snapshot_on_hygiene is enabled, export core memories during hygiene.
+ if config.snapshot_enabled && config.snapshot_on_hygiene {
+ if let Err(e) = snapshot::export_snapshot(workspace_dir) {
+ tracing::warn!("memory snapshot skipped: {e}");
+ }
+ }
+
+ // Auto-hydration: if brain.db is missing but MEMORY_SNAPSHOT.md exists,
+ // restore the "soul" from the snapshot before creating the backend.
+ if config.auto_hydrate
+ && matches!(classify_memory_backend(&config.backend), MemoryBackendKind::Sqlite | MemoryBackendKind::Lucid)
+ && snapshot::should_hydrate(workspace_dir)
+ {
+ tracing::info!("๐งฌ Cold boot detected โ hydrating from MEMORY_SNAPSHOT.md");
+ match snapshot::hydrate_from_snapshot(workspace_dir) {
+ Ok(count) => {
+ if count > 0 {
+ tracing::info!("๐งฌ Hydrated {count} core memories from snapshot");
+ }
+ }
+ Err(e) => {
+ tracing::warn!("memory hydration failed: {e}");
+ }
+ }
+ }
+
fn build_sqlite_memory(
config: &MemoryConfig,
workspace_dir: &Path,
@@ -113,6 +142,35 @@ pub fn create_memory_for_migration(
)
}
+/// Factory: create an optional response cache from config.
+pub fn create_response_cache(
+ config: &MemoryConfig,
+ workspace_dir: &Path,
+) -> Option {
+ if !config.response_cache_enabled {
+ return None;
+ }
+
+ match ResponseCache::new(
+ workspace_dir,
+ config.response_cache_ttl_minutes,
+ config.response_cache_max_entries,
+ ) {
+ Ok(cache) => {
+ tracing::info!(
+ "๐พ Response cache enabled (TTL: {}min, max: {} entries)",
+ config.response_cache_ttl_minutes,
+ config.response_cache_max_entries
+ );
+ Some(cache)
+ }
+ Err(e) => {
+ tracing::warn!("Response cache disabled due to error: {e}");
+ None
+ }
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/src/memory/response_cache.rs b/src/memory/response_cache.rs
new file mode 100644
index 0000000..843b971
--- /dev/null
+++ b/src/memory/response_cache.rs
@@ -0,0 +1,371 @@
+//! Response cache โ avoid burning tokens on repeated prompts.
+//!
+//! Stores LLM responses in a separate SQLite table keyed by a SHA-256 hash of
+//! `(model, system_prompt_hash, user_prompt)`. Entries expire after a
+//! configurable TTL (default: 1 hour). The cache is optional and disabled by
+//! default โ users opt in via `[memory] response_cache_enabled = true`.
+
+use anyhow::Result;
+use chrono::{Duration, Local};
+use rusqlite::{params, Connection};
+use sha2::{Digest, Sha256};
+use std::path::{Path, PathBuf};
+use std::sync::Mutex;
+
+/// Response cache backed by a dedicated SQLite database.
+///
+/// Lives alongside `brain.db` as `response_cache.db` so it can be
+/// independently wiped without touching memories.
+pub struct ResponseCache {
+ conn: Mutex,
+ #[allow(dead_code)]
+ db_path: PathBuf,
+ ttl_minutes: i64,
+ max_entries: usize,
+}
+
+impl ResponseCache {
+ /// Open (or create) the response cache database.
+ pub fn new(workspace_dir: &Path, ttl_minutes: u32, max_entries: usize) -> Result {
+ let db_dir = workspace_dir.join("memory");
+ std::fs::create_dir_all(&db_dir)?;
+ let db_path = db_dir.join("response_cache.db");
+
+ let conn = Connection::open(&db_path)?;
+
+ conn.execute_batch(
+ "PRAGMA journal_mode = WAL;
+ PRAGMA synchronous = NORMAL;
+ PRAGMA temp_store = MEMORY;",
+ )?;
+
+ conn.execute_batch(
+ "CREATE TABLE IF NOT EXISTS response_cache (
+ prompt_hash TEXT PRIMARY KEY,
+ model TEXT NOT NULL,
+ response TEXT NOT NULL,
+ token_count INTEGER NOT NULL DEFAULT 0,
+ created_at TEXT NOT NULL,
+ accessed_at TEXT NOT NULL,
+ hit_count INTEGER NOT NULL DEFAULT 0
+ );
+ CREATE INDEX IF NOT EXISTS idx_rc_accessed ON response_cache(accessed_at);
+ CREATE INDEX IF NOT EXISTS idx_rc_created ON response_cache(created_at);",
+ )?;
+
+ Ok(Self {
+ conn: Mutex::new(conn),
+ db_path,
+ ttl_minutes: i64::from(ttl_minutes),
+ max_entries,
+ })
+ }
+
+ /// Build a deterministic cache key from model + system prompt + user prompt.
+ pub fn cache_key(model: &str, system_prompt: Option<&str>, user_prompt: &str) -> String {
+ let mut hasher = Sha256::new();
+ hasher.update(model.as_bytes());
+ hasher.update(b"|");
+ if let Some(sys) = system_prompt {
+ hasher.update(sys.as_bytes());
+ }
+ hasher.update(b"|");
+ hasher.update(user_prompt.as_bytes());
+ let hash = hasher.finalize();
+ format!("{:064x}", hash)
+ }
+
+ /// Look up a cached response. Returns `None` on miss or expired entry.
+ pub fn get(&self, key: &str) -> Result