diff --git a/src/app/runtime/actions/mod.rs b/src/app/runtime/actions/mod.rs index 53b5a12..9a2ad4c 100644 --- a/src/app/runtime/actions/mod.rs +++ b/src/app/runtime/actions/mod.rs @@ -992,10 +992,19 @@ fn run_agent_turn( // ── AUTO CEO PIPELINE ── // Before the main agent starts working, check if the pipeline should run. - let user_msg_count = msgs.iter() - .filter(|m| matches!(m.role, crate::dto::chat::message::Role::User)) - .count(); - let should_pipeline = if user_msg_count <= 2 { + // Gated on whether a hive-mind convergence has already happened earlier + // in this session (detected from message content), not an arbitrary + // message-count cutoff — a complex request in message 5 deserves the + // same treatment as one in message 1, as long as this session hasn't + // already converged once. + let already_ran_hive_mind = crate::app::workflow::hive_mind::hive_mind_already_ran( + msgs.iter() + .filter(|m| matches!(m.role, crate::dto::chat::message::Role::System)) + .filter_map(|m| m.content.as_deref()) + ); + let should_pipeline = if already_ran_hive_mind { + false + } else { let user_request = msgs.iter() .rev().find(|m| matches!(m.role, crate::dto::chat::message::Role::User)) .and_then(|m| m.content.as_deref()) @@ -1006,8 +1015,6 @@ fn run_agent_turn( } else { crate::app::workflow::hive_mind::is_complex_request(user_request) } - } else { - false }; if should_pipeline { @@ -1112,7 +1119,8 @@ fn run_agent_turn( tracing::info!("[hive-mind] convergence completed successfully"); let pipeline_msg = ChatMessage::system(format!( - "[Hive-Mind Consensus]\n{consensus}", + "{}\n{consensus}", + crate::app::workflow::hive_mind::HIVE_MIND_CONSENSUS_TAG, )); archive_message(tc.db.as_ref(), &tc.session_id, &pipeline_msg); msgs.push(pipeline_msg); diff --git a/src/app/workflow/hive_mind.rs b/src/app/workflow/hive_mind.rs index 5814bf3..b573112 100644 --- a/src/app/workflow/hive_mind.rs +++ b/src/app/workflow/hive_mind.rs @@ -69,6 +69,27 @@ pub struct NodeReport { pub output: String, } +/// Tag prefixing the system message `run_hive_mind`'s caller pushes into +/// the conversation after a successful convergence. Shared between the +/// push site (`actions/mod.rs`) and `hive_mind_already_ran` below so the +/// two can never drift out of sync. +pub const HIVE_MIND_CONSENSUS_TAG: &str = "[Hive-Mind Consensus]"; + +/// Detect whether a hive-mind convergence has already run earlier in this +/// conversation, by checking prior system-message bodies for the +/// consensus tag. +/// +/// Why: gates re-triggering the Core Intelligence pipeline more than once +/// per session on message *content* actually observed, rather than an +/// arbitrary "first two user messages" cutoff that silently disabled the +/// pipeline for any complex request phrased later in a long conversation. +/// +/// Return: `true` if any prior system message starts with +/// `HIVE_MIND_CONSENSUS_TAG`. +pub fn hive_mind_already_ran<'a>(system_message_bodies: impl Iterator) -> bool { + system_message_bodies.into_iter().any(|body| body.starts_with(HIVE_MIND_CONSENSUS_TAG)) +} + /// Build the live-state callback that forwards node status updates to the /// TUI's workflow panel. fn build_live( @@ -408,4 +429,19 @@ mod tests { let node_id = format!("Node-{}-{}", 2, 1); assert_eq!(node_id, "Node-2-1"); } + + #[test] + fn hive_mind_already_ran_detects_prior_consensus_tag() { + let bodies = vec![ + "you are a helpful assistant".to_string(), + format!("{HIVE_MIND_CONSENSUS_TAG}\nthe bug is a null check"), + ]; + assert!(hive_mind_already_ran(bodies.iter().map(std::string::String::as_str))); + } + + #[test] + fn hive_mind_already_ran_false_when_no_prior_convergence() { + let bodies = vec!["you are a helpful assistant".to_string()]; + assert!(!hive_mind_already_ran(bodies.iter().map(std::string::String::as_str))); + } }