agent visits: track when agents successfully process nodes

New append-only visits.capnp log records which agent processed which
node and when. Only recorded on successful completion — transient
errors don't mark nodes as "seen."

Schema: AgentVisit{nodeUuid, nodeKey, agent, timestamp, outcome}
Storage: append_visits(), replay_visits(), in-memory VisitIndex
Recording: daemon records visits after successful LLM call
API: agent_prompt() returns AgentBatch{prompt, node_keys} so callers
know which nodes to mark as visited.

Groundwork for using visit recency in agent node selection — agents
will deprioritize recently-visited nodes.
This commit is contained in:
ProofOfConcept 2026-03-10 14:30:53 -04:00
parent 9f14a29181
commit 0e1e5a1981
6 changed files with 237 additions and 34 deletions

View file

@ -137,10 +137,11 @@ fn job_consolidation_agent(
};
ctx.log_line(&format!("building prompt: {}", label));
let prompt = super::prompts::agent_prompt(&store, &agent, batch)?;
ctx.log_line(&format!("prompt: {} chars, calling Sonnet", prompt.len()));
let agent_batch = super::prompts::agent_prompt(&store, &agent, batch)?;
ctx.log_line(&format!("prompt: {} chars ({} nodes), calling Sonnet",
agent_batch.prompt.len(), agent_batch.node_keys.len()));
let response = super::llm::call_sonnet("consolidate", &prompt)?;
let response = super::llm::call_sonnet("consolidate", &agent_batch.prompt)?;
let ts = crate::store::format_datetime(crate::store::now_epoch())
.replace([':', '-', 'T'], "");
@ -148,6 +149,13 @@ fn job_consolidation_agent(
store.upsert_provenance(&report_key, &response,
crate::store::Provenance::AgentConsolidate).ok();
// Record visits for successfully processed nodes
if !agent_batch.node_keys.is_empty() {
if let Err(e) = store.record_agent_visits(&agent_batch.node_keys, &agent) {
ctx.log_line(&format!("visit recording: {}", e));
}
}
ctx.log_line(&format!("done: {} lines → {}", response.lines().count(), report_key));
Ok(())
})
@ -165,14 +173,16 @@ fn job_rename_agent(
let batch = if batch_size == 0 { 10 } else { batch_size };
ctx.log_line(&format!("building prompt: rename (batch={})", batch));
let prompt = super::prompts::agent_prompt(&store, "rename", batch)?;
ctx.log_line(&format!("prompt: {} chars, calling Sonnet", prompt.len()));
let agent_batch = super::prompts::agent_prompt(&store, "rename", batch)?;
ctx.log_line(&format!("prompt: {} chars ({} nodes), calling Sonnet",
agent_batch.prompt.len(), agent_batch.node_keys.len()));
let response = super::llm::call_sonnet("consolidate", &prompt)?;
let response = super::llm::call_sonnet("consolidate", &agent_batch.prompt)?;
// Parse RENAME actions directly from response
let mut applied = 0;
let mut skipped = 0;
let mut successfully_renamed: Vec<String> = Vec::new();
for line in response.lines() {
let trimmed = line.trim();
if !trimmed.starts_with("RENAME ") { continue; }
@ -208,6 +218,7 @@ fn job_rename_agent(
match store.rename_node(&resolved, new_key) {
Ok(()) => {
ctx.log_line(&format!("renamed: {}{}", resolved, new_key));
successfully_renamed.push(new_key.to_string());
applied += 1;
}
Err(e) => {
@ -221,6 +232,13 @@ fn job_rename_agent(
store.save()?;
}
// Record visits for successfully renamed nodes
if !successfully_renamed.is_empty() {
if let Err(e) = store.record_agent_visits(&successfully_renamed, "rename") {
ctx.log_line(&format!("visit recording: {}", e));
}
}
// Also store the report for auditing
let ts = crate::store::format_datetime(crate::store::now_epoch())
.replace([':', '-', 'T'], "");