#[cfg(feature = "daemon")]
use std::path::Path;
use std::path::PathBuf;
#[cfg(feature = "daemon")]
use std::sync::Arc;
#[cfg(feature = "daemon")]
use anyhow::Context as _;
use anyhow::Result;
#[cfg(feature = "daemon")]
use async_trait::async_trait;
#[cfg(feature = "daemon")]
use serde_json::Value;
#[cfg(feature = "daemon")]
use tracing::info;
#[cfg(feature = "daemon")]
use trusty_common::uds::server::{serve_until, RpcError, RpcFallback, RpcRouter, RpcServeOptions};
#[cfg(feature = "daemon")]
use crate::transport::methods::{activity, admin, chat, health, kg, palaces};
#[cfg(feature = "daemon")]
use crate::transport::rpc::{dispatch, JsonRpcRequest};
#[cfg(feature = "daemon")]
use crate::{is_data_dir_override_active, AppState};
pub const METHOD_HEALTH: &str = "memory.health";
pub const FOLDED_METHODS: &[&str] = &[
METHOD_HEALTH,
"memory.status",
"memory.config",
"memory.palace_get",
"memory.palaces_list",
"memory.drawers_list",
"memory.drawer_create",
"memory.drawer_delete",
"memory.kg_all",
"memory.kg_count",
"memory.kg_subjects_with_counts",
"memory.kg_graph",
"memory.kg_graph_seed",
"memory.kg_graph_neighbors",
"memory.kg_delete_triple",
"memory.dream_status",
"memory.palace_dream_status",
"memory.dream_run",
"memory.activity",
"memory.logs_tail",
"memory.admin_stop",
"memory.remember_async",
"memory.chat_providers",
"memory.messages_list",
"memory.message_send",
"memory.message_mark_read",
];
pub const STREAM_METHODS: &[&str] = &["memory.chat", "memory.activity_stream"];
pub const MAX_FRAME_BYTES: u64 = 32 * 1024 * 1024;
#[cfg(feature = "daemon")]
struct DispatchFallback {
state: AppState,
}
#[cfg(feature = "daemon")]
#[async_trait]
impl RpcFallback for DispatchFallback {
async fn call(&self, method: &str, params: Value) -> Result<Value, RpcError> {
let request = JsonRpcRequest {
jsonrpc: Some("2.0".to_string()),
id: None,
method: method.to_string(),
params: Some(params),
};
let response = dispatch(&self.state, request).await;
match (response.result, response.error) {
(Some(result), _) => Ok(result),
(None, Some(error)) => Err(RpcError::new(i64::from(error.code), error.message)),
(None, None) => Err(RpcError::internal(format!(
"dispatcher answered {method} with neither result nor error"
))),
}
}
}
#[cfg(feature = "daemon")]
pub fn dispatcher_method_count() -> usize {
crate::transport::rpc::method_names().len()
}
#[cfg(feature = "daemon")]
pub fn build_router(state: AppState) -> RpcRouter {
macro_rules! bind {
($router:expr, $name:expr, $req:ty, $handler:path) => {{
let state = state.clone();
$router.typed::<$req, Value, _, _>($name, move |req| {
let state = state.clone();
async move { $handler(&state, req).await.map_err(RpcError::from) }
})
}};
}
use crate::transport::methods::{NoParams, PalaceParams};
let router = RpcRouter::new();
let router = bind!(router, METHOD_HEALTH, health::HealthQuery, health::health);
let router = bind!(router, "memory.status", NoParams, palaces::status);
let router = bind!(router, "memory.config", NoParams, palaces::config);
let router = bind!(
router,
"memory.palace_get",
PalaceParams,
palaces::get_palace
);
let router = bind!(
router,
"memory.palaces_list",
NoParams,
palaces::palaces_list
);
let router = bind!(
router,
"memory.drawers_list",
palaces::ListDrawersParams,
palaces::list_drawers
);
let router = bind!(
router,
"memory.drawer_create",
palaces::CreateDrawerParams,
palaces::create_drawer
);
let router = bind!(
router,
"memory.drawer_delete",
palaces::DeleteDrawerParams,
palaces::delete_drawer
);
let router = bind!(router, "memory.kg_all", kg::KgListParams, kg::kg_all);
let router = bind!(router, "memory.kg_count", PalaceParams, kg::kg_count);
let router = bind!(
router,
"memory.kg_subjects_with_counts",
kg::KgListParams,
kg::kg_subjects_with_counts
);
let router = bind!(router, "memory.kg_graph", PalaceParams, kg::kg_graph);
let router = bind!(
router,
"memory.kg_graph_seed",
kg::KgSeedParams,
kg::kg_graph_seed
);
let router = bind!(
router,
"memory.kg_graph_neighbors",
kg::KgNeighborsParams,
kg::kg_graph_neighbors
);
let router = bind!(
router,
"memory.kg_delete_triple",
kg::DeleteTripleParams,
kg::kg_delete_triple
);
let router = bind!(router, "memory.dream_status", NoParams, kg::dream_status);
let router = bind!(
router,
"memory.palace_dream_status",
PalaceParams,
kg::palace_dream_status
);
let router = bind!(router, "memory.dream_run", NoParams, kg::dream_run);
let router = bind!(
router,
"memory.activity",
activity::ActivityParams,
activity::activity
);
let router = bind!(
router,
"memory.logs_tail",
admin::LogsTailParams,
admin::logs_tail
);
let router = bind!(router, "memory.admin_stop", NoParams, admin::admin_stop);
let router = bind!(
router,
"memory.remember_async",
admin::RememberAsyncParams,
admin::remember_async
);
let router = bind!(
router,
"memory.chat_providers",
NoParams,
chat::chat_providers
);
let router = bind!(
router,
"memory.messages_list",
chat::ListMessagesParams,
chat::messages_list
);
let router = bind!(
router,
"memory.message_send",
chat::SendMessageParams,
chat::message_send
);
let router = bind!(
router,
"memory.message_mark_read",
chat::MarkReadParams,
chat::message_mark_read
);
let chat_state = state.clone();
let router = router.typed_stream::<crate::chat::ChatBody, _, _>("memory.chat", move |body| {
let state = chat_state.clone();
async move { crate::chat::chat_stream(&state, body).await }
});
let activity_state = state.clone();
let router = router.typed_stream::<NoParams, _, _>("memory.activity_stream", move |_params| {
let state = activity_state.clone();
async move { activity::activity_stream(&state).await }
});
router.fallback(DispatchFallback { state })
}
#[cfg(feature = "daemon")]
fn serve_options() -> RpcServeOptions {
RpcServeOptions {
max_frame_bytes: MAX_FRAME_BYTES,
..RpcServeOptions::default()
}
}
pub fn socket_path() -> Result<PathBuf> {
trusty_common::daemon_socket_path("trusty-memory")
}
#[cfg(feature = "daemon")]
pub async fn serve(state: AppState, socket: &Path) -> Result<()> {
remove_retired_discovery_files();
serve_with_shutdown(state, socket, trusty_common::shutdown_signal()).await
}
#[cfg(feature = "daemon")]
pub async fn serve_with_shutdown(
state: AppState,
socket: &Path,
shutdown: impl std::future::Future<Output = ()> + Send,
) -> Result<()> {
let listener = trusty_common::uds::bind_singleton_hardened(socket)
.await
.with_context(|| format!("bind trusty-memory socket at {}", socket.display()))?;
let bm25 = state.bm25.clone();
let router = Arc::new(build_router(state));
info!(
socket = %socket.display(),
folded = router.method_names().count(),
streams = router.stream_names().count(),
dispatcher = dispatcher_method_count(),
"trusty-memory serving"
);
eprintln!("trusty-memory: serving on {}", socket.display());
serve_until(&listener, Arc::clone(&router), serve_options(), shutdown).await;
if let Some(lane) = bm25 {
flush_within_reserve(lane.shutdown(), trusty_common::shutdown::CLEANUP_RESERVE).await;
}
if let Err(e) = std::fs::remove_file(socket) {
tracing::debug!(socket = %socket.display(), error = %e, "socket already gone");
}
drop(listener);
Ok(())
}
#[cfg(feature = "daemon")]
async fn flush_within_reserve<F>(flush: F, budget: std::time::Duration) -> bool
where
F: std::future::Future<Output = ()>,
{
if tokio::time::timeout(budget, flush).await.is_err() {
tracing::warn!(
budget_secs = budget.as_secs(),
"BM25 exit flush exceeded the shutdown cleanup reserve and was \
abandoned so the socket is still unlinked; palaces not yet flushed \
keep the snapshot their last ticker published"
);
return false;
}
true
}
#[cfg(feature = "daemon")]
fn remove_retired_discovery_files() {
if let Ok(dir) = trusty_common::resolve_data_dir("trusty-memory") {
remove_if_present(&dir.join("http_addr"));
}
if is_data_dir_override_active() {
return;
}
if let Some(home) = dirs::home_dir() {
remove_if_present(&home.join(".trusty-memory").join("http_addr"));
}
}
#[cfg(feature = "daemon")]
fn remove_if_present(path: &Path) {
match std::fs::remove_file(path) {
Ok(()) => tracing::info!(
path = %path.display(),
"removed a retired http_addr discovery file (#6286)"
),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => tracing::debug!(
path = %path.display(),
error = %e,
"could not remove a retired http_addr discovery file"
),
}
}
#[cfg(all(test, feature = "daemon"))]
#[path = "uds_tests.rs"]
mod tests;