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:
parent
41fcec58f0
commit
85fa54cba9
5 changed files with 72 additions and 54 deletions
|
|
@ -153,42 +153,42 @@ pub fn dispatch(name: &str, args: &serde_json::Value, provenance: Option<&str>)
|
||||||
"journal_tail" => {
|
"journal_tail" => {
|
||||||
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) as usize;
|
||||||
let store = Store::load().map_err(|e| anyhow::anyhow!("{}", e))?;
|
let store = Store::load().map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
let content = store.nodes.get("journal")
|
let mut entries: Vec<&crate::store::Node> = store.nodes.values()
|
||||||
.map(|n| n.content.as_str())
|
.filter(|n| n.node_type == crate::store::NodeType::EpisodicSession)
|
||||||
.unwrap_or("");
|
.collect();
|
||||||
let mut entries: Vec<&str> = Vec::new();
|
entries.sort_by_key(|n| n.timestamp);
|
||||||
let mut remaining = content;
|
let start = entries.len().saturating_sub(count);
|
||||||
while let Some(pos) = remaining.rfind("\n## ") {
|
if entries[start..].is_empty() {
|
||||||
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() {
|
|
||||||
Ok("(no journal entries)".into())
|
Ok("(no journal entries)".into())
|
||||||
} else {
|
} else {
|
||||||
Ok(entries.join("\n\n"))
|
Ok(entries[start..].iter()
|
||||||
|
.map(|n| n.content.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n\n"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"journal_new" => {
|
"journal_new" => {
|
||||||
let title = get_str(args, "title")?;
|
let title = get_str(args, "title")?;
|
||||||
let body = get_str(args, "body")?;
|
let body = get_str(args, "body")?;
|
||||||
let ts = chrono::Local::now().format("%Y-%m-%dT%H:%M");
|
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 mut store = Store::load().map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
let existing = store.nodes.get("journal")
|
let mut node = crate::store::new_node(&key, &content);
|
||||||
.map(|n| n.content.clone())
|
node.node_type = crate::store::NodeType::EpisodicSession;
|
||||||
.unwrap_or_default();
|
node.provenance = prov.to_string();
|
||||||
let new_content = if existing.is_empty() {
|
store.upsert_node(node).map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
entry.clone()
|
|
||||||
} else {
|
|
||||||
format!("{}\n\n{}", existing.trim_end(), entry)
|
|
||||||
};
|
|
||||||
store.upsert_provenance("journal", &new_content, prov)
|
|
||||||
.map_err(|e| anyhow::anyhow!("{}", e))?;
|
|
||||||
store.save().map_err(|e| anyhow::anyhow!("{}", e))?;
|
store.save().map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
let word_count = body.split_whitespace().count();
|
let word_count = body.split_whitespace().count();
|
||||||
Ok(format!("New entry '{}' ({} words)", title, word_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" => {
|
"journal_update" => {
|
||||||
let body = get_str(args, "body")?;
|
let body = get_str(args, "body")?;
|
||||||
let mut store = Store::load().map_err(|e| anyhow::anyhow!("{}", e))?;
|
let mut store = Store::load().map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
let existing = store.nodes.get("journal")
|
// Find most recent EpisodicSession node
|
||||||
.map(|n| n.content.clone())
|
let latest_key = store.nodes.values()
|
||||||
.unwrap_or_default();
|
.filter(|n| n.node_type == crate::store::NodeType::EpisodicSession)
|
||||||
if existing.is_empty() {
|
.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");
|
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);
|
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))?;
|
.map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
store.save().map_err(|e| anyhow::anyhow!("{}", e))?;
|
store.save().map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
let word_count = body.split_whitespace().count();
|
let word_count = body.split_whitespace().count();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// cli/journal.rs — journal subcommand handlers
|
// cli/journal.rs — journal subcommand handlers
|
||||||
|
|
||||||
|
|
||||||
pub fn cmd_tail(n: usize, full: bool) -> Result<(), String> {
|
pub fn cmd_tail(n: usize, full: bool, provenance: Option<&str>) -> Result<(), String> {
|
||||||
let path = crate::store::nodes_path();
|
let path = crate::store::nodes_path();
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return Err("No node log found".into());
|
return Err("No node log found".into());
|
||||||
|
|
@ -24,6 +24,11 @@ pub fn cmd_tail(n: usize, full: bool) -> Result<(), String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by provenance if specified (prefix match)
|
||||||
|
if let Some(prov) = provenance {
|
||||||
|
entries.retain(|n| n.provenance.contains(prov));
|
||||||
|
}
|
||||||
|
|
||||||
let start = entries.len().saturating_sub(n);
|
let start = entries.len().saturating_sub(n);
|
||||||
for node in &entries[start..] {
|
for node in &entries[start..] {
|
||||||
let ts = if node.timestamp > 0 && node.timestamp < 4_000_000_000 {
|
let ts = if node.timestamp > 0 && node.timestamp < 4_000_000_000 {
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,8 @@ fn surface_observe_cycle(session: &Session, out: &mut String, log_f: &mut File)
|
||||||
// Read surface output and inject into context
|
// Read surface output and inject into context
|
||||||
let surface_path = state_dir.join("surface");
|
let surface_path = state_dir.join("surface");
|
||||||
if let Ok(content) = fs::read_to_string(&surface_path) {
|
if let Ok(content) = fs::read_to_string(&surface_path) {
|
||||||
let Ok(store) = crate::store::Store::load() else { return; };
|
match crate::store::Store::load() {
|
||||||
|
Ok(store) => {
|
||||||
let mut seen = session.seen();
|
let mut seen = session.seen();
|
||||||
let seen_path = session.path("seen");
|
let seen_path = session.path("seen");
|
||||||
for key in content.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
|
for key in content.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
|
||||||
|
|
@ -173,6 +174,11 @@ fn surface_observe_cycle(session: &Session, out: &mut String, log_f: &mut File)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let _ = writeln!(log_f, "error loading store: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
// Clear surface output after consuming
|
// Clear surface output after consuming
|
||||||
fs::remove_file(&surface_path).ok();
|
fs::remove_file(&surface_path).ok();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,9 @@ enum Command {
|
||||||
/// Show full content
|
/// Show full content
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
full: bool,
|
full: bool,
|
||||||
|
/// Filter by provenance (substring match, e.g. "surface-observe")
|
||||||
|
#[arg(long, short)]
|
||||||
|
provenance: Option<String>,
|
||||||
},
|
},
|
||||||
/// Summary of memory state
|
/// Summary of memory state
|
||||||
Status,
|
Status,
|
||||||
|
|
@ -782,7 +785,8 @@ impl Run for Command {
|
||||||
Self::Write { key } => cli::node::cmd_write(&key),
|
Self::Write { key } => cli::node::cmd_write(&key),
|
||||||
Self::Edit { key } => cli::node::cmd_edit(&key),
|
Self::Edit { key } => cli::node::cmd_edit(&key),
|
||||||
Self::History { full, key } => cli::node::cmd_history(&key, full),
|
Self::History { full, key } => cli::node::cmd_history(&key, full),
|
||||||
Self::Tail { n, full } => cli::journal::cmd_tail(n, full),
|
Self::Tail { n, full, provenance }
|
||||||
|
=> cli::journal::cmd_tail(n, full, provenance.as_deref()),
|
||||||
Self::Status => cli::misc::cmd_status(),
|
Self::Status => cli::misc::cmd_status(),
|
||||||
Self::Query { expr } => cli::misc::cmd_query(&expr),
|
Self::Query { expr } => cli::misc::cmd_query(&expr),
|
||||||
Self::Used { key } => cli::node::cmd_used(&key),
|
Self::Used { key } => cli::node::cmd_used(&key),
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ pub async fn call_api_with_tools(
|
||||||
let mut next_prompt_idx = 1; // index of next prompt to inject
|
let mut next_prompt_idx = 1; // index of next prompt to inject
|
||||||
let reasoning = crate::config::get().api_reasoning.clone();
|
let reasoning = crate::config::get().api_reasoning.clone();
|
||||||
|
|
||||||
let max_turns = 50;
|
let max_turns = 50 * prompts.len();
|
||||||
for turn in 0..max_turns {
|
for turn in 0..max_turns {
|
||||||
log(&format!("\n=== TURN {} ({} messages) ===\n", turn, messages.len()));
|
log(&format!("\n=== TURN {} ({} messages) ===\n", turn, messages.len()));
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue