#[cfg(all(feature = "comms", any(unix, windows)))]
use anyhow::Context;
use anyhow::Result;
pub(crate) fn cmd_statusline(root: Option<&std::path::Path>) -> Result<()> {
if let Some(root) = root {
println!("{}", render_repo_statusline(root));
return Ok(());
}
#[cfg(all(feature = "comms", any(unix, windows)))]
{
use basemind::comms::client::CommsClient;
use basemind::comms::ids::AgentId;
use basemind::comms::singleton;
let line = (|| -> Option<String> {
let paths = singleton::resolve_paths().ok()?;
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.ok()?;
runtime.block_on(async move {
let agent = AgentId::parse("basemind-statusline").ok()?;
let mut client = CommsClient::connect(&paths, agent, None, None).await.ok()?;
let hot = client.accessed_paths().await.ok()?;
Some(format_statusline(&hot))
})
})();
if let Some(line) = line {
println!("{line}");
}
}
Ok(())
}
const BRAND: &str = "\x1b[38;2;249;115;22m";
const CYAN: &str = "\x1b[38;5;51m";
const MAGENTA: &str = "\x1b[38;5;201m";
const LABEL: &str = "\x1b[38;5;255m";
const SEP: &str = "\x1b[38;5;240m";
const BOLD: &str = "\x1b[1m";
const RESET: &str = "\x1b[0m";
const BRAND_GLYPH: &str = "◆";
fn brand_mark() -> String {
format!("{BRAND}{BRAND_GLYPH}{RESET} {BOLD}{BRAND}basemind{RESET}")
}
fn render_repo_statusline(root: &std::path::Path) -> String {
use basemind::store::{read_status_sidecar, workspace_cache_dir};
let basemind_dir = workspace_cache_dir(root);
let Some(status) = read_status_sidecar(&basemind_dir) else {
return format!(
"{} {SEP}│{RESET} {LABEL}no index — run:{RESET} {BOLD}{CYAN}basemind scan{RESET}",
brand_mark()
);
};
let age = format_scan_age(status.scanned_unix);
let (calls, saved) = telemetry_today(&basemind_dir);
let mut out = format!(
"{} {BOLD}{CYAN}{}{RESET} {LABEL}files{RESET} {SEP}·{RESET} {BOLD}{CYAN}{age}{RESET}",
brand_mark(),
fmt_count(status.file_count as u64),
);
out.push_str(&format!(
" {SEP}│{RESET} {BOLD}{MAGENTA}{}{RESET} {LABEL}calls{RESET} {SEP}·{RESET} {BOLD}{MAGENTA}{}{RESET} {LABEL}saved{RESET}",
fmt_count(calls),
fmt_count(saved),
));
out
}
fn format_scan_age(scanned_unix: i64) -> String {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let delta = now - scanned_unix;
if scanned_unix <= 0 || delta < 0 {
return "never".to_string();
}
if delta < 60 {
format!("{delta}s ago")
} else if delta < 3_600 {
format!("{}m ago", delta / 60)
} else if delta < 86_400 {
format!("{}h ago", delta / 3_600)
} else {
format!("{}d ago", delta / 86_400)
}
}
#[derive(serde::Deserialize)]
struct StatuslineTelemetryRow {
ts_micros: i64,
#[serde(default)]
est_tokens_saved: u64,
}
fn telemetry_today(basemind_dir: &std::path::Path) -> (u64, u64) {
use std::io::{BufRead, BufReader};
const TAIL_ROWS: usize = 2_000;
const DAY_MICROS: i64 = 24 * 3_600 * 1_000_000;
let now_micros = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| i64::try_from(d.as_micros()).unwrap_or(i64::MAX))
.unwrap_or(0);
let cutoff = now_micros.saturating_sub(DAY_MICROS);
let Ok(file) = std::fs::File::open(basemind_dir.join("telemetry.jsonl")) else {
return (0, 0);
};
let mut tail: std::collections::VecDeque<StatuslineTelemetryRow> =
std::collections::VecDeque::with_capacity(TAIL_ROWS);
for line in BufReader::new(file).lines().map_while(Result::ok) {
if line.trim().is_empty() {
continue;
}
if let Ok(row) = serde_json::from_str::<StatuslineTelemetryRow>(&line) {
if tail.len() == TAIL_ROWS {
tail.pop_front();
}
tail.push_back(row);
}
}
let mut calls = 0u64;
let mut saved = 0u64;
for row in tail.iter().filter(|r| r.ts_micros >= cutoff) {
calls += 1;
saved = saved.saturating_add(row.est_tokens_saved);
}
(calls, saved)
}
fn fmt_count(n: u64) -> String {
if n < 1_000 {
format!("{n}")
} else if n < 10_000 {
format!("{}.{}k", n / 1_000, (n * 10 / 1_000) % 10)
} else if n < 1_000_000 {
format!("{}k", n / 1_000)
} else {
format!("{}M", n / 1_000_000)
}
}
#[cfg(all(feature = "comms", any(unix, windows)))]
fn format_statusline(workspaces: &[basemind::comms::workspace_pool::AccessedWorkspace]) -> String {
if workspaces.is_empty() {
return "bm: idle".to_string();
}
const MAX_NAMES: usize = 3;
let names: Vec<&str> = workspaces
.iter()
.take(MAX_NAMES)
.map(|w| w.root.file_name().and_then(|n| n.to_str()).unwrap_or("?"))
.collect();
let mut label = names.join(" · ");
if workspaces.len() > MAX_NAMES {
label.push_str(&format!(" +{}", workspaces.len() - MAX_NAMES));
}
format!("bm: {label} · {} hot", workspaces.len())
}
#[cfg(all(feature = "comms", any(unix, windows)))]
pub(crate) fn cmd_comms(action: crate::CommsLifecycleCmd, json: bool) -> Result<()> {
match action {
crate::CommsLifecycleCmd::Daemon => basemind::cli::comms_daemon::run(),
crate::CommsLifecycleCmd::Start => cmd_comms_start(),
crate::CommsLifecycleCmd::Stop { all: true } => cmd_comms_stop_all(json),
crate::CommsLifecycleCmd::Stop { all: false } => cmd_comms_lifecycle_rpc(CommsRpc::Stop, json),
crate::CommsLifecycleCmd::Status => cmd_comms_lifecycle_rpc(CommsRpc::Status, json),
crate::CommsLifecycleCmd::Doctor { probe, clear_fatal } => cmd_comms_doctor(json, probe, clear_fatal),
}
}
#[cfg(all(feature = "comms", any(unix, windows)))]
#[derive(Clone, Copy, PartialEq, Eq)]
enum CommsRpc {
Stop,
Status,
}
#[cfg(all(feature = "comms", any(unix, windows)))]
const HTTP_READY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
#[cfg(all(feature = "comms", any(unix, windows)))]
pub(crate) fn cmd_daemon(action: crate::DaemonCmd, json: bool) -> Result<()> {
match action {
crate::DaemonCmd::Ensure => cmd_daemon_ensure(json),
}
}
#[cfg(all(feature = "comms", any(unix, windows)))]
fn cmd_daemon_ensure(json: bool) -> Result<()> {
use basemind::comms::http_frontend;
use basemind::comms::singleton;
let paths = singleton::resolve_paths().context("resolve comms paths")?;
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.context("build tokio runtime")?;
let comms_dir = paths.comms_dir.clone();
let addr = runtime.block_on(async move {
singleton::ensure_daemon(&paths)
.await
.map_err(|e| anyhow::anyhow!("ensure comms daemon: {e}"))?;
if !http_frontend::http_enabled() {
return anyhow::Ok(None);
}
http_frontend::await_http_ready(&paths.comms_dir, HTTP_READY_TIMEOUT)
.await
.context("wait for streamable-HTTP MCP transport")
.map(Some)
})?;
let Some(addr) = addr else {
let grant = http_frontend::ALLOW_HTTP_ENV;
if json {
println!("{{\"ready\":false,\"http\":\"disabled\",\"grant\":\"{grant}\"}}");
} else {
println!(
"comms daemon is running; its streamable-HTTP MCP transport is opt-in and not \
enabled (set {grant}=1 in the daemon's environment and restart it)"
);
}
return Ok(());
};
let url = http_frontend::base_url(&addr);
let token = http_frontend::published_token(&comms_dir).unwrap_or_default();
if json {
println!("{{\"ready\":true,\"addr\":\"{addr}\",\"url\":\"{url}\",\"token\":\"{token}\"}}");
} else {
println!("{url}");
if token.is_empty() {
eprintln!(
"warning: the daemon published no bearer token, so this URL will be refused. It \
predates the token requirement — restart it to mint one."
);
} else {
eprintln!("Authorization: Bearer {token}");
}
}
Ok(())
}
#[cfg(all(feature = "comms", any(unix, windows)))]
fn cmd_comms_start() -> Result<()> {
use basemind::comms::singleton;
let paths = singleton::resolve_paths().context("resolve comms paths")?;
let socket_path = paths.socket_path.clone();
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.context("build tokio runtime")?;
runtime.block_on(async move {
singleton::ensure_daemon_explicit(&paths)
.await
.map_err(|e| anyhow::anyhow!("ensure comms daemon: {e}"))
})?;
println!("comms daemon is running ({})", socket_path.display());
Ok(())
}
#[cfg(all(feature = "comms", any(unix, windows)))]
fn cmd_comms_lifecycle_rpc(rpc: CommsRpc, json: bool) -> Result<()> {
use basemind::comms::client::CommsClient;
use basemind::comms::singleton;
let paths = singleton::resolve_paths().context("resolve comms paths")?;
let comms_dir = paths.comms_dir.clone();
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.context("build tokio runtime")?;
let verdict = runtime.block_on(async move {
let root = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
let agent = basemind::comms::identity::cli_agent_id(&root);
match rpc {
CommsRpc::Stop => {
match tokio::time::timeout(STOP_RPC_TIMEOUT, async {
CommsClient::connect(&paths, agent, None, None).await?.stop().await
})
.await
{
Ok(Ok(())) => return Ok::<StopVerdict, anyhow::Error>(StopVerdict::Accepted),
Ok(Err(error)) => {
if !is_broken_store_error(&error) {
return Err(anyhow::anyhow!("stop: {error}"));
}
tracing::warn!(%error, "comms: daemon reports a broken store; falling back to pid reclaim");
return Ok(StopVerdict::Reclaimable);
}
Err(_) => {
tracing::warn!(
timeout_secs = STOP_RPC_TIMEOUT.as_secs(),
"comms: daemon did not answer the stop RPC in time; falling back to pid reclaim"
);
return Ok(StopVerdict::Reclaimable);
}
}
}
CommsRpc::Status => {
let mut client = CommsClient::connect(&paths, agent, None, None)
.await
.map_err(|e| anyhow::anyhow!("connect to comms daemon: {e}"))?;
let status = client.status().await.map_err(|e| anyhow::anyhow!("status: {e}"))?;
if json {
println!(
"{}",
serde_json::to_string(&status).map_err(|e| anyhow::anyhow!("serialize status: {e}"))?
);
} else {
println!(
"pid={} version={} build={} proto={} uptime={}s threads={} subscribers={}",
status.pid,
status.version,
if status.build_id.is_empty() {
"unreported"
} else {
&status.build_id
},
status.proto_ver,
status.uptime_secs,
status.threads,
status.subscribers,
);
let ours = basemind::version::build_id();
if !status.build_id.is_empty() && status.build_id != ours {
println!(
" WARNING: this daemon is running a DIFFERENT build of the same version \
(daemon {} vs this binary {}).",
status.build_id, ours
);
println!(
" It will keep answering with its own code — a version check cannot see this. \
Restart it with `basemind comms stop` to pick up the current binary."
);
}
}
}
}
Ok::<StopVerdict, anyhow::Error>(StopVerdict::Accepted)
})?;
if !matches!(rpc, CommsRpc::Stop) {
return Ok(());
}
let ours: Vec<_> = basemind::daemon_lock::live_daemons_of(basemind::daemon_lock::DaemonKind::Comms)
.into_iter()
.filter(|record| record.dir == comms_dir)
.collect();
let forced = reclaim_unresponsive(&ours);
let accepted = verdict == StopVerdict::Accepted;
if json {
println!(
"{}",
serde_json::json!({ "stopped": accepted, "force_terminated": forced })
);
} else if forced > 0 {
println!("comms daemon did not answer; terminated it by pid");
} else if accepted {
println!("comms daemon stopping");
} else {
println!("no live comms daemon for {}", comms_dir.display());
}
Ok(())
}
#[cfg(all(feature = "comms", any(unix, windows)))]
fn now_unix() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
#[cfg(all(feature = "comms", any(unix, windows)))]
fn clear_recorded_failures(json: bool) -> Result<()> {
use basemind::comms::{singleton, store_health};
let paths = singleton::resolve_paths().context("resolve comms paths")?;
let cleared = store_health::clear(&paths.comms_dir);
let cleared_scans = basemind::scan_evidence::stale_inflight_scans()
.iter()
.filter(|scan| basemind::scan_evidence::clear(&scan.cache_dir))
.count();
if json {
println!(
"{}",
serde_json::json!({ "cleared_fatal": cleared, "cleared_stale_scans": cleared_scans })
);
return Ok(());
}
if cleared {
println!(
"cleared the recorded fatal store error for {}",
paths.comms_dir.display()
);
} else {
println!("no recorded fatal store error for {}", paths.comms_dir.display());
}
println!("cleared {cleared_scans} stale scan-inflight record(s)");
Ok(())
}
#[cfg(all(feature = "comms", any(unix, windows)))]
fn report_stale_scans(stale_scans: &[basemind::scan_evidence::StaleScan], now: i64) {
if stale_scans.is_empty() {
return;
}
println!(
"\n{} scan(s) left a breadcrumb behind and their process is gone:",
stale_scans.len()
);
for scan in stale_scans {
println!(
" pid={} phase={} candidates={} started {} ago build={} root={}",
scan.record.pid,
scan.record.phase,
scan.record
.candidates
.map_or_else(|| "?".to_string(), |count| count.to_string()),
humanize_age(now.saturating_sub(scan.record.started_unix as i64)),
scan.record.version,
scan.record.root.display(),
);
}
println!(
" A scan that fails normally removes its own record, so these died without unwinding \
— most often an OOM kill. Acknowledge with `basemind comms doctor --clear-fatal`."
);
}
#[cfg(all(feature = "comms", any(unix, windows)))]
fn cmd_comms_doctor(json: bool, probe: bool, clear_fatal: bool) -> Result<()> {
use basemind::comms::{singleton, store_health};
use basemind::daemon_lock::{self, DaemonKind};
if clear_fatal {
return clear_recorded_failures(json);
}
let stale_scans = basemind::scan_evidence::stale_inflight_scans();
let daemons = daemon_lock::live_daemons();
let ceiling = daemon_lock::max_live_daemons();
let now = now_unix();
let verdicts: Vec<Option<singleton::DaemonProbe>> = daemons
.iter()
.map(|record| {
(probe && record.kind == DaemonKind::Comms)
.then(|| singleton::probe_serving(&singleton::comms_socket_path(&record.dir)))
})
.collect();
let fatal = singleton::resolve_paths()
.ok()
.and_then(|paths| store_health::read(&paths.comms_dir).map(|record| (paths.comms_dir, record)));
if json {
let items: Vec<serde_json::Value> = daemons
.iter()
.zip(&verdicts)
.map(|(record, verdict)| {
let mut row = serde_json::json!({
"pid": record.pid,
"kind": record.kind,
"dir": record.dir,
"version": record.version,
"uptime_secs": (now - record.started_unix).max(0),
"log": basemind::comms::singleton::daemon_log_path(&record.dir),
});
if let Some(verdict) = verdict {
row["reachable"] = probe_json(verdict);
}
row
})
.collect();
let report = serde_json::json!({
"count": daemons.len(),
"ceiling": ceiling,
"over_ceiling": daemons.len() > ceiling,
"checked": if probe { "registry+probe" } else { "registry" },
"daemons": items,
"last_fatal_store_error": fatal.as_ref().map(|(dir, record)| serde_json::json!({
"comms_dir": dir,
"record": record,
})),
"stale_scans": stale_scans.iter().map(|scan| serde_json::json!({
"cache_dir": scan.cache_dir,
"record": scan.record,
})).collect::<Vec<_>>(),
});
println!("{report}");
return Ok(());
}
if daemons.is_empty() {
println!("no live basemind daemons");
} else {
println!("{} live daemon(s) (ceiling {ceiling}):", daemons.len());
for (record, verdict) in daemons.iter().zip(&verdicts) {
println!(
" pid={} kind={} version={} uptime={}s{} dir={}",
record.pid,
record.kind,
record.version,
(now - record.started_unix).max(0),
verdict.as_ref().map(probe_label).unwrap_or_default(),
record.dir.display(),
);
if record.kind == DaemonKind::Comms {
println!(
" log: {}",
basemind::comms::singleton::daemon_log_path(&record.dir).display()
);
}
}
if daemons.len() > ceiling {
println!(
"WARNING: {} daemons exceed the ceiling of {ceiling}; run `basemind comms stop --all` to reclaim",
daemons.len(),
);
}
}
if let Some((dir, record)) = &fatal {
println!(
"\nlast fatal store error in {} (pid {}, {} ago, on `{}`, build {}):\n {}",
dir.display(),
record.pid,
humanize_age(now.saturating_sub(record.epoch_secs as i64)),
record.request,
record.version,
record.error,
);
println!(" That daemon released its lock and exited. Acknowledge with `basemind comms doctor --clear-fatal`.");
}
report_stale_scans(&stale_scans, now);
if probe {
println!("\nchecked: the pidfile registry, plus a Status RPC per comms daemon (`serving` above).");
} else {
println!("\nchecked: the pidfile registry only — whether a process with that pid exists.");
println!(
" This does NOT mean a daemon is serving. Use `basemind comms doctor --probe`, or `basemind comms status`, to test that."
);
}
Ok(())
}
#[cfg(all(feature = "comms", any(unix, windows)))]
fn probe_label(verdict: &basemind::comms::singleton::DaemonProbe) -> String {
use basemind::comms::singleton::DaemonProbe;
match verdict {
DaemonProbe::Serving { threads } => format!(" serving({threads} threads)"),
DaemonProbe::Failing { code, message } => format!(" LIVE BUT FAILING [{code}: {message}]"),
DaemonProbe::Unreachable => " LIVE BUT NOT RESPONDING".to_string(),
}
}
#[cfg(all(feature = "comms", any(unix, windows)))]
fn probe_json(verdict: &basemind::comms::singleton::DaemonProbe) -> serde_json::Value {
use basemind::comms::singleton::DaemonProbe;
match verdict {
DaemonProbe::Serving { threads } => serde_json::json!({ "state": "serving", "threads": threads }),
DaemonProbe::Failing { code, message } => {
serde_json::json!({ "state": "failing", "code": code, "message": message })
}
DaemonProbe::Unreachable => serde_json::json!({ "state": "not_responding" }),
}
}
#[cfg(all(feature = "comms", any(unix, windows)))]
fn humanize_age(secs: i64) -> String {
match secs {
s if s < 0 => "an unknown time".to_string(),
s if s < 60 => format!("{s}s"),
s if s < 3600 => format!("{}m", s / 60),
s if s < 86400 => format!("{}h", s / 3600),
s => format!("{}d", s / 86400),
}
}
#[cfg(all(feature = "comms", any(unix, windows)))]
fn cmd_comms_stop_all(json: bool) -> Result<()> {
use basemind::comms::singleton;
use basemind::daemon_lock::{self, DaemonKind};
let daemons = daemon_lock::live_daemons_of(DaemonKind::Comms);
let mut reclaimable = Vec::new();
let mut refused = Vec::new();
for record in &daemons {
let outcome = singleton::request_stop_classified(&singleton::comms_socket_path(&record.dir));
if outcome.permits_reclaim() {
reclaimable.push(record.clone());
} else if let singleton::StopOutcome::Refused { code, message } = outcome {
refused.push((record.pid, code, message));
}
}
let forced = reclaim_unresponsive(&reclaimable);
if json {
println!(
"{}",
serde_json::json!({
"stopped": reclaimable.len(),
"force_terminated": forced,
"refused": refused
.iter()
.map(|(pid, code, message)| serde_json::json!({
"pid": pid,
"code": code,
"message": message,
}))
.collect::<Vec<_>>(),
})
);
} else if daemons.is_empty() {
println!("no live basemind daemons to stop");
} else {
println!("asked {} daemon(s) to stop", daemons.len());
if forced > 0 {
println!(" force-terminated {forced} daemon(s) that did not answer the stop request");
}
for (pid, code, message) in &refused {
println!(" pid={pid} refused [{code}]: {message}");
println!(" left running — stopping it would disconnect the clients it is serving");
}
}
Ok(())
}
#[cfg(all(feature = "comms", any(unix, windows)))]
#[derive(Clone, Copy, PartialEq, Eq)]
enum StopVerdict {
Accepted,
Reclaimable,
}
#[cfg(all(feature = "comms", any(unix, windows)))]
fn is_broken_store_error(error: &basemind::comms::client::CommsClientError) -> bool {
matches!(error, basemind::comms::client::CommsClientError::Broker { code, .. } if code == "store_error")
}
#[cfg(all(feature = "comms", any(unix, windows)))]
const STOP_RPC_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
#[cfg(all(feature = "comms", any(unix, windows)))]
const STOP_GRACE: std::time::Duration = std::time::Duration::from_secs(12);
#[cfg(all(feature = "comms", any(unix, windows)))]
fn reclaim_unresponsive(daemons: &[basemind::daemon_lock::DaemonRecord]) -> usize {
use basemind::comms::singleton;
use basemind::daemon_lock::pid_is_live;
let deadline = std::time::Instant::now() + STOP_GRACE;
let mut stubborn: Vec<u32> = daemons.iter().map(|record| record.pid).collect();
while std::time::Instant::now() < deadline {
stubborn.retain(|pid| pid_is_live(*pid));
if stubborn.is_empty() {
return 0;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
stubborn
.iter()
.filter(|pid| {
tracing::warn!(pid, "comms: daemon ignored the stop request; terminating by pid");
singleton::force_terminate(**pid, std::time::Duration::from_secs(3))
})
.count()
}
#[cfg(all(test, feature = "comms", any(unix, windows)))]
mod statusline_tests {
use std::path::PathBuf;
use basemind::comms::workspace_pool::AccessedWorkspace;
fn ws(root: &str) -> AccessedWorkspace {
AccessedWorkspace {
root: PathBuf::from(root),
key: "k".to_string(),
idle_secs: 0,
}
}
#[test]
fn empty_hot_set_reads_idle() {
assert_eq!(super::format_statusline(&[]), "bm: idle");
}
#[test]
fn lists_workspace_basenames_and_the_hot_count() {
let hot = [ws("/repos/web"), ws("/repos/api")];
assert_eq!(super::format_statusline(&hot), "bm: web · api · 2 hot");
}
#[test]
fn caps_the_name_list_with_an_overflow_marker() {
let hot = [ws("/a/one"), ws("/a/two"), ws("/a/three"), ws("/a/four"), ws("/a/five")];
assert_eq!(super::format_statusline(&hot), "bm: one · two · three +2 · 5 hot");
}
}