engram-memory 0.2.0

Core binary and shared infrastructure for Engram memory system
Documentation
use std::sync::Arc;

use serde::Deserialize;
use serde_json::{Value, json};

use engram_storage::Memory;

use crate::error::CoreError;
use crate::export_handler::memory_to_portable_json;
use crate::server::ServerState;

#[derive(Deserialize)]
struct InsightsParams {
    action: String,
    id: Option<String>,
}

pub async fn handle(state: &Arc<ServerState>, params: Value) -> Result<Value, CoreError> {
    let parsed: InsightsParams = serde_json::from_value(params)
        .map_err(|error| CoreError::DispatchError(error.to_string()))?;
    match parsed.action.as_str() {
        "list" => handle_list(state).await,
        "generate" => Err(CoreError::DispatchError(
            "insight generation requires engram-trainer (Phase 5)".into(),
        )),
        "delete" => handle_delete(state, parsed.id).await,
        other => Err(CoreError::DispatchError(format!(
            "invalid insights action: {other}"
        ))),
    }
}

async fn handle_list(state: &Arc<ServerState>) -> Result<Value, CoreError> {
    let state_clone = Arc::clone(state);
    tokio::task::spawn_blocking(move || {
        let database = state_clone.database.lock().unwrap();
        let insights = query_active_insights(&database)?;
        let count = insights.len();
        let serialized: Vec<Value> = insights.iter().map(memory_to_portable_json).collect();
        Ok::<Value, CoreError>(json!({ "insights": serialized, "count": count }))
    })
    .await
    .map_err(|error| CoreError::SocketError(error.to_string()))?
}

async fn handle_delete(state: &Arc<ServerState>, id: Option<String>) -> Result<Value, CoreError> {
    let insight_id =
        id.ok_or_else(|| CoreError::DispatchError("delete requires 'id' parameter".into()))?;
    let state_clone = Arc::clone(state);
    tokio::task::spawn_blocking(move || {
        let database = state_clone.database.lock().unwrap();
        let memory = database.get_memory(&insight_id)?;
        if memory.memory_type != "insight" {
            return Err(CoreError::DispatchError(format!(
                "memory {} is not an insight",
                insight_id
            )));
        }
        database.delete_memory(&insight_id)?;
        Ok::<Value, CoreError>(json!({ "deleted": insight_id }))
    })
    .await
    .map_err(|error| CoreError::SocketError(error.to_string()))?
}

fn query_active_insights(database: &engram_storage::Database) -> Result<Vec<Memory>, CoreError> {
    let mut statement = database
        .connection()
        .prepare(
            "SELECT * FROM memories WHERE memory_type = 'insight' \
             AND superseded_by IS NULL",
        )
        .map_err(|error| CoreError::Storage(engram_storage::StorageError::Sqlite(error)))?;
    let rows = statement
        .query_map([], engram_storage::row_to_memory)
        .map_err(|error| CoreError::Storage(engram_storage::StorageError::Sqlite(error)))?;
    let mut insights = Vec::new();
    for row in rows {
        insights.push(
            row.map_err(|error| CoreError::Storage(engram_storage::StorageError::Sqlite(error)))?,
        );
    }
    Ok(insights)
}