use axum::{
extract::{Path, Query, State},
response::Json,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tracing::info;
use super::state::MultiUserMemoryManager;
use super::types::MemoryEvent;
use crate::errors::{AppError, ValidationErrorExt};
use crate::memory::{self, ExperienceType, Memory};
use crate::validation;
pub type AppState = std::sync::Arc<MultiUserMemoryManager>;
#[derive(Debug, Serialize)]
pub struct MemoryWithHierarchy {
#[serde(flatten)]
pub memory: Memory,
pub children_ids: Vec<String>,
pub children_count: usize,
}
#[derive(Debug, Deserialize)]
pub struct ListQuery {
pub limit: Option<usize>,
#[serde(rename = "type")]
pub memory_type: Option<String>,
pub query: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct ListResponse {
pub memories: Vec<ListMemoryItem>,
pub total: usize,
}
#[derive(Debug, Deserialize)]
pub struct ListMemoriesRequest {
pub user_id: String,
pub limit: Option<usize>,
#[serde(rename = "type")]
pub memory_type: Option<String>,
pub query: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct ListMemoryItem {
pub id: String,
pub content: String,
pub memory_type: String,
pub importance: f32,
pub tags: Vec<String>,
pub created_at: String,
pub tier: String,
}
#[derive(Debug, Deserialize)]
pub struct UpdateMemoryRequest {
pub user_id: String,
pub content: String,
pub embeddings: Option<Vec<f32>>,
}
#[derive(Debug, Serialize)]
pub struct UpdateMemoryResponse {
pub success: bool,
pub id: String,
pub message: String,
}
#[derive(Debug, Serialize)]
pub struct DeleteMemoryResponse {
pub success: bool,
pub id: String,
pub message: String,
}
#[derive(Debug, Deserialize)]
pub struct ForgetByIdRequest {
pub user_id: String,
pub memory_id: String,
}
#[derive(Debug, Deserialize)]
pub struct ForgetByAgeRequest {
pub user_id: String,
pub days_old: u32,
}
#[derive(Debug, Deserialize)]
pub struct ForgetByImportanceRequest {
pub user_id: String,
pub threshold: f32,
}
#[derive(Debug, Deserialize)]
pub struct ForgetByPatternRequest {
pub user_id: String,
pub pattern: String,
}
#[derive(Debug, Deserialize)]
pub struct ForgetByTagsRequest {
pub user_id: String,
pub tags: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub struct ForgetByDateRequest {
pub user_id: String,
pub start: chrono::DateTime<chrono::Utc>,
pub end: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Deserialize)]
pub struct BulkDeleteRequest {
pub user_id: String,
pub tags: Option<Vec<String>>,
pub memory_type: Option<String>,
pub created_after: Option<chrono::DateTime<chrono::Utc>>,
pub created_before: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, Deserialize)]
pub struct ClearAllRequest {
pub user_id: String,
pub confirm: String,
}
#[derive(Debug, Deserialize)]
pub struct PatchMemoryRequest {
pub user_id: String,
pub content: Option<String>,
pub tags: Option<Vec<String>>,
pub memory_type: Option<String>,
}
#[tracing::instrument(skip(state))]
pub async fn get_memory(
State(state): State<AppState>,
Path(memory_id): Path<String>,
Query(params): Query<HashMap<String, String>>,
) -> Result<Json<MemoryWithHierarchy>, AppError> {
let user_id = params
.get("user_id")
.ok_or_else(|| AppError::InvalidInput {
field: "user_id".to_string(),
reason: "user_id required".to_string(),
})?;
validation::validate_user_id(user_id).map_validation_err("user_id")?;
let memory = state.get_user_memory(user_id).map_err(AppError::Internal)?;
let memory_guard = memory.read();
let shared_memory = resolve_memory(&memory_guard, &memory_id)?;
let memory_obj = (*shared_memory).clone();
let resolved_id = shared_memory.id.clone();
let children = memory_guard
.get_memory_children(&resolved_id)
.unwrap_or_default();
let children_ids: Vec<String> = children.iter().map(|c| c.id.0.to_string()).collect();
let children_count = children_ids.len();
Ok(Json(MemoryWithHierarchy {
memory: memory_obj,
children_ids,
children_count,
}))
}
#[tracing::instrument(skip(state), fields(user_id = %user_id))]
pub async fn list_memories(
State(state): State<AppState>,
Path(user_id): Path<String>,
Query(query): Query<ListQuery>,
) -> Result<Json<ListResponse>, AppError> {
validation::validate_user_id(&user_id).map_validation_err("user_id")?;
let memory = state
.get_user_memory(&user_id)
.map_err(AppError::Internal)?;
let all_memories = {
let memory = memory.clone();
tokio::task::spawn_blocking(move || {
let memory_guard = memory.read();
memory_guard.get_all_memories()
})
.await
.map_err(|e| AppError::Internal(anyhow::anyhow!("Blocking task panicked: {e}")))?
.map_err(AppError::Internal)?
};
let mut filtered: Vec<_> = if let Some(ref type_filter) = query.memory_type {
let type_lower = type_filter.to_lowercase();
all_memories
.into_iter()
.filter(|m| format!("{:?}", m.experience.experience_type).to_lowercase() == type_lower)
.collect()
} else {
all_memories
};
if let Some(ref text_query) = query.query {
let query_lower = text_query.to_lowercase();
filtered.retain(|m| {
if m.experience.content.to_lowercase().contains(&query_lower) {
return true;
}
for tag in &m.experience.entities {
if tag.to_lowercase().contains(&query_lower) {
return true;
}
}
false
});
}
let total = filtered.len();
let limit = query.limit.unwrap_or(100).min(1000);
let memories: Vec<ListMemoryItem> = filtered
.into_iter()
.take(limit)
.map(|m| ListMemoryItem {
id: m.id.0.to_string(),
content: m.experience.content.chars().take(500).collect(),
memory_type: format!("{:?}", m.experience.experience_type),
importance: m.importance(),
tags: m.experience.entities.clone(),
created_at: m.created_at.to_rfc3339(),
tier: format!("{:?}", m.tier),
})
.collect();
Ok(Json(ListResponse { memories, total }))
}
#[tracing::instrument(skip(state), fields(user_id = %req.user_id))]
pub async fn list_memories_post(
State(state): State<AppState>,
Json(req): Json<ListMemoriesRequest>,
) -> Result<Json<ListResponse>, AppError> {
list_memories_inner(state, req).await
}
#[derive(Debug, Deserialize)]
pub struct ListMemoriesQuery {
pub user_id: String,
pub limit: Option<usize>,
#[serde(rename = "type")]
pub memory_type: Option<String>,
pub query: Option<String>,
}
#[tracing::instrument(skip(state), fields(user_id = %params.user_id))]
pub async fn list_memories_get(
State(state): State<AppState>,
Query(params): Query<ListMemoriesQuery>,
) -> Result<Json<ListResponse>, AppError> {
let req = ListMemoriesRequest {
user_id: params.user_id,
limit: params.limit,
memory_type: params.memory_type,
query: params.query,
};
list_memories_inner(state, req).await
}
async fn list_memories_inner(
state: AppState,
req: ListMemoriesRequest,
) -> Result<Json<ListResponse>, AppError> {
validation::validate_user_id(&req.user_id).map_validation_err("user_id")?;
let memory = state
.get_user_memory(&req.user_id)
.map_err(AppError::Internal)?;
let all_memories = {
let memory = memory.clone();
tokio::task::spawn_blocking(move || {
let memory_guard = memory.read();
memory_guard.get_all_memories()
})
.await
.map_err(|e| AppError::Internal(anyhow::anyhow!("Blocking task panicked: {e}")))?
.map_err(AppError::Internal)?
};
let mut filtered: Vec<_> = if let Some(ref type_filter) = req.memory_type {
let type_lower = type_filter.to_lowercase();
all_memories
.into_iter()
.filter(|m| format!("{:?}", m.experience.experience_type).to_lowercase() == type_lower)
.collect()
} else {
all_memories
};
if let Some(ref text_query) = req.query {
let query_lower = text_query.to_lowercase();
filtered.retain(|m| {
if m.experience.content.to_lowercase().contains(&query_lower) {
return true;
}
for tag in &m.experience.entities {
if tag.to_lowercase().contains(&query_lower) {
return true;
}
}
false
});
}
let total = filtered.len();
let limit = req.limit.unwrap_or(100).min(1000);
let memories: Vec<ListMemoryItem> = filtered
.into_iter()
.take(limit)
.map(|m| ListMemoryItem {
id: m.id.0.to_string(),
content: m.experience.content.chars().take(500).collect(),
memory_type: format!("{:?}", m.experience.experience_type),
importance: m.importance(),
tags: m.experience.entities.clone(),
created_at: m.created_at.to_rfc3339(),
tier: format!("{:?}", m.tier),
})
.collect();
Ok(Json(ListResponse { memories, total }))
}
#[tracing::instrument(skip(state), fields(memory_id = %memory_id))]
pub async fn update_memory(
State(state): State<AppState>,
Path(memory_id): Path<String>,
Json(req): Json<UpdateMemoryRequest>,
) -> Result<Json<UpdateMemoryResponse>, AppError> {
validation::validate_user_id(&req.user_id).map_validation_err("user_id")?;
validation::validate_content(&req.content, false).map_validation_err("content")?;
if let Some(ref emb) = req.embeddings {
validation::validate_embeddings(emb)
.map_err(|e| AppError::InvalidEmbeddings(e.to_string()))?;
}
let memory = state
.get_user_memory(&req.user_id)
.map_err(AppError::Internal)?;
let memory_guard = memory.read();
let shared_memory = resolve_memory(&memory_guard, &memory_id)?;
let mut current_memory = (*shared_memory).clone();
let resolved_id_str = current_memory.id.0.to_string();
let content_preview: String = req.content.chars().take(50).collect();
current_memory.experience.content = req.content;
if let Some(emb) = req.embeddings {
current_memory.experience.embeddings = Some(emb);
} else {
current_memory.experience.embeddings = None;
}
memory_guard
.update_memory(¤t_memory)
.map_err(AppError::Internal)?;
state.log_event(
&req.user_id,
"UPDATE",
&resolved_id_str,
&format!("Updated memory content: {content_preview}"),
);
Ok(Json(UpdateMemoryResponse {
success: true,
id: resolved_id_str,
message: "Memory updated successfully".to_string(),
}))
}
#[tracing::instrument(skip(state), fields(memory_id = %memory_id))]
pub async fn delete_memory(
State(state): State<AppState>,
Path(memory_id): Path<String>,
Query(params): Query<HashMap<String, String>>,
) -> Result<Json<DeleteMemoryResponse>, AppError> {
let user_id = params
.get("user_id")
.ok_or_else(|| AppError::InvalidInput {
field: "user_id".to_string(),
reason: "user_id required".to_string(),
})?;
validation::validate_user_id(user_id).map_validation_err("user_id")?;
let memory = state.get_user_memory(user_id).map_err(AppError::Internal)?;
let memory_guard = memory.read();
let shared_memory = resolve_memory(&memory_guard, &memory_id)?;
let resolved_id = shared_memory.id.clone();
let resolved_id_str = resolved_id.0.to_string();
memory_guard
.forget(memory::ForgetCriteria::ById(resolved_id))
.map_err(AppError::Internal)?;
state.log_event(user_id, "DELETE", &resolved_id_str, "Memory deleted");
state.emit_event(MemoryEvent {
event_type: "DELETE".to_string(),
timestamp: chrono::Utc::now(),
user_id: user_id.to_string(),
memory_id: Some(resolved_id_str.clone()),
content_preview: None,
memory_type: None,
importance: None,
count: None,
entities: None,
results: None,
});
Ok(Json(DeleteMemoryResponse {
success: true,
id: resolved_id_str,
message: "Memory deleted successfully".to_string(),
}))
}
#[tracing::instrument(skip(state), fields(memory_id = %req.memory_id))]
pub async fn forget_by_id(
State(state): State<AppState>,
Json(req): Json<ForgetByIdRequest>,
) -> Result<Json<DeleteMemoryResponse>, AppError> {
validation::validate_user_id(&req.user_id).map_validation_err("user_id")?;
let memory = state
.get_user_memory(&req.user_id)
.map_err(AppError::Internal)?;
let memory_guard = memory.read();
let shared_memory = resolve_memory(&memory_guard, &req.memory_id)?;
let resolved_id = shared_memory.id.clone();
let resolved_id_str = resolved_id.0.to_string();
memory_guard
.forget(memory::ForgetCriteria::ById(resolved_id))
.map_err(AppError::Internal)?;
state.log_event(&req.user_id, "DELETE", &resolved_id_str, "Memory deleted");
state.emit_event(MemoryEvent {
event_type: "DELETE".to_string(),
timestamp: chrono::Utc::now(),
user_id: req.user_id.clone(),
memory_id: Some(resolved_id_str.clone()),
content_preview: None,
memory_type: None,
importance: None,
count: None,
entities: None,
results: None,
});
Ok(Json(DeleteMemoryResponse {
success: true,
id: resolved_id_str,
message: "Memory deleted successfully".to_string(),
}))
}
#[tracing::instrument(skip(state), fields(memory_id = %memory_id))]
pub async fn patch_memory(
State(state): State<AppState>,
Path(memory_id): Path<String>,
Json(req): Json<PatchMemoryRequest>,
) -> Result<Json<serde_json::Value>, AppError> {
validation::validate_user_id(&req.user_id).map_validation_err("user_id")?;
let memory = state
.get_user_memory(&req.user_id)
.map_err(AppError::Internal)?;
let memory_guard = memory.read();
let shared_memory = resolve_memory(&memory_guard, &memory_id)?;
let mut current_memory = (*shared_memory).clone();
let resolved_id_str = current_memory.id.0.to_string();
let mut changes = Vec::new();
if let Some(ref new_content) = req.content {
validation::validate_content(new_content, false).map_validation_err("content")?;
current_memory.experience.content = new_content.clone();
current_memory.experience.embeddings = None;
changes.push("content");
}
if let Some(ref new_tags) = req.tags {
for tag in new_tags {
if !current_memory.experience.entities.contains(tag) {
current_memory.experience.entities.push(tag.clone());
}
}
changes.push("tags");
}
if let Some(ref type_str) = req.memory_type {
current_memory.experience.experience_type = parse_experience_type(type_str)?;
changes.push("type");
}
if changes.is_empty() {
return Err(AppError::InvalidInput {
field: "body".to_string(),
reason: "No fields to update provided".to_string(),
});
}
memory_guard
.update_memory(¤t_memory)
.map_err(AppError::Internal)?;
state.log_event(
&req.user_id,
"PATCH",
&resolved_id_str,
&format!("Updated fields: {}", changes.join(", ")),
);
Ok(Json(serde_json::json!({
"success": true,
"id": resolved_id_str,
"updated_fields": changes
})))
}
#[tracing::instrument(skip(state), fields(user_id = %req.user_id))]
pub async fn forget_by_age(
State(state): State<AppState>,
Json(req): Json<ForgetByAgeRequest>,
) -> Result<Json<serde_json::Value>, AppError> {
validation::validate_user_id(&req.user_id).map_validation_err("user_id")?;
let memory_sys = state
.get_user_memory(&req.user_id)
.map_err(AppError::Internal)?;
let memory_guard = memory_sys.read();
let count = memory_guard
.forget(memory::ForgetCriteria::OlderThan(req.days_old))
.map_err(AppError::Internal)?;
state.log_event(
&req.user_id,
"FORGET_BY_AGE",
&format!("{} days", req.days_old),
&format!("Forgot {count} memories"),
);
Ok(Json(serde_json::json!({
"success": true,
"forgotten_count": count,
"criteria": format!("older than {} days", req.days_old)
})))
}
#[tracing::instrument(skip(state), fields(user_id = %req.user_id))]
pub async fn forget_by_importance(
State(state): State<AppState>,
Json(req): Json<ForgetByImportanceRequest>,
) -> Result<Json<serde_json::Value>, AppError> {
validation::validate_user_id(&req.user_id).map_validation_err("user_id")?;
if req.threshold < 0.0 || req.threshold > 1.0 {
return Err(AppError::InvalidInput {
field: "threshold".to_string(),
reason: "Must be between 0.0 and 1.0".to_string(),
});
}
let memory_sys = state
.get_user_memory(&req.user_id)
.map_err(AppError::Internal)?;
let memory_guard = memory_sys.read();
let count = memory_guard
.forget(memory::ForgetCriteria::LowImportance(req.threshold))
.map_err(AppError::Internal)?;
state.log_event(
&req.user_id,
"FORGET_BY_IMPORTANCE",
&format!("threshold {}", req.threshold),
&format!("Forgot {count} memories"),
);
Ok(Json(serde_json::json!({
"success": true,
"forgotten_count": count,
"criteria": format!("importance < {}", req.threshold)
})))
}
#[tracing::instrument(skip(state), fields(user_id = %req.user_id))]
pub async fn forget_by_pattern(
State(state): State<AppState>,
Json(req): Json<ForgetByPatternRequest>,
) -> Result<Json<serde_json::Value>, AppError> {
validation::validate_user_id(&req.user_id).map_validation_err("user_id")?;
if req.pattern.len() > 1000 {
return Err(AppError::InvalidInput {
field: "pattern".to_string(),
reason: "pattern too long (max 1000 chars)".to_string(),
});
}
if regex::Regex::new(&req.pattern).is_err() {
return Err(AppError::InvalidInput {
field: "pattern".to_string(),
reason: "invalid regex pattern".to_string(),
});
}
let memory_sys = state
.get_user_memory(&req.user_id)
.map_err(AppError::Internal)?;
let memory_guard = memory_sys.read();
let count = memory_guard
.forget(memory::ForgetCriteria::Pattern(req.pattern.clone()))
.map_err(AppError::Internal)?;
state.log_event(
&req.user_id,
"FORGET_BY_PATTERN",
&req.pattern,
&format!("Forgot {count} memories"),
);
Ok(Json(serde_json::json!({
"success": true,
"forgotten_count": count,
"pattern": req.pattern
})))
}
#[tracing::instrument(skip(state), fields(user_id = %req.user_id))]
pub async fn forget_by_tags(
State(state): State<AppState>,
Json(req): Json<ForgetByTagsRequest>,
) -> Result<Json<serde_json::Value>, AppError> {
validation::validate_user_id(&req.user_id).map_validation_err("user_id")?;
if req.tags.is_empty() {
return Err(AppError::InvalidInput {
field: "tags".to_string(),
reason: "At least one tag must be provided".to_string(),
});
}
let memory_sys = state
.get_user_memory(&req.user_id)
.map_err(AppError::Internal)?;
let memory_guard = memory_sys.read();
let deleted_count = memory_guard
.forget(memory::ForgetCriteria::ByTags(req.tags.clone()))
.map_err(AppError::Internal)?;
info!(
"🏷️ Forget by tags: user={}, tags={:?}, deleted={}",
req.user_id, req.tags, deleted_count
);
state.emit_event(MemoryEvent {
event_type: "DELETE".to_string(),
timestamp: chrono::Utc::now(),
user_id: req.user_id.clone(),
memory_id: None,
content_preview: Some(format!("tags: {:?}", req.tags)),
memory_type: None,
importance: None,
count: Some(deleted_count),
entities: None,
results: None,
});
Ok(Json(serde_json::json!({
"success": true,
"deleted_count": deleted_count,
"tags": req.tags
})))
}
#[tracing::instrument(skip(state), fields(user_id = %req.user_id))]
pub async fn forget_by_date(
State(state): State<AppState>,
Json(req): Json<ForgetByDateRequest>,
) -> Result<Json<serde_json::Value>, AppError> {
validation::validate_user_id(&req.user_id).map_validation_err("user_id")?;
if req.end < req.start {
return Err(AppError::InvalidInput {
field: "end".to_string(),
reason: "End date must be after start date".to_string(),
});
}
let memory_sys = state
.get_user_memory(&req.user_id)
.map_err(AppError::Internal)?;
let memory_guard = memory_sys.read();
let deleted_count = memory_guard
.forget(memory::ForgetCriteria::ByDateRange {
start: req.start,
end: req.end,
})
.map_err(AppError::Internal)?;
info!(
"📅 Forget by date: user={}, start={}, end={}, deleted={}",
req.user_id, req.start, req.end, deleted_count
);
state.emit_event(MemoryEvent {
event_type: "DELETE".to_string(),
timestamp: chrono::Utc::now(),
user_id: req.user_id.clone(),
memory_id: None,
content_preview: Some(format!(
"{} to {}",
req.start.format("%Y-%m-%d"),
req.end.format("%Y-%m-%d")
)),
memory_type: None,
importance: None,
count: Some(deleted_count),
entities: None,
results: None,
});
Ok(Json(serde_json::json!({
"success": true,
"deleted_count": deleted_count,
"start": req.start.to_rfc3339(),
"end": req.end.to_rfc3339()
})))
}
#[tracing::instrument(skip(state), fields(user_id = %req.user_id))]
pub async fn bulk_delete_memories(
State(state): State<AppState>,
Json(req): Json<BulkDeleteRequest>,
) -> Result<Json<serde_json::Value>, AppError> {
validation::validate_user_id(&req.user_id).map_validation_err("user_id")?;
let memory_sys = state
.get_user_memory(&req.user_id)
.map_err(AppError::Internal)?;
let memory_guard = memory_sys.read();
let mut total_count = 0;
if let Some(ref tags) = req.tags {
if !tags.is_empty() {
let count = memory_guard
.forget(memory::ForgetCriteria::ByTags(tags.clone()))
.map_err(AppError::Internal)?;
total_count += count;
}
}
if let Some(ref type_str) = req.memory_type {
let exp_type = parse_experience_type(type_str)?;
let count = memory_guard
.forget(memory::ForgetCriteria::ByType(exp_type))
.map_err(AppError::Internal)?;
total_count += count;
}
if req.created_after.is_some() || req.created_before.is_some() {
let start = req
.created_after
.unwrap_or(chrono::DateTime::<chrono::Utc>::MIN_UTC);
let end = req.created_before.unwrap_or(chrono::Utc::now());
let count = memory_guard
.forget(memory::ForgetCriteria::ByDateRange { start, end })
.map_err(AppError::Internal)?;
total_count += count;
}
state.log_event(
&req.user_id,
"BULK_DELETE",
"multiple",
&format!("Deleted {total_count} memories"),
);
Ok(Json(serde_json::json!({
"success": true,
"deleted_count": total_count
})))
}
#[tracing::instrument(skip(state), fields(user_id = %req.user_id))]
pub async fn clear_all_memories(
State(state): State<AppState>,
Json(req): Json<ClearAllRequest>,
) -> Result<Json<serde_json::Value>, AppError> {
validation::validate_user_id(&req.user_id).map_validation_err("user_id")?;
if req.confirm != "CONFIRM" {
return Err(AppError::InvalidInput {
field: "confirm".to_string(),
reason: "Must provide confirm: \"CONFIRM\" to clear all memories".to_string(),
});
}
let memory_sys = state
.get_user_memory(&req.user_id)
.map_err(AppError::Internal)?;
let memory_guard = memory_sys.read();
let count = memory_guard
.forget(memory::ForgetCriteria::All)
.map_err(AppError::Internal)?;
state.log_event(
&req.user_id,
"CLEAR_ALL",
"GDPR",
&format!("GDPR erasure: deleted {count} memories"),
);
Ok(Json(serde_json::json!({
"success": true,
"deleted_count": count,
"message": "All memories have been permanently deleted (GDPR erasure)"
})))
}
fn resolve_memory(
memory_guard: &memory::MemorySystem,
memory_id_str: &str,
) -> Result<memory::SharedMemory, AppError> {
validation::validate_memory_id_or_prefix(memory_id_str)
.map_err(|e| AppError::InvalidMemoryId(e.to_string()))?;
memory_guard
.find_memory_by_prefix(memory_id_str)
.map_err(|e| {
let msg = e.to_string();
if msg.starts_with("Ambiguous") {
let count = msg
.rsplit("matches ")
.next()
.and_then(|s| s.split(' ').next())
.and_then(|s| s.parse().ok())
.unwrap_or(0);
AppError::AmbiguousMemoryId {
prefix: memory_id_str.to_string(),
count,
}
} else {
AppError::Internal(e)
}
})?
.ok_or_else(|| AppError::MemoryNotFound(memory_id_str.to_string()))
}
fn parse_experience_type(type_str: &str) -> Result<ExperienceType, AppError> {
match type_str.to_lowercase().as_str() {
"observation" => Ok(ExperienceType::Observation),
"decision" => Ok(ExperienceType::Decision),
"learning" => Ok(ExperienceType::Learning),
"error" => Ok(ExperienceType::Error),
"discovery" => Ok(ExperienceType::Discovery),
"pattern" => Ok(ExperienceType::Pattern),
"context" => Ok(ExperienceType::Context),
"task" => Ok(ExperienceType::Task),
"codeedit" | "code_edit" => Ok(ExperienceType::CodeEdit),
"fileaccess" | "file_access" => Ok(ExperienceType::FileAccess),
"search" => Ok(ExperienceType::Search),
"command" => Ok(ExperienceType::Command),
"conversation" => Ok(ExperienceType::Conversation),
"intention" => Ok(ExperienceType::Intention),
_ => Err(AppError::InvalidInput {
field: "memory_type".to_string(),
reason: format!("Invalid memory type: {type_str}"),
}),
}
}