use std::sync::Arc;
use std::sync::atomic::{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::git_history_ops::HistoryEntry;
use super::ids::{AgentId, ThreadId};
use super::protocol::{CommsNotification, 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(30 * 60);
pub const IDLE_REAP_CHECK_EVERY: Duration = Duration::from_secs(60);
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 const DRAIN_GRACE: Duration = Duration::from_secs(10);
const DRAIN_POLL_EVERY: Duration = Duration::from_millis(25);
pub struct LinkGuard {
broker: Arc<Broker>,
}
impl Drop for LinkGuard {
fn drop(&mut self) {
self.broker.link_disconnected();
}
}
pub struct WorkGuard<'a> {
broker: &'a Broker,
}
impl Drop for WorkGuard<'_> {
fn drop(&mut self) {
self.broker.work_inflight.fetch_sub(1, Ordering::SeqCst);
self.broker.touch();
}
}
pub const THREAD_IDLE_TTL: Duration = Duration::from_secs(14 * 24 * 60 * 60);
pub const THREAD_RETENTION_TTL: Duration = Duration::from_secs(30 * 24 * 60 * 60);
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) 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>,
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: Mutex<MachineRegistry>,
blob_gc_lock: RwLock<()>,
shutdown: std::sync::OnceLock<watch::Sender<bool>>,
pub(super) subscriber_count: AtomicUsize,
link_count: AtomicUsize,
work_inflight: AtomicUsize,
last_activity_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 {
Self {
store,
workspaces: Arc::new(WorkspacePool::new(workspace_pool::DEFAULT_HOT_CAP)),
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: Mutex::new(machine_registry),
blob_gc_lock: RwLock::new(()),
shutdown: std::sync::OnceLock::new(),
subscriber_count: AtomicUsize::new(0),
link_count: AtomicUsize::new(0),
work_inflight: AtomicUsize::new(0),
last_activity_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 fn link_connected(&self) {
self.link_count.fetch_add(1, Ordering::Relaxed);
self.touch();
}
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 fn begin_work(&self) -> WorkGuard<'_> {
self.work_inflight.fetch_add(1, Ordering::SeqCst);
WorkGuard { broker: self }
}
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 matches!(self.state().await, LifecycleState::Draining | LifecycleState::Stopped) {
return false;
}
let now_ms = self.started.elapsed().as_millis() as u64;
let last = self.last_activity_ms.load(Ordering::SeqCst);
now_ms.saturating_sub(last) >= idle_for.as_millis() as u64
}
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 {
return false;
}
let now_ms = self.started.elapsed().as_millis() as u64;
let last = self.last_activity_ms.load(Ordering::SeqCst);
if now_ms.saturating_sub(last) < 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 async fn run_blob_gc(&self) -> Result<crate::store_gc::GcReport, crate::store_gc::GcError> {
let _working = self.begin_work();
let _sweep_guard = self.blob_gc_lock.write().await;
tokio::task::spawn_blocking(crate::store_gc::reap_and_gc_global)
.await
.map_err(|join| crate::store_gc::GcError::Join(join.to_string()))?
}
pub async fn handle(
&self,
req: CommsRequest,
session: &mut Session,
link_tx: &mpsc::Sender<CommsOut>,
) -> CommsResponse {
self.touch();
match self.dispatch(req, session, link_tx).await {
Ok(resp) => resp,
Err(e) => 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)?;
if let (CommsResponse::Welcome { .. }, Some(root)) = (&resp, cwd) {
let mut registry = self.machine_registry.lock().await;
if let Err(error) = registry.register_workspace(&root) {
tracing::warn!(%error, root = %root.display(), "comms: registry auto-register failed");
}
}
Ok(resp)
}
CommsRequest::Register { card } => self.on_register(session, card),
CommsRequest::ListAgents { thread } => self.on_list_agents(thread),
CommsRequest::ThreadStart { subject, path, members } => {
self.on_thread_start(session, subject, path, members)
}
CommsRequest::ThreadJoin { thread } => self.on_thread_join(session, thread),
CommsRequest::ThreadLeave { thread } => self.on_thread_leave(session, thread),
CommsRequest::ThreadList {
remote,
cwd,
subject_contains,
include_archived,
} => self.on_thread_list(session, remote, cwd, subject_contains, include_archived),
CommsRequest::ThreadPost {
thread,
subject,
tags,
reply_to,
body,
} => self.on_post(session, thread, subject, tags, reply_to, body).await,
CommsRequest::ThreadHistory {
thread,
cursor,
limit,
since_micros,
} => self.on_history(thread, cursor, limit, since_micros),
CommsRequest::ThreadMembers { thread } => self.on_thread_members(thread),
CommsRequest::ThreadAddMember { thread, member } => self.on_thread_add_member(session, thread, member),
CommsRequest::ThreadRemoveMember { thread, member } => {
self.on_thread_remove_member(session, thread, member)
}
CommsRequest::ThreadArchive { thread } => self.on_thread_archive(session, thread),
CommsRequest::GetBody { message_id } => self.on_get_body(message_id),
CommsRequest::Inbox {
cursor,
limit,
mark_read,
since_micros,
..
} => self.on_inbox(session, cursor, limit, mark_read, since_micros),
CommsRequest::AckInbox {
message_ids,
thread,
to_seq,
} => self.on_ack(session, message_ids, thread, to_seq),
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::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 => Ok(self.on_status().await),
CommsRequest::Stop => {
self.begin_drain().await;
Ok(CommsResponse::Ok)
}
}
}
async fn on_rescan(
&self,
root: std::path::PathBuf,
paths: Option<Vec<std::path::PathBuf>>,
full: bool,
embed: bool,
) -> CommsResponse {
self.mark_active().await;
let _rescan_guard = self.blob_gc_lock.read().await;
let pool = Arc::clone(&self.workspaces);
let started = Instant::now();
match tokio::task::spawn_blocking(move || pool.rescan(&root, paths, full, embed)).await {
Ok(Ok(stats)) => CommsResponse::Rescanned {
scanned: stats.scanned,
updated: stats.updated,
removed: stats.removed,
elapsed_ms: started.elapsed().as_millis() as u64,
},
Ok(Err(error)) => CommsResponse::Error {
code: "rescan_failed".to_string(),
message: error.to_string(),
},
Err(join) => CommsResponse::Error {
code: "rescan_panicked".to_string(),
message: join.to_string(),
},
}
}
async fn on_resolved_refs(
&self,
root: std::path::PathBuf,
query: crate::comms::resolved_proto::ResolvedRefQuery,
) -> CommsResponse {
self.mark_active().await;
let pool = Arc::clone(&self.workspaces);
match tokio::task::spawn_blocking(move || {
pool.with_workspace(&root, |store| resolve_refs_against(store, &query))
})
.await
{
Ok(Ok(result)) => CommsResponse::ResolvedRefs(result),
Ok(Err(error)) => CommsResponse::Error {
code: "resolved_refs_failed".to_string(),
message: error.to_string(),
},
Err(join) => CommsResponse::Error {
code: "resolved_refs_panicked".to_string(),
message: join.to_string(),
},
}
}
#[cfg(feature = "memory")]
async fn on_memory(
&self,
root: std::path::PathBuf,
scope: String,
op: super::memory_proto::MemoryOp,
) -> CommsResponse {
self.mark_active().await;
let pool = Arc::clone(&self.workspaces);
let outcome = tokio::task::spawn_blocking(move || {
pool.with_workspace_mut(&root, |store| {
let idx = store
.index_db
.as_ref()
.ok_or(crate::mcp::memory_ops::MemoryOpError::IndexUnavailable)?;
crate::mcp::memory_ops::run_memory_op(idx, &scope, &op)
})
})
.await;
match outcome {
Ok(Ok(Ok(outcome))) => CommsResponse::Memory(outcome),
Ok(Ok(Err(error))) => CommsResponse::Error {
code: "memory_op_failed".to_string(),
message: error.to_string(),
},
Ok(Err(error)) => CommsResponse::Error {
code: "memory_workspace_failed".to_string(),
message: error.to_string(),
},
Err(join) => CommsResponse::Error {
code: "memory_panicked".to_string(),
message: join.to_string(),
},
}
}
#[cfg(feature = "memory")]
async fn on_governance(
&self,
root: std::path::PathBuf,
scope: String,
op: super::proposals_proto::GovernanceOp,
) -> CommsResponse {
self.mark_active().await;
let pool = Arc::clone(&self.workspaces);
let outcome = tokio::task::spawn_blocking(move || {
pool.with_workspace_mut(&root, |store| {
let idx = store
.index_db
.as_ref()
.ok_or(crate::mcp::memory_ops::MemoryOpError::IndexUnavailable)?;
crate::mcp::proposals_ops::run_governance_op(idx, &scope, &op)
})
})
.await;
match outcome {
Ok(Ok(Ok(outcome))) => CommsResponse::Governance(outcome),
Ok(Ok(Err(error))) => CommsResponse::Error {
code: "governance_op_failed".to_string(),
message: error.to_string(),
},
Ok(Err(error)) => CommsResponse::Error {
code: "governance_workspace_failed".to_string(),
message: error.to_string(),
},
Err(join) => CommsResponse::Error {
code: "governance_panicked".to_string(),
message: join.to_string(),
},
}
}
fn on_accessed_paths(&self) -> CommsResponse {
CommsResponse::Accessed {
workspaces: self.workspaces.accessed(),
}
}
async fn on_workspaces_list(&self) -> CommsResponse {
let registry = self.machine_registry.lock().await;
CommsResponse::Workspaces {
workspaces: registry.workspaces(),
}
}
async fn on_worktrees_list(&self, repo_id: String) -> CommsResponse {
let registry = self.machine_registry.lock().await;
CommsResponse::Worktrees {
worktrees: registry.worktrees(&repo_id),
}
}
async fn on_branches_list(&self, repo_id: String) -> CommsResponse {
let registry = self.machine_registry.lock().await;
CommsResponse::Branches {
branches: registry.branches(&repo_id),
}
}
async fn on_worktree_claim(&self, repo_id: String, name: String, claimant: String) -> CommsResponse {
let mut registry = self.machine_registry.lock().await;
match registry.claim_worktree(&repo_id, &name, &claimant) {
Ok(held) => CommsResponse::ClaimOutcome { held },
Err(error) => registry_error(error),
}
}
async fn on_worktree_release(&self, repo_id: String, name: String, claimant: String) -> CommsResponse {
let mut registry = self.machine_registry.lock().await;
match registry.release_worktree(&repo_id, &name, &claimant) {
Ok(held) => CommsResponse::ClaimOutcome { held },
Err(error) => registry_error(error),
}
}
pub async fn begin_drain(&self) {
let sinks: Vec<mpsc::Sender<CommsOut>> = {
let mut reg = self.registry.lock().await;
reg.state = LifecycleState::Draining;
reg.sinks.values().map(|s| s.tx.clone()).collect()
};
self.finish_drain(sinks).await;
}
async fn finish_drain(&self, sinks: Vec<mpsc::Sender<CommsOut>>) {
for tx in sinks {
let _ = tx.send(CommsOut::Notification(CommsNotification::Shutdown)).await;
}
if let Some(shutdown) = self.shutdown.get() {
let _ = shutdown.send(true);
}
}
pub async fn state(&self) -> LifecycleState {
self.registry.lock().await.state
}
}
#[derive(Default)]
pub struct Session {
pub agent: Option<AgentId>,
pub chain: Option<ScopeChain>,
}
fn resolve_refs_against(
store: &crate::store::Store,
query: &crate::comms::resolved_proto::ResolvedRefQuery,
) -> crate::comms::resolved_proto::ResolvedRefResult {
use crate::comms::resolved_proto::{ResolvedRefQuery, ResolvedRefResult};
match query {
ResolvedRefQuery::ReferencesTo { def_path, def_start } => {
ResolvedRefResult::References(crate::query::resolved_references(store, def_path, *def_start))
}
ResolvedRefQuery::DefinitionOf { use_path, use_start } => {
ResolvedRefResult::Definition(crate::query::definition_of(store, use_path, *use_start))
}
}
}
fn registry_error(error: crate::registry::RegistryError) -> CommsResponse {
CommsResponse::Error {
code: "registry_error".to_string(),
message: error.to_string(),
}
}
#[path = "daemon_threads.rs"]
pub(super) mod threads;
#[cfg(test)]
use super::model::now_micros;
#[cfg(test)]
use super::protocol::{PROTO_VER, SeqMeta};
#[cfg(test)]
use threads::{sanitize_id, validate_dimensions};
#[cfg(test)]
#[path = "daemon_tests.rs"]
mod tests;