journal_tail: thin wrapper around memory_query

Instead of reimplementing filtering logic, journal_tail builds a
query string (type + sort + age + limit) and delegates to query().
Supports format and after parameters. Removes keys_only in favor
of format:"compact". Digest agent updated to use dates not key names.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
ProofOfConcept 2026-04-10 16:09:46 -04:00
parent db49f49958
commit 1cf51876a8
2 changed files with 36 additions and 50 deletions

View file

@ -94,9 +94,10 @@ pub fn journal_tools() -> [super::Tool; 3] {
parameters_json: r#"{
"type": "object",
"properties": {
"count": {"type": "integer", "description": "Number of entries", "default": 1},
"level": {"type": "integer", "description": "0=journal, 1=daily, 2=weekly, 3=monthly", "default": 0},
"keys_only": {"type": "boolean", "description": "Return only node keys, not content", "default": false}
"count": {"type": "integer", "description": "Number of entries", "default": 1},
"level": {"type": "integer", "description": "0=journal, 1=daily, 2=weekly, 3=monthly", "default": 0},
"format": {"type": "string", "description": "compact or full (with content)", "default": "full"},
"after": {"type": "string", "description": "Only entries after this date (YYYY-MM-DD)"}
}
}"#,
handler: Arc::new(|_a, v| Box::pin(async move { journal_tail(&v).await })) },
@ -280,40 +281,31 @@ async fn query(args: &serde_json::Value) -> Result<String> {
// ── Journal tools ──────────────────────────────────────────────
async fn journal_tail(args: &serde_json::Value) -> Result<String> {
use crate::store::NodeType;
let count = args.get("count").and_then(|v| v.as_u64()).unwrap_or(1) as usize;
let count = args.get("count").and_then(|v| v.as_u64()).unwrap_or(1);
let level = args.get("level").and_then(|v| v.as_u64()).unwrap_or(0);
let keys_only = args.get("keys_only").and_then(|v| v.as_bool()).unwrap_or(false);
let format = args.get("format").and_then(|v| v.as_str()).unwrap_or("full");
let after = args.get("after").and_then(|v| v.as_str());
let node_type = match level {
0 => NodeType::EpisodicSession,
1 => NodeType::EpisodicDaily,
2 => NodeType::EpisodicWeekly,
3 => NodeType::EpisodicMonthly,
let type_name = match level {
0 => "episodic",
1 => "daily",
2 => "weekly",
3 => "monthly",
_ => return Err(anyhow::anyhow!("invalid level: {} (0=journal, 1=daily, 2=weekly, 3=monthly)", level)),
};
let arc = cached_store().await?;
let store = arc.lock().await;
let mut entries: Vec<&crate::store::Node> = store.nodes.values()
.filter(|n| n.node_type == node_type)
.collect();
entries.sort_by_key(|n| n.created_at);
let start = entries.len().saturating_sub(count);
if entries[start..].is_empty() {
Ok("(no entries)".into())
} else if keys_only {
Ok(entries[start..].iter()
.map(|n| n.key.as_str())
.collect::<Vec<_>>()
.join("\n"))
} else {
Ok(entries[start..].iter()
.map(|n| format!("## {}\n\n{}", n.key, n.content))
.collect::<Vec<_>>()
.join("\n\n---\n\n"))
let mut q = format!("all | type:{} | sort:timestamp", type_name);
if let Some(date) = after {
// Convert date to age in seconds
if let Ok(nd) = chrono::NaiveDate::parse_from_str(date, "%Y-%m-%d") {
let ts = nd.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp();
let age = chrono::Utc::now().timestamp() - ts;
q.push_str(&format!(" | age:<{}", age));
}
}
q.push_str(&format!(" | limit:{}", count));
query(&serde_json::json!({"query": q, "format": format})).await
}
async fn journal_new(agent: &Option<std::sync::Arc<crate::agent::Agent>>, args: &serde_json::Value) -> Result<String> {