use crate::{ActivitySource, AppState, DaemonEvent};
use anyhow::{anyhow, Context, Result};
use serde_json::{json, Value};
use trusty_common::memory_core::palace::{Palace, PalaceId};
use uuid::Uuid;
use super::helpers::{open_palace_handle, resolve_palace};
fn validate_slug_format(slug: &str) -> Result<()> {
use trusty_common::palace_id::{palace_id_is_valid, PALACE_ID_MAX_LEN};
if palace_id_is_valid(slug) {
return Ok(());
}
if slug.is_empty() || slug.len() > PALACE_ID_MAX_LEN {
return Err(anyhow!(
"palace slug must be 1–{PALACE_ID_MAX_LEN} characters (got {}): {slug:?}",
slug.len()
));
}
Err(anyhow!(
"palace slug must match [a-z0-9][a-z0-9-]{{0,62}} \
(lowercase letters, digits, hyphens only): {slug:?}"
))
}
pub(crate) async fn handle_palace_create(state: &AppState, args: Value) -> Result<Value> {
let palace_name = args
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("palace_create: missing 'name'"))?;
let force = args.get("force").and_then(|v| v.as_bool()).unwrap_or(false);
let skip_enforcement = std::env::var("TRUSTY_SKIP_PALACE_ENFORCEMENT").as_deref() == Ok("1");
if force {
crate::authz::authorize_force_palace_create(state)?;
}
if !skip_enforcement {
validate_slug_format(palace_name)?;
}
if !skip_enforcement && !force {
let cwd = args
.get("cwd")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(std::path::Path::new)
.map(|p| p.to_path_buf())
.or_else(|| std::env::current_dir().ok())
.unwrap_or_else(|| state.data_root.clone());
crate::project_root::validate_palace_name(palace_name, &cwd)?;
}
let description = args
.get("description")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let palace = Palace {
id: PalaceId::new(palace_name),
name: palace_name.to_string(),
description,
created_at: chrono::Utc::now(),
data_dir: state.data_root.join(palace_name),
};
let _handle = state
.registry
.create_palace(&state.data_root, palace)
.context("create_palace")?;
state
.palace_names
.insert(palace_name.to_string(), palace_name.to_string());
state.emit(DaemonEvent::PalaceCreated {
id: palace_name.to_string(),
name: palace_name.to_string(),
source: ActivitySource::Mcp,
});
let bootstrap_summary = match crate::bootstrap::bootstrap_palace(state, palace_name, None).await
{
Ok(r) => Some(serde_json::json!({
"triples_asserted": r.triples_asserted,
"project_subject": r.project_subject,
})),
Err(e) => {
tracing::warn!(
palace = %palace_name,
"auto-bootstrap on palace_create failed: {e:#}",
);
None
}
};
Ok(json!({
"palace_id": palace_name,
"status": "created",
"bootstrap": bootstrap_summary,
}))
}
pub(crate) async fn handle_palace_list(state: &AppState, _args: Value) -> Result<Value> {
let root = state.data_root.clone();
let palaces = tokio::task::spawn_blocking(move || {
trusty_common::memory_core::PalaceRegistry::list_palaces(&root)
})
.await
.context("join list_palaces")??;
let ids: Vec<String> = palaces.iter().map(|p| p.id.as_str().to_string()).collect();
Ok(json!({ "palaces": ids }))
}
pub(crate) async fn handle_palace_delete(state: &AppState, args: Value) -> Result<Value> {
let palace_id = args
.get("palace_id")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("palace_delete: missing 'palace_id'"))?
.to_string();
let force = args.get("force").and_then(|v| v.as_bool()).unwrap_or(false);
use crate::service::{MemoryService, ServiceError};
let svc = MemoryService::new(state.clone());
match svc.delete_palace(&palace_id, force).await {
Ok(()) => Ok(json!({ "deleted": palace_id })),
Err(ServiceError::NotFound(_)) => Err(anyhow!("Palace not found: {palace_id}")),
Err(ServiceError::Conflict(msg)) => Err(anyhow!(msg)),
Err(e) => Err(anyhow!("palace_delete: {e}")),
}
}
pub(crate) async fn handle_palace_update(state: &AppState, args: Value) -> Result<Value> {
let palace_id = args
.get("palace_id")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("palace_update: missing 'palace_id'"))?
.to_string();
let name = args
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("palace_update: missing 'name'"))?
.to_string();
use crate::service::MemoryService;
let svc = MemoryService::new(state.clone());
match svc.update_palace_name(&palace_id, &name).await {
Ok(_info) => Ok(json!({ "updated": palace_id, "name": name.trim() })),
Err(e) => Err(anyhow!("palace_update: {e}")),
}
}
pub(crate) async fn handle_palace_info(state: &AppState, args: Value) -> Result<Value> {
let palace = resolve_palace(state, &args, "palace_info")?;
let handle = open_palace_handle(state, &palace)?;
let drawer_count = handle.list_drawers(None, None, usize::MAX).len();
let data_dir = handle
.data_dir
.as_ref()
.map(|p| p.to_string_lossy().to_string());
let store = handle.kg.store();
let (room_count, wing_count) = tokio::task::spawn_blocking(move || {
anyhow::Ok((store.list_rooms()?.len(), store.list_wings()?.len()))
})
.await
.context("join palace_info counts")?
.context("count rooms and wings")?;
Ok(json!({
"id": handle.id.as_str(),
"name": handle.id.as_str(),
"drawer_count": drawer_count,
"room_count": room_count,
"wing_count": wing_count,
"data_dir": data_dir,
}))
}
pub(crate) async fn handle_palace_reembed(state: &AppState, args: Value) -> Result<Value> {
use trusty_common::memory_core::retrieval::VectorBackfillOptions;
let palace = resolve_palace(state, &args, "palace_reembed")?;
let handle = open_palace_handle(state, &palace)?;
let dry_run = args
.get("dry_run")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let limit = args
.get("limit")
.and_then(|v| v.as_u64())
.map(|n| n as usize);
let report = handle
.backfill_missing_vectors(VectorBackfillOptions {
dry_run,
limit,
..Default::default()
})
.await?;
let health = handle.embed_health();
Ok(json!({
"palace": report.palace_id,
"dry_run": report.dry_run,
"drawer_count": report.drawer_count,
"vector_count": report.vector_count,
"missing": report.missing,
"attempted": report.attempted,
"repaired": report.repaired,
"still_failing": report.still_failing,
"still_missing_ids": report.still_missing_ids
.iter().map(|i| i.to_string()).collect::<Vec<_>>(),
"embedder_ready": health.embedder_ready,
"recorded_failures": health.recorded_failures.len(),
"alias_audit": alias_audit_state(&report.alias_audit),
"alias_audit_error": report.alias_audit.unavailable_reason(),
"vector_key_rows": report.alias_audit.counts().map(|(rows, _)| rows),
"distinct_vector_ids": report.alias_audit.counts().map(|(_, ids)| ids),
"aliased": report.alias_audit.aliased_drawer_ids().map(<[Uuid]>::len),
"aliased_ids": report.alias_audit.aliased_drawer_ids()
.map(|ids| ids.iter().map(|i| i.to_string()).collect::<Vec<_>>()),
}))
}
pub(crate) async fn handle_palace_unalias(state: &AppState, args: Value) -> Result<Value> {
use trusty_common::memory_core::retrieval::{AliasRepairOptions, AliasRepairOutcome};
let palace = resolve_palace(state, &args, "palace_unalias")?;
let handle = open_palace_handle(state, &palace)?;
let dry_run = args
.get("dry_run")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let report =
tokio::task::spawn_blocking(move || handle.repair_aliases(AliasRepairOptions { dry_run }))
.await
.context("join palace_unalias")??;
let ids = |v: &[Uuid]| v.iter().map(|i| i.to_string()).collect::<Vec<_>>();
let (still_aliased_ids, not_freed_ids, unparsed_keys) = match &report.outcome {
AliasRepairOutcome::Partial {
still_aliased,
not_freed,
unparsed_keys,
} => (
Some(ids(still_aliased)),
Some(ids(not_freed)),
Some(unparsed_keys.clone()),
),
_ => (None, None, None),
};
let error = match &report.outcome {
AliasRepairOutcome::Unavailable { reason } => Some(reason.as_str()),
_ => None,
};
Ok(json!({
"palace": report.palace_id,
"dry_run": report.dry_run,
"outcome": report.outcome.as_str(),
"success": report.outcome.is_success(),
"freed_ids": ids(&report.freed_ids),
"aliased_before_ids": report.before.aliased_drawer_ids().map(ids),
"unnameable_keys": report.unnameable_keys.clone(),
"still_aliased_ids": still_aliased_ids,
"not_freed_ids": not_freed_ids,
"unparsed_keys": unparsed_keys,
"error": error,
"reembed_required": report.reembed_required(),
}))
}
fn alias_audit_state(audit: &trusty_common::memory_core::retrieval::AliasAudit) -> &'static str {
if audit.unavailable_reason().is_some() {
"unavailable"
} else if audit.is_clean() {
"clean"
} else {
"aliased"
}
}
pub(crate) async fn handle_palace_compact(state: &AppState, args: Value) -> Result<Value> {
let palace = resolve_palace(state, &args, "palace_compact")?;
let handle = open_palace_handle(state, &palace)?;
let res = handle.compact_vector_orphans().await?;
Ok(json!({
"palace": palace,
"total_checked": res.total_checked,
"orphans_removed": res.orphans_removed,
"index_size_before": res.index_size_before,
"index_size_after": res.index_size_after,
}))
}