journal tools: use NodeType instead of string key matching

- journal_new: create EpisodicSession node with auto-generated key
- journal_tail: query by node_type, not by parsing a monolithic node
- journal_update: find latest EpisodicSession by timestamp
- No string key matching anywhere — all typed
- Fixes journal entries not appearing in 'poc-memory journal tail'
- Also: added --provenance/-p filter to 'poc-memory tail'
- Also: fix early return in surface_observe_cycle store load failure
- Also: scale max_turns by number of steps (50 per step)

Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
ProofOfConcept 2026-03-26 18:41:10 -04:00
parent 41fcec58f0
commit 85fa54cba9
5 changed files with 72 additions and 54 deletions

View file

@ -153,42 +153,42 @@ pub fn dispatch(name: &str, args: &serde_json::Value, provenance: Option<&str>)
"journal_tail" => {
let count = args.get("count").and_then(|v| v.as_u64()).unwrap_or(1) as usize;
let store = Store::load().map_err(|e| anyhow::anyhow!("{}", e))?;
let content = store.nodes.get("journal")
.map(|n| n.content.as_str())
.unwrap_or("");
let mut entries: Vec<&str> = Vec::new();
let mut remaining = content;
while let Some(pos) = remaining.rfind("\n## ") {
entries.push(&remaining[pos + 1..]);
remaining = &remaining[..pos];
if entries.len() >= count { break; }
}
if entries.len() < count && remaining.starts_with("## ") {
entries.push(remaining);
}
entries.reverse();
if entries.is_empty() {
let mut entries: Vec<&crate::store::Node> = store.nodes.values()
.filter(|n| n.node_type == crate::store::NodeType::EpisodicSession)
.collect();
entries.sort_by_key(|n| n.timestamp);
let start = entries.len().saturating_sub(count);
if entries[start..].is_empty() {
Ok("(no journal entries)".into())
} else {
Ok(entries.join("\n\n"))
Ok(entries[start..].iter()
.map(|n| n.content.as_str())
.collect::<Vec<_>>()
.join("\n\n"))
}
}
"journal_new" => {
let title = get_str(args, "title")?;
let body = get_str(args, "body")?;
let ts = chrono::Local::now().format("%Y-%m-%dT%H:%M");
let entry = format!("## {}{}\n\n{}", ts, title, body);
let content = format!("## {}{}\n\n{}", ts, title, body);
let slug: String = title.split_whitespace()
.take(6)
.map(|w| w.to_lowercase()
.chars().filter(|c| c.is_alphanumeric() || *c == '-')
.collect::<String>())
.collect::<Vec<_>>()
.join("-");
let slug = if slug.len() > 50 { &slug[..50] } else { &slug };
let key = format!("journal-j-{}-{}",
ts.to_string().to_lowercase().replace(':', "-"), slug);
let mut store = Store::load().map_err(|e| anyhow::anyhow!("{}", e))?;
let existing = store.nodes.get("journal")
.map(|n| n.content.clone())
.unwrap_or_default();
let new_content = if existing.is_empty() {
entry.clone()
} else {
format!("{}\n\n{}", existing.trim_end(), entry)
};
store.upsert_provenance("journal", &new_content, prov)
.map_err(|e| anyhow::anyhow!("{}", e))?;
let mut node = crate::store::new_node(&key, &content);
node.node_type = crate::store::NodeType::EpisodicSession;
node.provenance = prov.to_string();
store.upsert_node(node).map_err(|e| anyhow::anyhow!("{}", e))?;
store.save().map_err(|e| anyhow::anyhow!("{}", e))?;
let word_count = body.split_whitespace().count();
Ok(format!("New entry '{}' ({} words)", title, word_count))
@ -196,14 +196,17 @@ pub fn dispatch(name: &str, args: &serde_json::Value, provenance: Option<&str>)
"journal_update" => {
let body = get_str(args, "body")?;
let mut store = Store::load().map_err(|e| anyhow::anyhow!("{}", e))?;
let existing = store.nodes.get("journal")
.map(|n| n.content.clone())
.unwrap_or_default();
if existing.is_empty() {
// Find most recent EpisodicSession node
let latest_key = store.nodes.values()
.filter(|n| n.node_type == crate::store::NodeType::EpisodicSession)
.max_by_key(|n| n.timestamp)
.map(|n| n.key.clone());
let Some(key) = latest_key else {
anyhow::bail!("no journal entry to update — use journal_new first");
}
};
let existing = store.nodes.get(&key).unwrap().content.clone();
let new_content = format!("{}\n\n{}", existing.trim_end(), body);
store.upsert_provenance("journal", &new_content, prov)
store.upsert_provenance(&key, &new_content, prov)
.map_err(|e| anyhow::anyhow!("{}", e))?;
store.save().map_err(|e| anyhow::anyhow!("{}", e))?;
let word_count = body.split_whitespace().count();