use clap::Parser;
use ix::builder::Builder;
use ix::format::Beacon;
use ix::idle::IdleTracker;
use ix::watcher::Watcher;
use llmosafe::{EscalationPolicy, ResourceGuard, SafetyDecision};
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
const ENTROPY_SAFE_THRESHOLD: u16 = 800; const ENTROPY_CRITICAL: u16 = 1000; const PRE_BUILD_WAIT_SECS: u64 = 5; const WARN_COOLDOWN_MS: u64 = 300;
#[derive(Parser)]
#[command(
name = "ixd",
version = env!("CARGO_PKG_VERSION"),
about = "Background daemon for automatic indexing with safety monitoring."
)]
struct Cli {
#[arg(default_value = ".", value_name = "PATH")]
path: PathBuf,
}
fn main() -> ix::error::Result<()> {
let cli = Cli::parse();
let root = cli.path.canonicalize().map_err(ix::error::Error::Io)?;
println!("ixd: watching {}...", root.display());
let running = Arc::new(AtomicBool::new(true));
{
use nix::sys::signal::{SaFlags, SigAction, SigHandler, SigSet, Signal, sigaction};
let action = SigAction::new(
SigHandler::Handler(handle_signal),
SaFlags::empty(),
SigSet::empty(),
);
unsafe {
sigaction(Signal::SIGTERM, &action).expect("failed to install SIGTERM handler");
sigaction(Signal::SIGINT, &action).expect("failed to install SIGINT handler");
}
}
let ix_dir_early = root.join(".ix");
let beacon_path = ix_dir_early.join("beacon.json");
if beacon_path.exists()
&& let Ok(existing) = ix::format::Beacon::read_from(&ix_dir_early)
{
let pid = nix::unistd::Pid::from_raw(existing.pid);
if nix::sys::signal::kill(pid, None).is_ok() {
eprintln!(
"ixd: another instance is already watching {} (PID {}). \
Stop it first or remove {}/beacon.json.",
root.display(),
existing.pid,
ix_dir_early.display()
);
std::process::exit(1);
}
eprintln!("ixd: removing stale beacon from PID {}", existing.pid);
let _ = std::fs::remove_file(&beacon_path);
}
let guard = ResourceGuard::auto(0.6);
let mut builder = match Builder::new(&root) {
Ok(b) => b.with_resource_guard(guard.clone()),
Err(e) => {
eprintln!("ixd: cannot create index in {}: {}", root.display(), e);
return Err(e);
}
};
let pre_build_timeout = Duration::from_secs(30);
let pre_build_start = std::time::Instant::now();
while pre_build_start.elapsed() < pre_build_timeout {
match guard.check_blocking() {
Ok(_) => break, Err(e) => {
eprintln!(
"ixd: memory pressure before initial build: {:?} — waiting...",
e
);
std::thread::sleep(Duration::from_secs(PRE_BUILD_WAIT_SECS));
}
}
}
if let Err(e) = builder.build() {
eprintln!(
"ixd: initial build failed: {} — will watch for changes anyway",
e
);
} else {
println!(
"ixd: initial build complete ({} files, {} trigrams)",
builder.files_len(),
builder.trigrams_len()
);
}
let mut watcher = Watcher::new(&root);
let rx = watcher.start()?;
let ix_dir = root.join(".ix");
if !ix_dir.exists() {
fs::create_dir_all(&ix_dir)?;
}
let mut beacon = Beacon::new(&root);
beacon.write_to(&ix_dir)?;
let mut idle = IdleTracker::new();
run_main_loop(
&mut builder, &rx, &ix_dir, &mut beacon, &mut idle, &guard, &running,
);
eprintln!("ixd: shutting down...");
watcher.stop();
let _ = fs::remove_file(ix_dir.join("beacon.json"));
Ok(())
}
fn run_main_loop(
builder: &mut Builder,
rx: &crossbeam_channel::Receiver<Vec<PathBuf>>,
ix_dir: &std::path::Path,
beacon: &mut Beacon,
idle: &mut IdleTracker,
guard: &ResourceGuard,
running: &Arc<AtomicBool>,
) {
loop {
if SHUTDOWN.load(Ordering::SeqCst) {
running.store(false, Ordering::SeqCst);
break;
}
match rx.recv_timeout(Duration::from_millis(500)) {
Ok(changed_files) => {
handle_changes(builder, &changed_files, ix_dir, beacon, idle, guard);
}
Err(crossbeam_channel::RecvTimeoutError::Timeout) => {}
Err(crossbeam_channel::RecvTimeoutError::Disconnected) => break,
}
}
}
fn handle_changes(
builder: &mut Builder,
changed_files: &[PathBuf],
ix_dir: &std::path::Path,
beacon: &mut Beacon,
idle: &mut IdleTracker,
guard: &ResourceGuard,
) {
let check_result = guard.check_blocking();
let (entropy, safety_decision) = match check_result {
Ok(synapse) => {
let raw = synapse.raw_entropy();
let policy = EscalationPolicy::default();
let decision = policy.decide(raw, 0, false);
(raw, decision)
}
Err(e) => {
eprintln!(
"ixd: resource check error: {:?} — proceeding with elevated caution",
e
);
(ENTROPY_CRITICAL, SafetyDecision::Escalate {
entropy: ENTROPY_CRITICAL,
reason: llmosafe::llmosafe_integration::EscalationReason::ResourcePressure,
cooldown_ms: ENTROPY_CRITICAL as u32,
})
}
};
match &safety_decision {
SafetyDecision::Halt(err, cooldown) => {
eprintln!(
"ixd: critical safety decision (Halt: {:?}) — pausing operations",
err
);
beacon.status = "safety halt".to_string();
let _ = beacon.write_to(ix_dir);
std::thread::sleep(Duration::from_millis(*cooldown as u64));
return;
}
SafetyDecision::Exit(err) => {
eprintln!("ixd: SAFETY EXIT (unrecoverable: {:?}) — terminating", err);
beacon.status = "safety exit".to_string();
let _ = beacon.write_to(ix_dir);
std::process::exit(1);
}
SafetyDecision::Escalate {
entropy: esc_entropy,
reason,
cooldown_ms,
} => {
eprintln!(
"ixd: safety escalation (entropy: {}, reason: {:?}) — throttling",
esc_entropy, reason
);
beacon.status = format!("escalated (entropy: {})", esc_entropy);
let _ = beacon.write_to(ix_dir);
std::thread::sleep(Duration::from_millis(*cooldown_ms as u64));
return;
}
SafetyDecision::Warn(reason) => {
if safety_decision.severity() >= 2 {
eprintln!(
"ixd: safety warning (severity {}): {}",
safety_decision.severity(),
reason
);
beacon.status = format!("warned: {}", reason);
let _ = beacon.write_to(ix_dir);
std::thread::sleep(Duration::from_millis(WARN_COOLDOWN_MS));
}
}
SafetyDecision::Proceed => {}
}
println!(
"ixd: {} files changed, updating index... (Entropy: {}, Decision: {:?})",
changed_files.len(),
entropy,
safety_decision
);
beacon.status = format!("indexing (entropy: {})", entropy);
beacon.last_event_at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let _ = beacon.write_to(ix_dir);
idle.record_change();
if entropy > ENTROPY_SAFE_THRESHOLD {
eprintln!("ixd: entropy too high ({}), deferring update", entropy);
beacon.status = format!("deferred (entropy: {})", entropy);
let _ = beacon.write_to(ix_dir);
return;
}
if let Err(e) = builder.update(changed_files) {
eprintln!("ixd: update failed: {} — retrying on next change", e);
}
beacon.status = "idle".to_string();
let _ = beacon.write_to(ix_dir);
}
static SHUTDOWN: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
extern "C" fn handle_signal(_: libc::c_int) {
SHUTDOWN.store(true, Ordering::SeqCst);
}