use super::*;
pub(crate) const NAVIGATION_LOOP_THRESHOLD: usize = 15;
pub(crate) const LISTING_LOOP_TRIP_COUNT: usize = 3;
pub(crate) const PLANNING_LISTING_LOOP_TRIP_COUNT: usize = 5;
pub(crate) const PLANNING_CONSECUTIVE_LOW_SIGNAL_THRESHOLD: u8 = 6;
pub(crate) const PLANNING_TOTAL_LOW_SIGNAL_THRESHOLD: u8 = 10;
pub(crate) const EXECUTION_TOTAL_LOW_SIGNAL_THRESHOLD: u8 = 12;
pub(crate) struct LoopTracker {
attempts: FxHashMap<String, (usize, Instant)>,
low_signal_attempts: FxHashMap<String, (usize, Instant)>,
pub(super) coarse_inspection_attempts: FxHashMap<String, (usize, Instant)>,
pub consecutive_mutations: usize,
pub verification_pending: bool,
pub fix_edits_remaining: u8,
pub verification_warning_emitted: bool,
pub verification_block_notice_emitted: bool,
pub verification_result_lost_notice_pending: bool,
pub piped_verification_notice_pending: bool,
pub verification_auto_recovery_attempts: u8,
pub auto_verification_executed: bool,
pub pending_verifier_session_id: Option<String>,
pub consecutive_navigations: usize,
pub navigation_loop_recoveries: usize,
pub consecutive_low_signal_navigations: u8,
pub total_low_signal_navigations: u8,
pub low_signal_tool_calls: u32,
pub planning_low_signal_synthesis_triggered: bool,
pub execution_total_low_signal_triggered: bool,
pub(super) nav_signatures: FxHashSet<String>,
}
impl LoopTracker {
pub(crate) fn new() -> Self {
Self {
attempts: FxHashMap::with_capacity_and_hasher(16, Default::default()),
low_signal_attempts: FxHashMap::with_capacity_and_hasher(8, Default::default()),
coarse_inspection_attempts: FxHashMap::with_capacity_and_hasher(8, Default::default()),
consecutive_mutations: 0,
verification_pending: false,
fix_edits_remaining: 0,
verification_warning_emitted: false,
verification_block_notice_emitted: false,
verification_result_lost_notice_pending: false,
piped_verification_notice_pending: false,
verification_auto_recovery_attempts: 0,
auto_verification_executed: false,
pending_verifier_session_id: None,
consecutive_navigations: 0,
navigation_loop_recoveries: 0,
consecutive_low_signal_navigations: 0,
total_low_signal_navigations: 0,
low_signal_tool_calls: 0,
planning_low_signal_synthesis_triggered: false,
execution_total_low_signal_triggered: false,
nav_signatures: FxHashSet::default(),
}
}
pub(crate) fn with_verification_snapshot(snapshot: (bool, u8)) -> Self {
let mut tracker = Self::new();
tracker.verification_pending = snapshot.0;
tracker.fix_edits_remaining = if snapshot.0 { snapshot.1 } else { 0 };
tracker
}
pub(crate) fn record(&mut self, signature: String) -> usize {
let entry = self.attempts.entry(signature).or_insert((0, Instant::now()));
entry.0 += 1;
entry.1 = Instant::now();
entry.0
}
pub(super) fn record_low_signal(&mut self, signature: String) -> usize {
let entry = self.low_signal_attempts.entry(signature).or_insert((0, Instant::now()));
entry.0 += 1;
entry.1 = Instant::now();
entry.0
}
pub(crate) fn max_count_filtered<F>(&self, exclude: F) -> usize
where
F: Fn(&str) -> bool,
{
self.attempts
.iter()
.filter_map(|(sig, (count, _))| if exclude(sig) { None } else { Some(*count) })
.max()
.unwrap_or(0)
}
pub(crate) fn max_low_signal_count(&self) -> usize {
self.low_signal_attempts.values().map(|(count, _)| *count).max().unwrap_or(0)
}
pub(crate) fn max_coarse_listing_count(&self) -> usize {
self.coarse_inspection_attempts
.iter()
.filter_map(|(family, (count, _))| {
family
.split("::")
.nth(2)
.is_some_and(|base| matches!(base, "ls" | "find" | "fd"))
.then_some(*count)
})
.max()
.unwrap_or(0)
}
pub(crate) fn dominant_churn(&self) -> Option<(String, usize)> {
let low_signal = self
.low_signal_attempts
.iter()
.max_by_key(|(_, (count, _))| *count)
.map(|(family, (count, _))| (family.clone(), *count));
let coarse = self
.coarse_inspection_attempts
.iter()
.max_by_key(|(_, (count, _))| *count)
.map(|(family, (count, _))| (family.clone(), *count));
match (low_signal, coarse) {
(Some(low), Some(coarse)) if coarse.1 > low.1 => Some(coarse),
(Some(low), _) => Some(low),
(None, Some(coarse)) => Some(coarse),
(None, None) => None,
}
}
pub(crate) fn repeated_navigation_count(&self) -> usize {
self.consecutive_navigations.saturating_sub(self.nav_signatures.len())
}
fn reset_low_signal_attempts(&mut self) {
self.low_signal_attempts.clear();
self.coarse_inspection_attempts.clear();
}
fn reset_low_signal_navigation_counters(&mut self) {
self.consecutive_low_signal_navigations = 0;
self.total_low_signal_navigations = 0;
}
pub(super) fn reset_navigation_window(&mut self, clear_low_signal_attempts: bool) {
self.consecutive_navigations = 0;
self.nav_signatures.clear();
self.reset_low_signal_navigation_counters();
if clear_low_signal_attempts {
self.reset_low_signal_attempts();
}
}
pub(super) fn record_navigation_signal(&mut self, is_low_signal: bool) {
if is_low_signal {
self.low_signal_tool_calls = self.low_signal_tool_calls.saturating_add(1);
self.consecutive_low_signal_navigations = self.consecutive_low_signal_navigations.saturating_add(1);
self.total_low_signal_navigations = self.total_low_signal_navigations.saturating_add(1);
} else {
self.consecutive_low_signal_navigations = 0;
}
}
pub(crate) fn reset_after_balancer_recovery(&mut self) {
self.attempts.clear();
self.reset_low_signal_attempts();
self.nav_signatures.clear();
self.consecutive_navigations = 0;
self.reset_low_signal_navigation_counters();
}
pub(crate) fn verification_is_pending(&self) -> bool {
self.verification_pending || self.consecutive_mutations >= BLIND_EDITING_THRESHOLD
}
pub(crate) fn verification_snapshot(&self) -> (bool, u8) {
(self.verification_is_pending(), self.fix_edits_remaining)
}
pub(crate) fn mark_verification_pending(&mut self) {
self.verification_pending = true;
}
pub(crate) fn take_verification_result_lost_notice(&mut self) -> bool {
std::mem::take(&mut self.verification_result_lost_notice_pending)
}
pub(crate) fn take_piped_verification_notice(&mut self) -> bool {
std::mem::take(&mut self.piped_verification_notice_pending)
}
pub(crate) fn record_failed_verification(&mut self) {
self.pending_verifier_session_id = None;
self.verification_pending = true;
self.fix_edits_remaining = FAILED_VERIFICATION_FIX_ALLOWANCE;
}
pub(super) fn record_successful_mutation(&mut self) {
if self.verification_pending && self.fix_edits_remaining > 0 {
self.fix_edits_remaining = self.fix_edits_remaining.saturating_sub(1);
return;
}
self.consecutive_mutations = self.consecutive_mutations.saturating_add(1);
if self.consecutive_mutations >= BLIND_EDITING_THRESHOLD {
self.verification_pending = true;
}
}
pub(crate) fn record_verification_auto_recovery_with_limit(&mut self, max_attempts: u8) -> bool {
if self.verification_auto_recovery_attempts >= max_attempts {
return false;
}
self.verification_auto_recovery_attempts = self.verification_auto_recovery_attempts.saturating_add(1);
true
}
pub(crate) fn verification_auto_recovery_attempts(&self) -> u8 {
self.verification_auto_recovery_attempts
}
pub(crate) fn should_auto_execute_verifier(&self) -> bool {
self.verification_is_pending()
&& (!self.auto_verification_executed || self.pending_verifier_session_id.is_some())
}
pub(crate) fn record_auto_verification_executed(&mut self) {
self.auto_verification_executed = true;
}
pub(super) fn mark_verification_complete(&mut self) {
self.pending_verifier_session_id = None;
self.consecutive_mutations = 0;
self.verification_pending = false;
self.fix_edits_remaining = 0;
self.verification_warning_emitted = false;
self.verification_block_notice_emitted = false;
self.verification_result_lost_notice_pending = false;
self.piped_verification_notice_pending = false;
self.verification_auto_recovery_attempts = 0;
self.auto_verification_executed = false;
}
}