use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use ahash::AHashMap;
use tokio::sync::Mutex;
use tokio::sync::RwLock;
use tokio::sync::mpsc;
use tokio::sync::watch;
use super::daemon_handlers::PostFields;
use super::git_history_ops::HistoryEntry;
use super::ids::{AgentId, ThreadId};
use super::protocol::{CommsOut, CommsRequest, CommsResponse};
use super::scope::ScopeChain;
use super::store::{CommsStore, CommsStoreError};
use super::workspace_pool::{self, WorkspacePool};
use crate::registry::Registry as MachineRegistry;
pub const DEFAULT_LIMIT: u32 = 100;
pub const MAX_LIMIT: u32 = 1000;
pub const IDLE_REAP_AFTER: Duration = Duration::from_secs(10 * 60);
pub const IDLE_REAP_CHECK_EVERY: Duration = Duration::from_secs(60);
pub const BOOTSTRAP_TIMEOUT: Duration = Duration::from_secs(120);
pub const BOOTSTRAP_TIMEOUT_ENV: &str = "BASEMIND_COMMS_BOOTSTRAP_SECS";
pub const IDLE_REAP_AFTER_ENV: &str = "BASEMIND_COMMS_IDLE_REAP_SECS";
pub const IDLE_REAP_CHECK_EVERY_ENV: &str = "BASEMIND_COMMS_IDLE_CHECK_SECS";
fn duration_from_env(var: &str, default: Duration) -> Duration {
match std::env::var(var) {
Ok(raw) => match raw.trim().parse::<u64>() {
Ok(secs) if secs > 0 => Duration::from_secs(secs),
_ => default,
},
Err(_) => default,
}
}
pub fn idle_reap_after() -> Duration {
duration_from_env(IDLE_REAP_AFTER_ENV, IDLE_REAP_AFTER)
}
pub fn idle_reap_check_every() -> Duration {
duration_from_env(IDLE_REAP_CHECK_EVERY_ENV, IDLE_REAP_CHECK_EVERY)
}
pub fn bootstrap_timeout() -> Duration {
duration_from_env(BOOTSTRAP_TIMEOUT_ENV, BOOTSTRAP_TIMEOUT)
}
pub const DRAIN_GRACE: Duration = Duration::from_secs(10);
const DRAIN_POLL_EVERY: Duration = Duration::from_millis(25);
const GC_LOCK_TIMEOUT: Duration = Duration::from_secs(5 * 60);
pub use super::daemon_guards::{LinkGuard, RelayGuard, WorkGuard};
pub use super::store::{THREAD_IDLE_TTL, THREAD_RETENTION_TTL};
pub const WORKSPACE_HOT_TTL: Duration = Duration::from_secs(15 * 60);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LifecycleState {
Starting,
Active,
Idle,
Draining,
Stopped,
}
pub(super) enum SubScope {
Thread(ThreadId),
Inbox {
thread: Option<ThreadId>,
},
}
pub(super) struct SubSink {
pub(super) scope: SubScope,
pub(super) agent: AgentId,
pub(super) chain: Option<ScopeChain>,
pub(super) tx: mpsc::Sender<CommsOut>,
}
pub(super) struct Registry {
pub(super) sinks: AHashMap<u64, SubSink>,
pub(super) state: LifecycleState,
}
pub struct Broker {
pub(super) store: Arc<CommsStore>,
pub(crate) heavy_admission: Arc<crate::mcp::admission::HeavyAdmission>,
pub(super) workspaces: Arc<WorkspacePool>,
pub(super) git_history: std::sync::Mutex<AHashMap<std::path::PathBuf, Arc<HistoryEntry>>>,
pub(super) git_history_open_lock: Mutex<()>,
pub(super) registry: Mutex<Registry>,
machine_registry: Arc<Mutex<MachineRegistry>>,
pub(super) store_write_gate: Arc<std::sync::Mutex<()>>,
#[cfg(test)]
pub(super) store_delay_ms: Arc<AtomicU64>,
blob_gc_lock: RwLock<()>,
pub(super) shutdown: std::sync::OnceLock<watch::Sender<bool>>,
pub(super) comms_dir: std::sync::OnceLock<std::path::PathBuf>,
pub(super) fatal_store: AtomicBool,
pub(super) scan_cancel: crate::scanner::ScanCancel,
pub(super) subscriber_count: AtomicUsize,
link_count: AtomicUsize,
pub(super) relay_count: AtomicUsize,
ever_linked: AtomicBool,
pub(super) work_inflight: AtomicUsize,
last_activity_ms: AtomicU64,
pub(super) http_inflight: AtomicUsize,
pub(super) last_http_ms: AtomicU64,
pub(super) next_sub: AtomicU64,
pub(super) started: Instant,
pub(super) version: String,
}
impl Broker {
pub fn new(store: Arc<CommsStore>) -> Self {
let registry = MachineRegistry::from_data_home().unwrap_or_else(|error| {
tracing::warn!(%error, "comms: machine registry open failed; using an empty in-memory registry");
MachineRegistry::open(
&std::env::temp_dir().join(format!("basemind-registry-fallback-{}", std::process::id())),
)
.expect("open fallback registry in temp dir")
});
Self::with_registry(store, registry)
}
pub fn with_registry(store: Arc<CommsStore>, machine_registry: MachineRegistry) -> Self {
let workspaces = Arc::new(WorkspacePool::new(workspace_pool::DEFAULT_HOT_CAP));
let scan_cancel = workspaces.scan_cancel();
Self {
store,
heavy_admission: Arc::new(crate::mcp::admission::HeavyAdmission::default()),
workspaces,
git_history: std::sync::Mutex::new(AHashMap::new()),
git_history_open_lock: Mutex::new(()),
registry: Mutex::new(Registry {
sinks: AHashMap::new(),
state: LifecycleState::Starting,
}),
machine_registry: Arc::new(Mutex::new(machine_registry)),
store_write_gate: Arc::new(std::sync::Mutex::new(())),
#[cfg(test)]
store_delay_ms: Arc::new(AtomicU64::new(0)),
blob_gc_lock: RwLock::new(()),
shutdown: std::sync::OnceLock::new(),
comms_dir: std::sync::OnceLock::new(),
fatal_store: AtomicBool::new(false),
scan_cancel,
subscriber_count: AtomicUsize::new(0),
link_count: AtomicUsize::new(0),
relay_count: AtomicUsize::new(0),
ever_linked: AtomicBool::new(false),
work_inflight: AtomicUsize::new(0),
last_activity_ms: AtomicU64::new(0),
http_inflight: AtomicUsize::new(0),
last_http_ms: AtomicU64::new(0),
next_sub: AtomicU64::new(1),
started: Instant::now(),
version: env!("CARGO_PKG_VERSION").to_string(),
}
}
pub fn install_shutdown(&self, shutdown: watch::Sender<bool>) {
let _ = self.shutdown.set(shutdown);
}
pub async fn mark_active(&self) {
let mut reg = self.registry.lock().await;
if reg.state == LifecycleState::Starting || reg.state == LifecycleState::Idle {
reg.state = LifecycleState::Active;
}
}
pub fn subscriber_count(&self) -> usize {
self.subscriber_count.load(Ordering::Relaxed)
}
pub async fn remove_link_subscriptions(&self, link_tx: &mpsc::Sender<CommsOut>) {
let removed = {
let mut registry = self.registry.lock().await;
let before = registry.sinks.len();
registry.sinks.retain(|_, sink| !sink.tx.same_channel(link_tx));
before.saturating_sub(registry.sinks.len())
};
if removed > 0 {
self.subscriber_count.fetch_sub(removed, Ordering::Relaxed);
self.maybe_idle().await;
}
}
pub fn link_connected(&self) {
self.link_count.fetch_add(1, Ordering::Relaxed);
self.ever_linked.store(true, Ordering::Relaxed);
self.touch();
}
pub fn ever_served(&self) -> bool {
self.ever_linked.load(Ordering::Relaxed) || self.last_http_ms.load(Ordering::Relaxed) != 0
}
pub fn link_disconnected(&self) {
self.link_count.fetch_sub(1, Ordering::Relaxed);
self.touch();
}
pub fn touch(&self) {
self.last_activity_ms
.store(self.started.elapsed().as_millis() as u64, Ordering::Relaxed);
}
pub fn register_link(self: &Arc<Self>) -> LinkGuard {
self.link_connected();
LinkGuard { broker: self.clone() }
}
pub async fn serve_relay_connection<S>(self: Arc<Self>, mut stream: S, link: LinkGuard)
where
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
{
use super::relay;
use tokio::io::AsyncReadExt as _;
self.mark_active().await;
let mut magic = [0u8; relay::RELAY_MAGIC.len()];
if let Err(error) = stream.read_exact(&mut magic).await {
tracing::warn!(%error, "relay: reading preamble failed");
return;
}
if magic != relay::RELAY_MAGIC {
tracing::warn!("relay: preamble mismatch after peek; dropping");
return;
}
let hello = match relay::read_hello(&mut stream).await {
Ok(hello) => hello,
Err(error) => {
tracing::warn!(%error, "relay: reading hello failed");
return;
}
};
let daemon_version = env!("CARGO_PKG_VERSION").to_string();
let decline = |code: &str| relay::RelayWelcome {
relay_proto_ver: relay::RELAY_PROTO_VER,
daemon_version: daemon_version.clone(),
accepted: false,
code: Some(code.to_string()),
};
if hello.relay_proto_ver != relay::RELAY_PROTO_VER {
let _ = relay::write_welcome(&mut stream, &decline("relay_proto_skew")).await;
return;
}
if hello.view != crate::store::VIEW_WORKING {
let _ = relay::write_welcome(&mut stream, &decline("view_unsupported")).await;
return;
}
let host = Arc::clone(&self.workspaces) as Arc<dyn crate::mcp::HostBackend>;
let git_history_host = Arc::clone(&self) as Arc<dyn crate::git_history::remote::HistoryHost>;
let shared = match self
.workspaces
.get_or_build_serve_state(&hello.root, host, git_history_host)
.await
{
Ok(shared) => shared,
Err(error) => {
tracing::warn!(%error, root = %hello.root.display(), "relay: hosting read stack failed");
let _ = relay::write_welcome(&mut stream, &decline("host_build_failed")).await;
return;
}
};
let _conn = match self.workspaces.begin_conn_async(&hello.root).await {
Ok(guard) => guard,
Err(error) => {
tracing::warn!(%error, "relay: connection accounting failed");
let _ = relay::write_welcome(&mut stream, &decline("host_build_failed")).await;
return;
}
};
let Some(_relay) = self.try_register_relay().await else {
let _ = relay::write_welcome(&mut stream, &decline("daemon_draining")).await;
return;
};
let welcome = relay::RelayWelcome {
relay_proto_ver: relay::RELAY_PROTO_VER,
daemon_version,
accepted: true,
code: None,
};
if let Err(error) = relay::write_welcome(&mut stream, &welcome).await {
tracing::warn!(%error, "relay: sending welcome failed");
return;
}
let agent = hello.agent.to_string();
tracing::info!(agent = %agent, root = %hello.root.display(), "relay: hosting rmcp session");
let server = crate::mcp::BasemindServer::from_shared(shared, agent, Arc::clone(&self.heavy_admission));
let (read_half, write_half) = tokio::io::split(stream);
match rmcp::ServiceExt::serve(server, (read_half, write_half)).await {
Ok(running) => {
if let Err(error) = running.waiting().await {
tracing::info!(%error, "relay: rmcp session ended with error");
}
}
Err(error) => tracing::warn!(%error, "relay: rmcp serve failed to start"),
}
drop(_conn);
drop(link);
}
pub fn begin_work(&self) -> WorkGuard<'_> {
self.work_inflight.fetch_add(1, Ordering::SeqCst);
WorkGuard { broker: self }
}
pub(super) fn end_work(&self) {
self.work_inflight.fetch_sub(1, Ordering::SeqCst);
self.touch();
}
pub fn work_inflight(&self) -> usize {
self.work_inflight.load(Ordering::SeqCst)
}
pub async fn is_idle_for(&self, idle_for: Duration) -> bool {
if self.link_count.load(Ordering::SeqCst) != 0 {
return false;
}
if self.work_inflight.load(Ordering::SeqCst) != 0 {
return false;
}
if self.http_inflight.load(Ordering::SeqCst) != 0 {
return false;
}
if matches!(self.state().await, LifecycleState::Draining | LifecycleState::Stopped) {
return false;
}
self.elapsed_since_activity() >= idle_for.as_millis() as u64
}
fn elapsed_since_activity(&self) -> u64 {
let now_ms = self.started.elapsed().as_millis() as u64;
let last = self
.last_activity_ms
.load(Ordering::SeqCst)
.max(self.last_http_ms.load(Ordering::SeqCst));
now_ms.saturating_sub(last)
}
pub async fn try_begin_idle_drain(&self, idle_for: Duration) -> bool {
let sinks: Vec<mpsc::Sender<CommsOut>> = {
let mut reg = self.registry.lock().await;
if matches!(reg.state, LifecycleState::Draining | LifecycleState::Stopped) {
return false;
}
if self.link_count.load(Ordering::SeqCst) != 0
|| self.work_inflight.load(Ordering::SeqCst) != 0
|| self.http_inflight.load(Ordering::SeqCst) != 0
{
return false;
}
if self.elapsed_since_activity() < idle_for.as_millis() as u64 {
return false;
}
reg.state = LifecycleState::Draining;
reg.sinks.values().map(|s| s.tx.clone()).collect()
};
self.finish_drain(sinks).await;
true
}
pub async fn drain_links(&self, grace: Duration) -> usize {
let deadline = Instant::now() + grace;
loop {
let open = self.link_count.load(Ordering::SeqCst);
if open == 0 {
return 0;
}
if Instant::now() >= deadline {
tracing::warn!(
open,
"comms: links still open at the end of the drain grace; exiting anyway"
);
return open;
}
tokio::time::sleep(DRAIN_POLL_EVERY).await;
}
}
pub fn archive_idle_threads(&self, ttl: Duration) -> Result<usize, CommsStoreError> {
self.store.archive_idle(ttl)
}
pub fn purge_archived_threads(&self, ttl: Duration) -> Result<usize, CommsStoreError> {
self.store.purge_archived(ttl)
}
pub fn evict_idle_workspaces(&self, ttl: Duration) -> usize {
self.git_history
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.retain(|_, entry| entry.idle_for() < ttl);
self.workspaces.evict_idle(ttl)
}
pub fn evict_idle_read_stacks(&self, ttl: Duration) -> usize {
self.workspaces.evict_idle_read_stacks(ttl)
}
pub async fn run_blob_gc(&self) -> Result<crate::store_gc::GcReport, crate::store_gc::GcError> {
self.run_blob_gc_with_lock_timeout(GC_LOCK_TIMEOUT).await
}
pub(crate) async fn run_blob_gc_with_lock_timeout(
&self,
lock_timeout: std::time::Duration,
) -> Result<crate::store_gc::GcReport, crate::store_gc::GcError> {
let _working = self.begin_work();
let Ok(_sweep_guard) = tokio::time::timeout(lock_timeout, self.blob_gc_lock.write()).await else {
let error = crate::store_gc::GcError::Starved(lock_timeout);
crate::store_gc::persist_gc_error(&error);
return Err(error);
};
let result = match tokio::task::spawn_blocking(crate::store_gc::reap_gc_and_enforce_budget).await {
Ok(result) => result,
Err(join) => Err(crate::store_gc::GcError::Join(join.to_string())),
};
if let Err(error) = &result {
crate::store_gc::persist_gc_error(error);
}
result
}
pub fn cleanup_signal(&self) -> Arc<tokio::sync::Notify> {
self.workspaces.cleanup_signal()
}
pub async fn handle_framed(
&self,
req: CommsRequest,
session: &mut Session,
link_tx: &mpsc::Sender<CommsOut>,
) -> CommsOut {
match req {
CommsRequest::Call { id, request } => CommsOut::Reply {
id,
response: self.handle(*request, session, link_tx).await,
},
bare => CommsOut::Response(self.handle(bare, session, link_tx).await),
}
}
pub async fn handle(
&self,
req: CommsRequest,
session: &mut Session,
link_tx: &mpsc::Sender<CommsOut>,
) -> CommsResponse {
self.touch();
if let Some(agent) = session.agent.clone() {
let touched = {
let agent = agent.clone();
self.store_write(move |store| store.touch_agent_if_stale(&agent, super::store::AGENT_TOUCH_INTERVAL))
.await
};
if let Err(error) = touched {
self.note_store_error("touch_agent", &error).await;
tracing::warn!(%error, agent = %agent, "comms: refresh agent activity failed");
}
}
let method = req.method();
match self.dispatch(req, session, link_tx).await {
Ok(resp) => resp,
Err(e) => {
self.note_store_error(method, &e).await;
CommsResponse::Error {
code: "store_error".to_string(),
message: e.to_string(),
}
}
}
}
async fn dispatch(
&self,
req: CommsRequest,
session: &mut Session,
link_tx: &mpsc::Sender<CommsOut>,
) -> Result<CommsResponse, CommsStoreError> {
match req {
CommsRequest::Hello {
agent,
proto_ver,
remote,
cwd,
} => {
let resp = self.on_hello(agent, proto_ver, remote, cwd.clone(), session).await?;
if let (CommsResponse::Welcome { .. }, Some(root)) = (&resp, cwd) {
let target = root.clone();
if let Err(error) = self.registry_blocking(move |reg| reg.register_workspace(&target)).await {
tracing::warn!(%error, root = %root.display(), "comms: registry auto-register failed");
}
}
Ok(resp)
}
CommsRequest::Register { card } => self.on_register(session, card).await,
CommsRequest::ListAgents { thread } => self.on_list_agents(thread).await,
request @ CommsRequest::Cleanup { .. } => Ok(self.on_agents_cleanup_request(request).await),
CommsRequest::AgentsStatus { agent_ttl_secs } => Ok(self.on_agents_status(agent_ttl_secs).await),
CommsRequest::ThreadStart { subject, path, members } => {
self.on_thread_start(session, subject, path, members).await
}
CommsRequest::ThreadJoin { thread } => self.on_thread_join(session, thread).await,
CommsRequest::ThreadLeave { thread } => self.on_thread_leave(session, thread).await,
CommsRequest::ThreadList {
remote,
cwd,
subject_contains,
include_archived,
} => {
self.on_thread_list(session, remote, cwd, subject_contains, include_archived)
.await
}
CommsRequest::ThreadPost {
thread,
subject,
tags,
reply_to,
body,
idempotency_key,
} => {
let fields = PostFields {
subject,
tags,
reply_to,
body,
idempotency_key,
};
self.on_post(session, thread, fields).await
}
CommsRequest::ThreadHistory {
thread,
cursor,
limit,
since_micros,
} => self.on_history(thread, cursor, limit, since_micros).await,
CommsRequest::ThreadMembers { thread } => self.on_thread_members(thread).await,
CommsRequest::ThreadAddMember { thread, member } => {
self.on_thread_add_member(session, thread, member).await
}
CommsRequest::ThreadRemoveMember { thread, member } => {
self.on_thread_remove_member(session, thread, member).await
}
CommsRequest::ThreadArchive { thread } => self.on_thread_archive(session, thread).await,
CommsRequest::GetBody { message_id } => self.on_get_body(session, message_id).await,
CommsRequest::Inbox {
cursor,
limit,
mark_read,
since_micros,
..
} => self.on_inbox(session, cursor, limit, mark_read, since_micros).await,
CommsRequest::AckInbox {
message_ids,
thread,
to_seq,
} => self.on_ack(session, message_ids, thread, to_seq).await,
CommsRequest::Subscribe { thread } => self.on_subscribe(session, thread, link_tx).await,
CommsRequest::SubscribeInbox { thread } => self.on_subscribe_inbox(session, thread, link_tx).await,
CommsRequest::Unsubscribe { sub } => self.on_unsubscribe(sub).await,
CommsRequest::Rescan {
root,
paths,
full,
embed,
} => Ok(self.on_rescan(root, paths, full, embed).await),
CommsRequest::ResolvedRefs { root, query } => Ok(self.on_resolved_refs(root, query).await),
CommsRequest::IndexRead { root, query } => Ok(self.on_index_read(root, query).await),
CommsRequest::CodeSearchLanes { root, query } => Ok(self.on_code_search_lanes(root, query).await),
CommsRequest::GitHistory { root, op } => Ok(self.on_git_history(root, op).await),
#[cfg(feature = "memory")]
CommsRequest::Memory { root, scope, op } => Ok(self.on_memory(root, scope, op).await),
#[cfg(feature = "memory")]
CommsRequest::Governance { root, scope, op } => Ok(self.on_governance(root, scope, op).await),
CommsRequest::AccessedPaths => Ok(self.on_accessed_paths()),
CommsRequest::WorkspacesList => Ok(self.on_workspaces_list().await),
CommsRequest::WorktreesList { repo_id } => Ok(self.on_worktrees_list(repo_id).await),
CommsRequest::BranchesList { repo_id } => Ok(self.on_branches_list(repo_id).await),
CommsRequest::WorktreeClaim {
repo_id,
name,
claimant,
} => Ok(self.on_worktree_claim(repo_id, name, claimant).await),
CommsRequest::WorktreeRelease {
repo_id,
name,
claimant,
} => Ok(self.on_worktree_release(repo_id, name, claimant).await),
CommsRequest::Ping => Ok(CommsResponse::Pong),
CommsRequest::Status => self.on_status().await,
CommsRequest::Stop => Ok(self.on_stop().await),
CommsRequest::Call { .. } => Ok(CommsResponse::Error {
code: "nested_call".to_string(),
message: "a correlated call cannot wrap another call".to_string(),
}),
}
}
}
#[derive(Default)]
pub struct Session {
pub agent: Option<AgentId>,
pub chain: Option<ScopeChain>,
}
#[path = "daemon_workspace_handlers.rs"]
mod workspace_handlers;
#[path = "daemon_store_blocking.rs"]
mod store_blocking;
#[path = "daemon_response.rs"]
mod response;
use response::registry_error;
#[path = "daemon_threads.rs"]
pub(super) mod threads;
#[cfg(test)]
use super::model::now_micros;
#[cfg(test)]
use super::protocol::{CommsNotification, PROTO_VER, SeqMeta};
#[cfg(test)]
use threads::{sanitize_id, validate_dimensions};
#[cfg(test)]
#[path = "daemon_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "daemon_forward_tests.rs"]
mod forward_tests;