feat(memory): lucid memory integration with optional backends (#285)

This commit is contained in:
Chummy 2026-02-17 00:31:50 +08:00 committed by GitHub
parent 04bf94443f
commit 53844f7207
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1089 additions and 137 deletions

View file

@ -5,6 +5,9 @@ use crate::config::{
RuntimeConfig, SecretsConfig, SlackConfig, TelegramConfig, WebhookConfig,
};
use crate::hardware::{self, HardwareConfig};
use crate::memory::{
default_memory_backend_key, memory_backend_profile, selectable_memory_backends,
};
use anyhow::{Context, Result};
use console::style;
use dialoguer::{Confirm, Input, Select};
@ -237,8 +240,38 @@ pub fn run_channels_repair_wizard() -> Result<Config> {
// ── Quick setup (zero prompts) ───────────────────────────────────
/// Non-interactive setup: generates a sensible default config instantly.
/// Use `zeroclaw onboard` or `zeroclaw onboard --api-key sk-... --provider openrouter --memory sqlite`.
/// Use `zeroclaw onboard` or `zeroclaw onboard --api-key sk-... --provider openrouter --memory sqlite|lucid`.
/// Use `zeroclaw onboard --interactive` for the full wizard.
fn backend_key_from_choice(choice: usize) -> &'static str {
selectable_memory_backends()
.get(choice)
.map_or(default_memory_backend_key(), |backend| backend.key)
}
fn memory_config_defaults_for_backend(backend: &str) -> MemoryConfig {
let profile = memory_backend_profile(backend);
MemoryConfig {
backend: backend.to_string(),
auto_save: profile.auto_save_default,
hygiene_enabled: profile.uses_sqlite_hygiene,
archive_after_days: if profile.uses_sqlite_hygiene { 7 } else { 0 },
purge_after_days: if profile.uses_sqlite_hygiene { 30 } else { 0 },
conversation_retention_days: 30,
embedding_provider: "none".to_string(),
embedding_model: "text-embedding-3-small".to_string(),
embedding_dimensions: 1536,
vector_weight: 0.7,
keyword_weight: 0.3,
embedding_cache_size: if profile.uses_sqlite_hygiene {
10000
} else {
0
},
chunk_max_tokens: 512,
}
}
#[allow(clippy::too_many_lines)]
pub fn run_quick_setup(
api_key: Option<&str>,
@ -265,36 +298,12 @@ pub fn run_quick_setup(
let provider_name = provider.unwrap_or("openrouter").to_string();
let model = default_model_for_provider(&provider_name);
let memory_backend_name = memory_backend.unwrap_or("sqlite").to_string();
let memory_backend_name = memory_backend
.unwrap_or(default_memory_backend_key())
.to_string();
// Create memory config based on backend choice
let memory_config = MemoryConfig {
backend: memory_backend_name.clone(),
auto_save: memory_backend_name != "none",
hygiene_enabled: memory_backend_name == "sqlite",
archive_after_days: if memory_backend_name == "sqlite" {
7
} else {
0
},
purge_after_days: if memory_backend_name == "sqlite" {
30
} else {
0
},
conversation_retention_days: 30,
embedding_provider: "none".to_string(),
embedding_model: "text-embedding-3-small".to_string(),
embedding_dimensions: 1536,
vector_weight: 0.7,
keyword_weight: 0.3,
embedding_cache_size: if memory_backend_name == "sqlite" {
10000
} else {
0
},
chunk_max_tokens: 512,
};
let memory_config = memory_config_defaults_for_backend(&memory_backend_name);
let config = Config {
workspace_dir: workspace_dir.clone(),
@ -2164,11 +2173,10 @@ fn setup_memory() -> Result<MemoryConfig> {
print_bullet("You can always change this later in config.toml.");
println!();
let options = vec![
"SQLite with Vector Search (recommended) — fast, hybrid search, embeddings",
"Markdown Files — simple, human-readable, no dependencies",
"None — disable persistent memory",
];
let options: Vec<&str> = selectable_memory_backends()
.iter()
.map(|backend| backend.label)
.collect();
let choice = Select::new()
.with_prompt(" Select memory backend")
@ -2176,20 +2184,16 @@ fn setup_memory() -> Result<MemoryConfig> {
.default(0)
.interact()?;
let backend = match choice {
1 => "markdown",
2 => "none",
_ => "sqlite", // 0 and any unexpected value defaults to sqlite
};
let backend = backend_key_from_choice(choice);
let profile = memory_backend_profile(backend);
let auto_save = if backend == "none" {
let auto_save = if !profile.auto_save_default {
false
} else {
let save = Confirm::new()
Confirm::new()
.with_prompt(" Auto-save conversations to memory?")
.default(true)
.interact()?;
save
.interact()?
};
println!(
@ -2199,21 +2203,9 @@ fn setup_memory() -> Result<MemoryConfig> {
if auto_save { "on" } else { "off" }
);
Ok(MemoryConfig {
backend: backend.to_string(),
auto_save,
hygiene_enabled: backend == "sqlite", // Only enable hygiene for SQLite
archive_after_days: if backend == "sqlite" { 7 } else { 0 },
purge_after_days: if backend == "sqlite" { 30 } else { 0 },
conversation_retention_days: 30,
embedding_provider: "none".to_string(),
embedding_model: "text-embedding-3-small".to_string(),
embedding_dimensions: 1536,
vector_weight: 0.7,
keyword_weight: 0.3,
embedding_cache_size: if backend == "sqlite" { 10000 } else { 0 },
chunk_max_tokens: 512,
})
let mut config = memory_config_defaults_for_backend(backend);
config.auto_save = auto_save;
Ok(config)
}
// ── Step 3: Channels ────────────────────────────────────────────
@ -4343,18 +4335,54 @@ mod tests {
}
#[test]
fn default_model_for_minimax_is_m2_5() {
assert_eq!(default_model_for_provider("minimax"), "MiniMax-M2.5");
fn backend_key_from_choice_maps_supported_backends() {
assert_eq!(backend_key_from_choice(0), "sqlite");
assert_eq!(backend_key_from_choice(1), "lucid");
assert_eq!(backend_key_from_choice(2), "markdown");
assert_eq!(backend_key_from_choice(3), "none");
assert_eq!(backend_key_from_choice(999), "sqlite");
}
#[test]
fn minimax_onboard_models_include_m2_variants() {
let model_names: Vec<&str> = MINIMAX_ONBOARD_MODELS
.iter()
.map(|(name, _)| *name)
.collect();
assert_eq!(model_names.first().copied(), Some("MiniMax-M2.5"));
assert!(model_names.contains(&"MiniMax-M2.1"));
assert!(model_names.contains(&"MiniMax-M2.1-highspeed"));
fn memory_backend_profile_marks_lucid_as_optional_sqlite_backed() {
let lucid = memory_backend_profile("lucid");
assert!(lucid.auto_save_default);
assert!(lucid.uses_sqlite_hygiene);
assert!(lucid.sqlite_based);
assert!(lucid.optional_dependency);
let markdown = memory_backend_profile("markdown");
assert!(markdown.auto_save_default);
assert!(!markdown.uses_sqlite_hygiene);
let none = memory_backend_profile("none");
assert!(!none.auto_save_default);
assert!(!none.uses_sqlite_hygiene);
let custom = memory_backend_profile("custom-memory");
assert!(custom.auto_save_default);
assert!(!custom.uses_sqlite_hygiene);
}
#[test]
fn memory_config_defaults_for_lucid_enable_sqlite_hygiene() {
let config = memory_config_defaults_for_backend("lucid");
assert_eq!(config.backend, "lucid");
assert!(config.auto_save);
assert!(config.hygiene_enabled);
assert_eq!(config.archive_after_days, 7);
assert_eq!(config.purge_after_days, 30);
assert_eq!(config.embedding_cache_size, 10000);
}
#[test]
fn memory_config_defaults_for_none_disable_sqlite_hygiene() {
let config = memory_config_defaults_for_backend("none");
assert_eq!(config.backend, "none");
assert!(!config.auto_save);
assert!(!config.hygiene_enabled);
assert_eq!(config.archive_after_days, 0);
assert_eq!(config.purge_after_days, 0);
assert_eq!(config.embedding_cache_size, 0);
}
}