use crate::attribution::{CreatorInfo, CreatorSource, MCP_CLIENT_NAME};
use crate::{ActivitySource, AppState, DaemonEvent, DaemonReadiness};
use anyhow::{anyhow, Context, Result};
use serde_json::{json, Value};
use trusty_common::memory_core::palace::RoomType;
use trusty_common::memory_core::retrieval::{list_drawers_in_wing, RecallScope, RememberOptions};
use uuid::Uuid;
use super::bm25::bm25_delete_document;
use super::helpers::{
apply_tier_c, attach_mcp_attribution, begin_budgeted_write, blocklist_gate, content_gate,
dedup_gate, mcp_remember_opts, open_palace_handle, open_palace_handle_within, parse_tags,
resolve_palace, resolve_tier_c, room_label, skipped_envelope, write_drawer, WriteDrawerParams,
};
use super::palace_index::{resolve_palace_or_index, PalaceScope};
use super::recall_ops::recall_scope;
use super::wing_ops::resolve_wing_arg;
pub(crate) async fn handle_memory_remember(state: &AppState, args: Value) -> Result<Value> {
let defer_embedding = state.readiness() == DaemonReadiness::Warming;
let palace = resolve_palace(state, &args, "memory_remember")?;
let palace = palace.as_str();
let raw_text = args
.get("text")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("memory_remember: missing 'text'"))?
.to_string();
let force = args.get("force").and_then(|v| v.as_bool()).unwrap_or(false);
let allow_secret_like = args
.get("allow_secret_like")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if !force {
if let Some(pattern) = blocklist_gate(&raw_text) {
let reason = format!("content gate: skipped (blocked pattern: {pattern:?})");
tracing::debug!(palace = %palace, pattern = %pattern, "{reason}");
return Ok(skipped_envelope(palace, &reason));
}
}
let ctx = args.get("context").and_then(|v| v.as_str());
let text = match content_gate(&raw_text, ctx, force) {
Some(t) => t,
None => {
return Ok(skipped_envelope(
palace,
"content gate: skipped (short prompt, no context)",
));
}
};
let room = args
.get("room")
.and_then(|v| v.as_str())
.map(RoomType::parse)
.unwrap_or(RoomType::General);
let mut tags = parse_tags(&args);
attach_mcp_attribution(&mut tags, &args);
let write_lock = state.palace_write_lock(palace);
let (_write_guard, budget) =
begin_budgeted_write(state, &write_lock, palace, "memory_remember").await?;
if !force {
let handle = open_palace_handle_within(state, palace, budget)?;
if dedup_gate(&handle, &text) {
tracing::debug!(
palace = %palace,
"content gate: skipped (duplicate within window)",
);
return Ok(skipped_envelope(
palace,
"content gate: skipped (duplicate within window)",
));
}
}
let room_label_for_kg = room_label(&room);
let wing_handle = open_palace_handle_within(state, palace, budget)?;
let wing_id = resolve_wing_arg(&wing_handle, &args, "memory_remember")?;
let admission = resolve_tier_c(&args, "memory_remember")?;
let mut opts = RememberOptions {
wing_id,
..mcp_remember_opts(force, defer_embedding, allow_secret_like)
};
let (tier, refusal) = apply_tier_c(&admission, &mut opts);
let drawer_id = write_drawer(
state,
WriteDrawerParams {
palace_id: palace,
content: text,
tags,
room,
importance: 0.5,
opts,
room_label_for_kg,
budget,
},
)
.await?;
let mut out = json!({
"drawer_id": drawer_id.to_string(),
"palace": palace,
"status": "stored",
"tier": tier,
});
if let Some(reason) = refusal {
out["tier_c_refused"] = json!(reason);
}
Ok(out)
}
pub(crate) async fn handle_memory_note(state: &AppState, args: Value) -> Result<Value> {
let defer_embedding = state.readiness() == DaemonReadiness::Warming;
let palace = resolve_palace(state, &args, "memory_note")?;
let palace = palace.as_str();
let raw_content = args
.get("content")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("memory_note: missing 'content'"))?
.to_string();
if let Some(pattern) = blocklist_gate(&raw_content) {
let reason = format!("content gate: skipped (blocked pattern: {pattern:?})");
tracing::debug!(palace = %palace, pattern = %pattern, "{reason}");
return Ok(skipped_envelope(palace, &reason));
}
let ctx = args.get("context").and_then(|v| v.as_str());
let content = match content_gate(&raw_content, ctx, false) {
Some(c) => c,
None => {
return Ok(skipped_envelope(
palace,
"content gate: skipped (short prompt, no context)",
));
}
};
let mut tags = parse_tags(&args);
attach_mcp_attribution(&mut tags, &args);
let write_lock = state.palace_write_lock(palace);
let (_write_guard, budget) =
begin_budgeted_write(state, &write_lock, palace, "memory_note").await?;
{
let handle = open_palace_handle_within(state, palace, budget)?;
if dedup_gate(&handle, &content) {
tracing::debug!(
palace = %palace,
"content gate: skipped (duplicate within window)",
);
return Ok(skipped_envelope(
palace,
"content gate: skipped (duplicate within window)",
));
}
}
let room = args
.get("room")
.and_then(|v| v.as_str())
.map(RoomType::parse)
.unwrap_or(RoomType::General);
let room_label_for_kg = room_label(&room);
let admission = resolve_tier_c(&args, "memory_note")?;
let mut opts = RememberOptions {
defer_embedding,
..RememberOptions::note()
};
let (tier, refusal) = apply_tier_c(&admission, &mut opts);
let drawer_id = write_drawer(
state,
WriteDrawerParams {
palace_id: palace,
content,
tags,
room,
importance: 1.0,
opts,
room_label_for_kg,
budget,
},
)
.await
.context("PalaceHandle::remember_with_options (note)")?;
let mut out = json!({
"drawer_id": drawer_id.to_string(),
"palace": palace,
"status": "stored",
"drawer_type": "UserFact",
"tier": tier,
});
if let Some(reason) = refusal {
out["tier_c_refused"] = json!(reason);
}
Ok(out)
}
pub(crate) async fn handle_memory_list(state: &AppState, args: Value) -> Result<Value> {
let palace = match resolve_palace_or_index(state, &args, "memory_list").await? {
PalaceScope::Palace(p) => p,
PalaceScope::Index(index) => return Ok(index),
};
let handle = open_palace_handle(state, &palace)?;
let tag = args
.get("tag")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
let drawers = match recall_scope(&handle, &args, "memory_list")? {
RecallScope::Wing(wing_id) => list_drawers_in_wing(&handle, wing_id, tag, limit),
RecallScope::Room(room) => handle.list_drawers(Some(room), tag, limit),
RecallScope::All => handle.list_drawers(None, tag, limit),
};
let payload: Vec<Value> = drawers
.iter()
.map(|d| {
json!({
"drawer_id": d.id.to_string(),
"content": d.content(),
"importance": d.importance,
"tags": d.tags,
"created_at": d.created_at.to_rfc3339(),
"drawer_type": d.drawer_type.as_str(),
"expires_at": d.expires_at.map(|t| t.to_rfc3339()),
})
})
.collect();
Ok(json!({ "palace": palace, "drawers": payload }))
}
pub(crate) async fn handle_memory_forget(state: &AppState, args: Value) -> Result<Value> {
let palace = resolve_palace(state, &args, "memory_forget")?;
let drawer_id_str = args
.get("drawer_id")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("memory_forget: missing 'drawer_id'"))?;
let drawer_id = Uuid::parse_str(drawer_id_str)
.map_err(|e| anyhow!("memory_forget: invalid drawer_id UUID: {e}"))?;
let handle = open_palace_handle(state, &palace)?;
let outcome = handle.forget(drawer_id).await.context("forget")?;
bm25_delete_document(state, handle.id.as_str(), drawer_id).await?;
if outcome.is_deleted() {
let drawer_count = handle.drawers.read().len();
state.emit(DaemonEvent::DrawerDeleted {
palace_id: palace.clone(),
drawer_count,
source: ActivitySource::Mcp,
});
}
Ok(json!({ "status": outcome.as_str(), "drawer_id": drawer_id_str, "palace": palace }))
}
pub(crate) async fn handle_memory_send_message(state: &AppState, args: Value) -> Result<Value> {
let to_palace = args
.get("to_palace")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("memory_send_message: missing 'to_palace'"))?
.to_string();
let purpose = args
.get("purpose")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("memory_send_message: missing 'purpose'"))?
.to_string();
let content = args
.get("content")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("memory_send_message: missing 'content'"))?
.to_string();
let from_palace = if let Some(s) = args.get("from_palace").and_then(|v| v.as_str()) {
s.to_string()
} else if let Some(d) = state.default_palace.clone() {
d
} else {
crate::messaging::cwd_palace_slug()
.context("memory_send_message: derive from_palace from cwd")?
};
let caller_cwd = args
.get("cwd")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty());
let caller_workstream = args
.get("workstream")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty());
let drawer_id = crate::messaging::send_message_to_palace(
&state.registry,
&state.data_root,
&from_palace,
&to_palace,
&purpose,
content,
CreatorInfo::new_for_caller(
MCP_CLIENT_NAME,
CreatorSource::Mcp,
caller_cwd,
caller_workstream,
),
)
.await
.context("memory_send_message")?;
Ok(json!({
"drawer_id": drawer_id.to_string(),
"from_palace": from_palace,
"to_palace": to_palace,
"purpose": purpose,
"status": "sent",
}))
}