agents: in-flight node exclusion prevents concurrent collisions

Track which nodes are being processed across all concurrent agents.
When an agent claims seeds, it adds them and their strongly-connected
neighbors (score = link_strength * node_weight > 0.15) to a shared
HashSet. Concurrent agents filter these out when running their query,
ensuring they work on distant parts of the graph.

This replaces the eager-visit approach with a proper scheduling
mechanism: the daemon serializes seed selection while parallelizing
LLM work. The in-flight set is released on completion (or error).

Previously: core-personality rewritten 12x, irc-regulars 10x, same
node superseded 12x — concurrent agents all selected the same
high-degree hub nodes. Now they'll spread across the graph.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kent Overstreet 2026-03-20 12:45:24 -04:00
parent 3fc108a251
commit d0f126b709
4 changed files with 128 additions and 18 deletions

View file

@ -41,7 +41,20 @@ pub fn run_and_apply_with_log(
llm_tag: &str,
log: &dyn Fn(&str),
) -> Result<(), String> {
let result = run_one_agent(store, agent_name, batch_size, llm_tag, log, false)?;
run_and_apply_excluded(store, agent_name, batch_size, llm_tag, log, &Default::default())
}
/// Like run_and_apply_with_log but with an in-flight exclusion set.
/// Returns the keys that were processed (for the daemon to track).
pub fn run_and_apply_excluded(
store: &mut Store,
agent_name: &str,
batch_size: usize,
llm_tag: &str,
log: &dyn Fn(&str),
exclude: &std::collections::HashSet<String>,
) -> Result<(), String> {
let result = run_one_agent_excluded(store, agent_name, batch_size, llm_tag, log, false, exclude)?;
// Mark conversation segments as mined after successful processing
if agent_name == "observation" {
@ -88,18 +101,25 @@ pub fn run_one_agent(
llm_tag: &str,
log: &dyn Fn(&str),
debug: bool,
) -> Result<AgentResult, String> {
run_one_agent_excluded(store, agent_name, batch_size, llm_tag, log, debug, &Default::default())
}
/// Like run_one_agent but excludes nodes currently being worked on by other agents.
pub fn run_one_agent_excluded(
store: &mut Store,
agent_name: &str,
batch_size: usize,
llm_tag: &str,
log: &dyn Fn(&str),
debug: bool,
exclude: &std::collections::HashSet<String>,
) -> Result<AgentResult, String> {
let def = super::defs::get_def(agent_name)
.ok_or_else(|| format!("no .agent file for {}", agent_name))?;
log("building prompt");
let agent_batch = super::defs::run_agent(store, &def, batch_size)?;
// Eagerly record visits so concurrent agents pick different seeds.
// The not-visited query filter checks this timestamp.
if !agent_batch.node_keys.is_empty() {
store.record_agent_visits(&agent_batch.node_keys, agent_name).ok();
}
let agent_batch = super::defs::run_agent(store, &def, batch_size, exclude)?;
run_one_agent_inner(store, agent_name, &def, agent_batch, llm_tag, log, debug)
}