//! 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` is //! constructed once in `main.rs` and cloned into `AppContext` and each //! transport's context struct. use crate::agents::AgentPool; use crate::chat::dispatcher::ChatDispatcher; use crate::service::permission_router::{ PendingPermReplies, RememberedPermissions, ResponderRegistry, }; use crate::service::question_router::{PendingQuestionReplies, QuestionResponderRegistry}; use crate::service::status::StatusBroadcaster; use std::collections::HashSet; 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, /// 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>>, /// 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, /// 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, /// Seconds to wait for a user to respond to a permission prompt before /// auto-denying (fail-closed). pub permission_timeout_secs: u64, /// 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, /// 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, /// 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, /// 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, /// 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, /// 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, } #[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 { let agents = std::sync::Arc::new(crate::agents::AgentPool::new_test(3000)); std::sync::Arc::new(Self { project_root, status: agents.status_broadcaster(), agents, bot_name, bot_user_id: String::new(), ambient_rooms: std::sync::Arc::new(std::sync::Mutex::new(HashSet::new())), permission_registry: ResponderRegistry::new(), pending_perm_replies: PendingPermReplies::new(), permission_timeout_secs: 120, remembered_permissions: RememberedPermissions::new(), question_registry: QuestionResponderRegistry::new(), pending_question_replies: PendingQuestionReplies::new(), question_timeout_secs: 120, chat_dispatcher: std::sync::Arc::new(ChatDispatcher::new(1_500)), }) } }