use std::sync::Mutex;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PheromoneSignal {
pub agent_id: String,
pub kind: SignalKind,
pub path: String,
pub symbol: Option<String>,
pub strength: f64,
pub deposited_at: DateTime<Utc>,
pub note: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum SignalKind {
Active,
Complexity,
ReviewNeeded,
Issue,
Completed,
Exploration,
}
static SIGNALS: Mutex<Vec<PheromoneSignal>> = Mutex::new(Vec::new());
pub(crate) fn deposit_signal(mut signal: PheromoneSignal) {
signal.strength = bounded(signal.strength);
signals().push(signal);
}
pub(crate) fn read_signals(path: &str, kind: Option<SignalKind>) -> Vec<PheromoneSignal> {
signals()
.iter()
.filter(|signal| signal.path == path && kind.is_none_or(|kind| signal.kind == kind))
.cloned()
.collect()
}
pub(crate) fn evaporate(decay_rate: f64, threshold: f64) {
let decay_rate = bounded(decay_rate);
let threshold = bounded(threshold);
let mut signals = signals();
for signal in signals.iter_mut() {
signal.strength *= 1.0 - decay_rate;
}
signals.retain(|signal| signal.strength >= threshold);
}
pub(crate) fn reset_signals() {
signals().clear();
}
fn signals() -> std::sync::MutexGuard<'static, Vec<PheromoneSignal>> {
SIGNALS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn bounded(value: f64) -> f64 {
if value.is_finite() {
value.clamp(0.0, 1.0)
} else {
0.0
}
}
#[cfg(test)]
mod tests {
use super::*;
static TEST_LOCK: Mutex<()> = Mutex::new(());
fn signal(path: &str, kind: SignalKind, strength: f64) -> PheromoneSignal {
PheromoneSignal {
agent_id: "codex-test".to_string(),
kind,
path: path.to_string(),
symbol: None,
strength,
deposited_at: Utc::now(),
note: None,
}
}
fn setup() -> std::sync::MutexGuard<'static, ()> {
let guard = TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
reset_signals();
guard
}
#[test]
fn deposit_and_read_signals() {
let _guard = setup();
deposit_signal(signal("src/lib.rs", SignalKind::Active, 0.8));
let found = read_signals("src/lib.rs", None);
assert_eq!(found.len(), 1);
assert_eq!(found[0].agent_id, "codex-test");
assert_eq!(found[0].strength, 0.8);
}
#[test]
fn read_filters_by_path() {
let _guard = setup();
deposit_signal(signal("src/lib.rs", SignalKind::Active, 0.8));
deposit_signal(signal("src/main.rs", SignalKind::Active, 0.6));
let found = read_signals("src/main.rs", None);
assert_eq!(found.len(), 1);
assert_eq!(found[0].path, "src/main.rs");
}
#[test]
fn read_filters_by_kind() {
let _guard = setup();
deposit_signal(signal("src/lib.rs", SignalKind::Active, 0.8));
deposit_signal(signal("src/lib.rs", SignalKind::ReviewNeeded, 0.6));
let found = read_signals("src/lib.rs", Some(SignalKind::ReviewNeeded));
assert_eq!(found.len(), 1);
assert_eq!(found[0].kind, SignalKind::ReviewNeeded);
}
#[test]
fn evaporate_reduces_strength() {
let _guard = setup();
deposit_signal(signal("src/lib.rs", SignalKind::Active, 0.8));
evaporate(0.25, 0.0);
let found = read_signals("src/lib.rs", None);
assert!((found[0].strength - 0.6).abs() < f64::EPSILON);
}
#[test]
fn evaporate_removes_weak_signals() {
let _guard = setup();
deposit_signal(signal("src/lib.rs", SignalKind::Active, 0.2));
evaporate(0.5, 0.11);
assert!(read_signals("src/lib.rs", None).is_empty());
}
}