use chrono::{DateTime, Utc};
use serde::Deserialize;
use serde_json::{Value, json};
#[derive(Debug, Clone)]
pub struct DrawerSummary {
pub title: String,
pub tags: Vec<String>,
pub created_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Deserialize)]
struct RawDrawer {
#[serde(default)]
content: String,
#[serde(default)]
tags: Vec<String>,
#[serde(default)]
created_at: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
struct MemoryListResult {
#[serde(default)]
drawers: Vec<RawDrawer>,
}
impl From<RawDrawer> for DrawerSummary {
fn from(r: RawDrawer) -> Self {
let created_at = r.created_at.as_deref().and_then(|s| s.parse().ok());
DrawerSummary {
title: r.content,
tags: r.tags,
created_at,
}
}
}
fn parse_memory_list_result(result: Value) -> anyhow::Result<Vec<DrawerSummary>> {
let parsed: MemoryListResult = serde_json::from_value(result)?;
let mut drawers: Vec<DrawerSummary> = parsed
.drawers
.into_iter()
.map(DrawerSummary::from)
.collect();
drawers.sort_by_key(|d| std::cmp::Reverse(d.created_at));
Ok(drawers)
}
pub async fn fetch_recent_palace_drawers(
memory_url: &str,
palace_id: &str,
limit: usize,
since: Option<DateTime<Utc>>,
) -> Option<Vec<DrawerSummary>> {
let params = json!({ "palace": palace_id, "limit": limit });
let result = match crate::mcp::memory_rpc::call_memory_tool_at(
memory_url,
"memory_list",
params,
)
.await
{
Ok(v) => v,
Err(e) => {
eprintln!("catchup: could not reach trusty-memory at {memory_url}: {e}");
return None;
}
};
let mut summaries = match parse_memory_list_result(result) {
Ok(d) => d,
Err(e) => {
eprintln!("catchup: could not parse memory_list response: {e}");
return None;
}
};
if let Some(since_ts) = since {
summaries.retain(|d| d.created_at.is_some_and(|ts| ts > since_ts));
}
Some(summaries)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_raw(title: &str, tags: Vec<&str>, created_at: Option<&str>) -> RawDrawer {
RawDrawer {
content: title.to_string(),
tags: tags.into_iter().map(|s| s.to_string()).collect(),
created_at: created_at.map(|s| s.to_string()),
}
}
#[test]
fn drawer_summary_from_raw() {
let raw = make_raw(
"My Drawer",
vec!["rust", "mpm"],
Some("2026-06-27T10:00:00Z"),
);
let summary = DrawerSummary::from(raw);
assert_eq!(summary.title, "My Drawer");
assert_eq!(summary.tags, vec!["rust", "mpm"]);
assert!(summary.created_at.is_some());
}
#[test]
fn drawer_since_filter() {
let raw_drawers = vec![
make_raw("Old", vec![], Some("2026-06-25T00:00:00Z")),
make_raw("New", vec![], Some("2026-06-27T00:00:00Z")),
];
let mut summaries: Vec<DrawerSummary> =
raw_drawers.into_iter().map(DrawerSummary::from).collect();
let since: DateTime<Utc> = "2026-06-26T00:00:00Z".parse().unwrap();
summaries.retain(|d| d.created_at.is_some_and(|ts| ts > since));
assert_eq!(summaries.len(), 1);
assert_eq!(summaries[0].title, "New");
}
#[test]
fn drawer_empty_array() {
let raw: Vec<RawDrawer> = vec![];
let summaries: Vec<DrawerSummary> = raw.into_iter().map(DrawerSummary::from).collect();
assert!(summaries.is_empty());
}
#[test]
fn drawer_missing_created_at_is_none() {
let raw = make_raw("No Date", vec![], None);
let s = DrawerSummary::from(raw);
assert!(s.created_at.is_none());
}
#[test]
fn parses_memory_list_result_and_sorts_newest_first() {
let result = json!({
"palace": "test-palace",
"drawers": [
{
"drawer_id": "old",
"content": "Old note",
"importance": 0.9,
"tags": ["a"],
"created_at": "2026-06-25T00:00:00Z",
},
{
"drawer_id": "new",
"content": "New note",
"importance": 0.1,
"tags": ["b"],
"created_at": "2026-06-27T00:00:00Z",
},
],
});
let drawers = parse_memory_list_result(result).unwrap();
assert_eq!(drawers.len(), 2);
assert_eq!(drawers[0].title, "New note");
assert_eq!(drawers[1].title, "Old note");
}
#[tokio::test]
#[ignore]
async fn live_drawer_fetch() {
let drawers =
fetch_recent_palace_drawers("http://127.0.0.1:19999", "test-palace", 5, None).await;
let _ = drawers;
}
}