per-message context headers, memory notes, conversation continuity

conversations API path now injects per-message context headers with
live timestamps, room name, and memory notes. this replaces the
template variables in agent instructions which were frozen at
creation time.

memory notes (topical + recent backfill) loaded before each response
in the conversations path — was previously only in the legacy path.

context hint seeds new conversations with recent room history after
resets, so sol doesn't lose conversational continuity on sneeze.

tool call results now logged with preview + length for debugging.
reset_all() clears both in-memory and sqlite conversation state.
This commit is contained in:
2026-03-22 15:00:43 +00:00
parent 904ffa2d4d
commit 7bf9e25361
4 changed files with 170 additions and 29 deletions

View File

@@ -8,6 +8,7 @@ mod conversations;
mod matrix_utils;
mod memory;
mod persistence;
mod sdk;
mod sync;
mod tools;
@@ -65,6 +66,10 @@ async fn main() -> anyhow::Result<()> {
let mistral_api_key = std::env::var("SOL_MISTRAL_API_KEY")
.map_err(|_| anyhow::anyhow!("SOL_MISTRAL_API_KEY not set"))?;
// Optional Gitea admin credentials for user impersonation
let gitea_admin_username = std::env::var("SOL_GITEA_ADMIN_USERNAME").ok();
let gitea_admin_password = std::env::var("SOL_GITEA_ADMIN_PASSWORD").ok();
let config = Arc::new(config);
// Initialize Matrix client with E2EE and sqlite store
@@ -131,13 +136,52 @@ async fn main() -> anyhow::Result<()> {
}
}
// Initialize persistent state database (needed by token store + agent/conversation registries)
let (store, state_recovery_failed) = match persistence::Store::open(&config.matrix.db_path) {
Ok(s) => (Arc::new(s), false),
Err(e) => {
error!("Failed to open state database at {}: {e}", config.matrix.db_path);
error!("Falling back to in-memory state — conversations will not survive restarts");
(Arc::new(persistence::Store::open_memory().expect("in-memory DB must work")), true)
}
};
// Initialize Vault client for secure token storage
let vault_client = Arc::new(sdk::vault::VaultClient::new(
&config.vault.url,
&config.vault.role,
&config.vault.mount,
));
// Initialize Gitea client if configured
let gitea_client: Option<Arc<sdk::gitea::GiteaClient>> =
if let (Some(gitea_config), Some(admin_user), Some(admin_pass)) =
(&config.services.gitea, &gitea_admin_username, &gitea_admin_password)
{
let token_store = Arc::new(sdk::tokens::TokenStore::new(
store.clone(),
vault_client.clone(),
));
info!(url = gitea_config.url.as_str(), "Gitea integration enabled");
Some(Arc::new(sdk::gitea::GiteaClient::new(
gitea_config.url.clone(),
admin_user.clone(),
admin_pass.clone(),
token_store,
)))
} else {
info!("Gitea integration disabled (missing config or credentials)");
None
};
let tool_registry = Arc::new(ToolRegistry::new(
os_client.clone(),
matrix_client.clone(),
config.clone(),
gitea_client,
));
let indexer = Arc::new(Indexer::new(os_client.clone(), config.clone()));
let evaluator = Arc::new(Evaluator::new(config.clone()));
let evaluator = Arc::new(Evaluator::new(config.clone(), system_prompt_text.clone()));
let responder = Arc::new(Responder::new(
config.clone(),
personality,
@@ -148,22 +192,12 @@ async fn main() -> anyhow::Result<()> {
// Start background flush task
let _flush_handle = indexer.start_flush_task();
// Initialize persistent state database
let (store, state_recovery_failed) = match persistence::Store::open(&config.matrix.db_path) {
Ok(s) => (Arc::new(s), false),
Err(e) => {
error!("Failed to open state database at {}: {e}", config.matrix.db_path);
error!("Falling back to in-memory state — conversations will not survive restarts");
(Arc::new(persistence::Store::open_memory().expect("in-memory DB must work")), true)
}
};
// Initialize agent registry and conversation registry (with SQLite backing)
let agent_registry = Arc::new(AgentRegistry::new(store.clone()));
let conversation_registry = Arc::new(ConversationRegistry::new(
config.mistral.default_model.clone(),
config.agents.compaction_threshold,
store,
store.clone(),
));
// Build shared state
@@ -182,9 +216,10 @@ async fn main() -> anyhow::Result<()> {
});
// Initialize orchestrator agent if conversations API is enabled
let mut agent_recreated = false;
if config.agents.use_conversations_api {
info!("Conversations API enabled — ensuring orchestrator agent exists");
let agent_tools = tools::ToolRegistry::agent_tool_definitions();
let agent_tools = tools::ToolRegistry::agent_tool_definitions(config.services.gitea.is_some());
match state
.agent_registry
.ensure_orchestrator(
@@ -192,12 +227,20 @@ async fn main() -> anyhow::Result<()> {
&config.agents.orchestrator_model,
agent_tools,
&state.mistral,
&[], // no domain agents yet — delegation section added when they are
)
.await
{
Ok(agent_id) => {
info!(agent_id = agent_id.as_str(), "Orchestrator agent ready");
Ok((agent_id, recreated)) => {
info!(agent_id = agent_id.as_str(), recreated, "Orchestrator agent ready");
state.conversation_registry.set_agent_id(agent_id).await;
if recreated {
// Agent was recreated (system prompt changed) — old conversations
// are bound to the stale agent and won't work. Reset everything.
state.conversation_registry.reset_all().await;
agent_recreated = true;
}
}
Err(e) => {
error!("Failed to create orchestrator agent: {e}");
@@ -221,8 +264,8 @@ async fn main() -> anyhow::Result<()> {
}
});
// If state recovery failed, sneeze into all rooms to signal the hiccup
if state_recovery_failed {
// If state recovery failed or agent was recreated, sneeze into all rooms
if state_recovery_failed || agent_recreated {
info!("State recovery failed — sneezing into all rooms");
for room in matrix_client.joined_rooms() {
let content = ruma::events::room::message::RoomMessageEventContent::text_plain(