2026-04-25 15:04:37 +00:00
|
|
|
//! Shared services bundle — common state threaded through HTTP handlers and chat transports.
|
|
|
|
|
//!
|
|
|
|
|
//! `Services` bundles the fields that every transport (Matrix, Slack, Discord,
|
|
|
|
|
//! WhatsApp) and the HTTP/MCP layer need. A single `Arc<Services>` is
|
|
|
|
|
//! constructed once in `main.rs` and cloned into `AppContext` and each
|
|
|
|
|
//! transport's context struct.
|
|
|
|
|
|
|
|
|
|
use crate::agents::AgentPool;
|
2026-05-15 11:57:00 +00:00
|
|
|
use crate::chat::dispatcher::ChatDispatcher;
|
2026-07-18 13:51:22 +00:00
|
|
|
use crate::service::permission_router::{
|
|
|
|
|
PendingPermReplies, RememberedPermissions, ResponderRegistry,
|
|
|
|
|
};
|
2026-07-19 19:17:34 +00:00
|
|
|
use crate::service::question_router::{PendingQuestionReplies, QuestionResponderRegistry};
|
2026-04-26 02:23:23 +00:00
|
|
|
use crate::service::status::StatusBroadcaster;
|
2026-07-16 13:13:41 +00:00
|
|
|
use std::collections::HashSet;
|
2026-04-25 15:04:37 +00:00
|
|
|
use std::path::PathBuf;
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
|
|
|
|
/// Shared state bundle constructed once at startup and cloned (via `Arc`) into
|
|
|
|
|
/// every context that needs access to the project root, agent pool, bot
|
|
|
|
|
/// identity, ambient-room set, or permission plumbing.
|
|
|
|
|
pub struct Services {
|
|
|
|
|
/// Absolute path to the project root directory.
|
|
|
|
|
pub project_root: PathBuf,
|
|
|
|
|
/// Agent pool for starting, stopping, and querying coding agents.
|
|
|
|
|
pub agents: Arc<AgentPool>,
|
|
|
|
|
/// Display name the bot uses to identify itself (from `bot.toml`).
|
|
|
|
|
pub bot_name: String,
|
|
|
|
|
/// String representation of the bot's user ID (e.g. `"@timmy:hs.local"`
|
|
|
|
|
/// for Matrix, `"slack-bot"` for Slack).
|
|
|
|
|
pub bot_user_id: String,
|
|
|
|
|
/// Set of room/channel IDs where ambient mode is active.
|
|
|
|
|
pub ambient_rooms: Arc<std::sync::Mutex<HashSet<String>>>,
|
2026-07-16 13:13:41 +00:00
|
|
|
/// Registry of tasks currently registered to receive forwarded MCP
|
|
|
|
|
/// `prompt_permission` requests (Matrix bot, sled uplink, WS chat
|
|
|
|
|
/// sessions, per-message chat transports). Replaces holding `perm_rx`'s
|
|
|
|
|
/// mutex as a presence signal.
|
|
|
|
|
pub permission_registry: Arc<ResponderRegistry>,
|
|
|
|
|
/// Pending permission replies awaiting a plain-language (yes/no) chat
|
|
|
|
|
/// reply, keyed by `request_id` with a per-location FIFO index so two
|
|
|
|
|
/// concurrent requests for the same room/sender/channel don't drop each
|
|
|
|
|
/// other's oneshot sender.
|
|
|
|
|
pub pending_perm_replies: Arc<PendingPermReplies>,
|
2026-04-25 15:04:37 +00:00
|
|
|
/// Seconds to wait for a user to respond to a permission prompt before
|
|
|
|
|
/// auto-denying (fail-closed).
|
|
|
|
|
pub permission_timeout_secs: u64,
|
2026-07-18 13:51:22 +00:00
|
|
|
/// In-memory, per-session "don't ask again" permission rules (story
|
|
|
|
|
/// 1218). Checked by `tool_prompt_permission` before forwarding a
|
|
|
|
|
/// request to chat; never persisted to disk and never affects a
|
|
|
|
|
/// different session's agent.
|
|
|
|
|
pub remembered_permissions: Arc<RememberedPermissions>,
|
2026-07-19 19:17:34 +00:00
|
|
|
/// Registry of tasks currently registered to receive forwarded MCP
|
|
|
|
|
/// `ask_question` requests (story 1228). Kept fully separate from
|
|
|
|
|
/// `permission_registry` so a question answer is never conflated with a
|
|
|
|
|
/// permission decision.
|
|
|
|
|
pub question_registry: Arc<QuestionResponderRegistry>,
|
|
|
|
|
/// Pending question replies awaiting a chat reply, keyed by `request_id`
|
|
|
|
|
/// with a per-location FIFO index, mirroring `pending_perm_replies` but
|
|
|
|
|
/// for `ask_question` (story 1228).
|
|
|
|
|
pub pending_question_replies: Arc<PendingQuestionReplies>,
|
|
|
|
|
/// Seconds to wait for a user to answer a question before giving up
|
|
|
|
|
/// (fail-closed): the MCP tool returns an error to the asking agent.
|
|
|
|
|
pub question_timeout_secs: u64,
|
2026-04-26 02:23:23 +00:00
|
|
|
/// Project-scoped status broadcaster.
|
|
|
|
|
///
|
|
|
|
|
/// Consumers (chat transports, Web UI, agent context) call
|
|
|
|
|
/// [`StatusBroadcaster::subscribe`] to receive pipeline status events.
|
|
|
|
|
/// The broadcaster is project-scoped: events published here are delivered
|
|
|
|
|
/// only to subscribers of this instance, providing natural multi-project
|
|
|
|
|
/// isolation.
|
|
|
|
|
pub status: Arc<StatusBroadcaster>,
|
2026-05-15 11:57:00 +00:00
|
|
|
/// Protocol-agnostic chat dispatcher shared by all transport handlers.
|
|
|
|
|
///
|
|
|
|
|
/// Transport handlers call [`ChatDispatcher::submit`] instead of spawning
|
|
|
|
|
/// `claude -p` directly. The dispatcher applies a coalesce window and a
|
|
|
|
|
/// per-session serial lock, preventing duplicate concurrent spawns.
|
|
|
|
|
pub chat_dispatcher: Arc<ChatDispatcher>,
|
2026-04-25 15:04:37 +00:00
|
|
|
}
|
2026-04-25 20:37:10 +00:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
impl Services {
|
|
|
|
|
/// Build a minimal `Services` for testing with the given project root and
|
|
|
|
|
/// bot display name.
|
|
|
|
|
pub fn new_test(project_root: std::path::PathBuf, bot_name: String) -> std::sync::Arc<Self> {
|
2026-04-27 18:00:53 +00:00
|
|
|
let agents = std::sync::Arc::new(crate::agents::AgentPool::new_test(3000));
|
2026-04-25 20:37:10 +00:00
|
|
|
std::sync::Arc::new(Self {
|
|
|
|
|
project_root,
|
2026-04-27 18:00:53 +00:00
|
|
|
status: agents.status_broadcaster(),
|
|
|
|
|
agents,
|
2026-04-25 20:37:10 +00:00
|
|
|
bot_name,
|
|
|
|
|
bot_user_id: String::new(),
|
|
|
|
|
ambient_rooms: std::sync::Arc::new(std::sync::Mutex::new(HashSet::new())),
|
2026-07-16 13:13:41 +00:00
|
|
|
permission_registry: ResponderRegistry::new(),
|
|
|
|
|
pending_perm_replies: PendingPermReplies::new(),
|
2026-04-25 20:37:10 +00:00
|
|
|
permission_timeout_secs: 120,
|
2026-07-18 13:51:22 +00:00
|
|
|
remembered_permissions: RememberedPermissions::new(),
|
2026-07-19 19:17:34 +00:00
|
|
|
question_registry: QuestionResponderRegistry::new(),
|
|
|
|
|
pending_question_replies: PendingQuestionReplies::new(),
|
|
|
|
|
question_timeout_secs: 120,
|
2026-05-15 11:57:00 +00:00
|
|
|
chat_dispatcher: std::sync::Arc::new(ChatDispatcher::new(1_500)),
|
2026-04-25 20:37:10 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|