feat: per-user auto-memory with ResponseContext

Three memory channels: hidden tool (sol.memory.set/get in scripts),
pre-response injection (relevant memories loaded into system prompt),
and post-response extraction (ministral-3b extracts facts after each
response). User isolation enforced at Rust level — user_id derived
from Matrix sender, never from script arguments.

New modules: context (ResponseContext), memory (schema, store, extractor).
ResponseContext threaded through responder → tools → script runtime.
OpenSearch index sol_user_memory created on startup alongside archive.
This commit is contained in:
2026-03-21 15:51:31 +00:00
parent 4dc20bee23
commit 4949e70ecc
23 changed files with 4494 additions and 124 deletions

View File

@@ -25,6 +25,8 @@ pub struct OpenSearchConfig {
pub flush_interval_ms: u64,
#[serde(default = "default_embedding_pipeline")]
pub embedding_pipeline: String,
#[serde(default = "default_memory_index")]
pub memory_index: String,
}
#[derive(Debug, Clone, Deserialize)]
@@ -59,6 +61,30 @@ pub struct BehaviorConfig {
pub backfill_on_join: bool,
#[serde(default = "default_backfill_limit")]
pub backfill_limit: usize,
#[serde(default)]
pub instant_responses: bool,
#[serde(default = "default_cooldown_after_response_ms")]
pub cooldown_after_response_ms: u64,
#[serde(default = "default_evaluation_context_window")]
pub evaluation_context_window: usize,
#[serde(default = "default_detect_sol_in_conversation")]
pub detect_sol_in_conversation: bool,
#[serde(default)]
pub evaluation_prompt_active: Option<String>,
#[serde(default)]
pub evaluation_prompt_passive: Option<String>,
#[serde(default = "default_reaction_threshold")]
pub reaction_threshold: f32,
#[serde(default = "default_reaction_enabled")]
pub reaction_enabled: bool,
#[serde(default = "default_script_timeout_secs")]
pub script_timeout_secs: u64,
#[serde(default = "default_script_max_heap_mb")]
pub script_max_heap_mb: usize,
#[serde(default)]
pub script_fetch_allowlist: Vec<String>,
#[serde(default = "default_memory_extraction_enabled")]
pub memory_extraction_enabled: bool,
}
fn default_batch_size() -> usize { 50 }
@@ -68,15 +94,24 @@ fn default_model() -> String { "mistral-medium-latest".into() }
fn default_evaluation_model() -> String { "ministral-3b-latest".into() }
fn default_research_model() -> String { "mistral-large-latest".into() }
fn default_max_tool_iterations() -> usize { 5 }
fn default_response_delay_min_ms() -> u64 { 2000 }
fn default_response_delay_max_ms() -> u64 { 8000 }
fn default_response_delay_min_ms() -> u64 { 100 }
fn default_response_delay_max_ms() -> u64 { 2300 }
fn default_spontaneous_delay_min_ms() -> u64 { 15000 }
fn default_spontaneous_delay_max_ms() -> u64 { 60000 }
fn default_spontaneous_threshold() -> f32 { 0.7 }
fn default_spontaneous_threshold() -> f32 { 0.85 }
fn default_cooldown_after_response_ms() -> u64 { 15000 }
fn default_evaluation_context_window() -> usize { 25 }
fn default_detect_sol_in_conversation() -> bool { true }
fn default_reaction_threshold() -> f32 { 0.6 }
fn default_reaction_enabled() -> bool { true }
fn default_room_context_window() -> usize { 30 }
fn default_dm_context_window() -> usize { 100 }
fn default_backfill_on_join() -> bool { true }
fn default_backfill_limit() -> usize { 10000 }
fn default_script_timeout_secs() -> u64 { 5 }
fn default_script_max_heap_mb() -> usize { 64 }
fn default_memory_index() -> String { "sol_user_memory".into() }
fn default_memory_extraction_enabled() -> bool { true }
impl Config {
pub fn load(path: &str) -> anyhow::Result<Self> {
@@ -155,19 +190,23 @@ backfill_limit = 5000
assert_eq!(config.opensearch.batch_size, 50);
assert_eq!(config.opensearch.flush_interval_ms, 2000);
assert_eq!(config.opensearch.embedding_pipeline, "tuwunel_embedding_pipeline");
assert_eq!(config.opensearch.memory_index, "sol_user_memory");
assert_eq!(config.mistral.default_model, "mistral-medium-latest");
assert_eq!(config.mistral.evaluation_model, "ministral-3b-latest");
assert_eq!(config.mistral.research_model, "mistral-large-latest");
assert_eq!(config.mistral.max_tool_iterations, 5);
assert_eq!(config.behavior.response_delay_min_ms, 2000);
assert_eq!(config.behavior.response_delay_max_ms, 8000);
assert_eq!(config.behavior.response_delay_min_ms, 100);
assert_eq!(config.behavior.response_delay_max_ms, 2300);
assert_eq!(config.behavior.spontaneous_delay_min_ms, 15000);
assert_eq!(config.behavior.spontaneous_delay_max_ms, 60000);
assert!((config.behavior.spontaneous_threshold - 0.7).abs() < f32::EPSILON);
assert!((config.behavior.spontaneous_threshold - 0.85).abs() < f32::EPSILON);
assert!(!config.behavior.instant_responses);
assert_eq!(config.behavior.cooldown_after_response_ms, 15000);
assert_eq!(config.behavior.room_context_window, 30);
assert_eq!(config.behavior.dm_context_window, 100);
assert!(config.behavior.backfill_on_join);
assert_eq!(config.behavior.backfill_limit, 10000);
assert!(config.behavior.memory_extraction_enabled);
}
#[test]