//! Managed application state — the product of the **composition root**. //! //! The composition root ([`build_app_state`]) is the *single* place that //! constructs concrete adapters (`new ConcreteAdapter`) and injects them as //! `Arc` into the use cases (ARCHITECTURE §1.1, §10). The use cases //! are then exposed through `tauri::State` to the command handlers. use std::collections::HashMap; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use application::{ CloseProject, CloseTab, CloseTerminal, ConfigureProfiles, CreateAgentFromScratch, CreateAgentFromTemplate, CreateLayout, CreateProject, CreateTemplate, DeleteAgent, DeleteLayout, DeleteProfile, DeleteTemplate, DetectAgentDrift, DetectProfiles, FirstRunState, GitBranches, GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage, GitStatus, GitUnstage, HealthUseCase, InspectConversation, LaunchAgent, ListAgents, ListLayouts, ListProfiles, ListProjects, ListSkills, ListTemplates, LiveAgentRegistry, LoadLayout, MoveTabToNewWindow, MutateLayout, OpenProject, OpenTerminal, OrchestratorService, ReadAgentContext, ReferenceProfiles, RenameLayout, ResizeTerminal, SaveProfile, SetActiveLayout, SnapshotRunningAgents, AssignSkillToAgent, CreateSkill, DeleteSkill, UnassignSkillFromAgent, UpdateSkill, CreateMemory, DeleteMemory, GetMemory, ListMemories, ReadMemoryIndex, RecallMemory, ResolveMemoryLinks, UpdateMemory, SyncAgentWithTemplate, TerminalSessions, UpdateAgentContext, UpdateTemplate, WriteToTerminal, }; use domain::ports::{ AgentContextStore, AgentRuntime, Clock, Embedder, EventBus, FileSystem, GitPort, IdGenerator, MemoryRecall, MemoryStore, ProcessSpawner, ProfileStore, ProjectStore, PtyPort, SkillStore, TemplateStore, }; use domain::{DomainEvent, EmbedderProfile, Project, ProjectId}; use infrastructure::{ embedder_from_profile, AdaptiveMemoryRecall, ClaudeTranscriptInspector, CliAgentRuntime, FsEmbedderProfileStore, FsMemoryStore, FsOrchestratorWatcher, FsProfileStore, FsProjectStore, FsSkillStore, FsTemplateStore, Git2Repository, IdeaiContextStore, LocalFileSystem, LocalProcessSpawner, NaiveMemoryRecall, OrchestratorWatchHandle, PortablePtyAdapter, SystemClock, TokioBroadcastEventBus, UuidGenerator, VectorMemoryRecall, ONNX_CACHE_SUBDIR, }; use crate::pty::PtyBridge; /// Everything the IPC layer needs at runtime, managed by Tauri. /// /// Use cases are stored behind `Arc` so handlers clone cheaply. The concrete /// adapters are owned here and never leak past the composition root as concrete /// types — downstream code only sees the `Arc` held inside the use /// cases. pub struct AppState { /// Trivial health use case validating the end-to-end wiring. pub health: Arc, /// Create a project (init `.ideai/`, register it). pub create_project: Arc, /// Open a project (load meta + manifest). pub open_project: Arc, /// Close a project (persist state). pub close_project: Arc, /// Close a tab. pub close_tab: Arc, /// List known projects. pub list_projects: Arc, /// Open a terminal (spawn PTY, register session). pub open_terminal: Arc, /// Write keystrokes to a terminal. pub write_terminal: Arc, /// Resize a terminal. pub resize_terminal: Arc, /// Close a terminal (kill PTY). pub close_terminal: Arc, /// Load a project's persisted layout tree. pub load_layout: Arc, /// Mutate + persist a project's layout tree. pub mutate_layout: Arc, /// List all named layouts for a project (#4). pub list_layouts: Arc, /// Create a new named layout (#4). pub create_layout: Arc, /// Rename a named layout (#4). pub rename_layout: Arc, /// Delete a named layout (#4). pub delete_layout: Arc, /// Set the active named layout (#4). pub set_active_layout: Arc, /// Freeze `agent_was_running` on every agent leaf before a PTY kill (T5). pub snapshot_running_agents: Arc, /// Detect which candidate profiles' CLIs are installed (first-run). pub detect_profiles: Arc, /// List configured profiles. pub list_profiles: Arc, /// Save (upsert) a profile. pub save_profile: Arc, /// Delete a profile. pub delete_profile: Arc, /// Persist the batch of chosen profiles (closes the first run). pub configure_profiles: Arc, /// Expose the pre-filled reference catalogue. pub reference_profiles: Arc, /// Whether the first-run wizard should show + the reference catalogue. pub first_run_state: Arc, /// The local PTY adapter, kept port-typed so the presentation layer can /// `subscribe_output` to pump bytes into the [`PtyBridge`] (it owns transport). pub pty_port: Arc, /// Active-terminal registry shared by the terminal use cases. pub terminal_sessions: Arc, /// The domain event bus (also handed to the event relay). pub event_bus: Arc, /// Generic PTY↔Channel bridge registry (consumed by L3). pub pty_bridge: Arc, // --- Agents (L6) --- /// Create a project agent from scratch. pub create_agent: Arc, /// List a project's agents. pub list_agents: Arc, /// Read an agent's Markdown context. pub read_agent_context: Arc, /// Overwrite an agent's Markdown context. pub update_agent_context: Arc, /// Delete an agent from the manifest. pub delete_agent: Arc, /// Launch an agent (spawn PTY, apply injection strategy). pub launch_agent: Arc, /// Best-effort inspection of a conversation (last topic + token indicator) /// for the resume popup (T7). Optional/extensible: backed by a `Vec` of /// [`domain::ports::SessionInspector`]s; an empty/missing match yields empty /// details, never an error. pub inspect_conversation: Arc, /// Project registry — used by agent commands to resolve a `Project` from an id. pub project_store: Arc, // --- Windows (L10) --- /// Detach a tab into a new OS window (persists the workspace topology). pub move_tab: Arc, // --- Templates & sync (L7) --- /// Create a template in the global store. pub create_template: Arc, /// Update a template's content (bumps version). pub update_template: Arc, /// List all templates in the global store. pub list_templates: Arc, /// Delete a template from the global store. pub delete_template: Arc, /// Create an agent from a template. pub create_agent_from_template: Arc, /// Detect which synchronized agents are behind their template. pub detect_agent_drift: Arc, /// Apply a template update to a synchronized agent. pub sync_agent_with_template: Arc, // --- Git (L8) --- /// Report the working-tree status of a repository. pub git_status: Arc, /// Stage a path. pub git_stage: Arc, /// Unstage a path. pub git_unstage: Arc, /// Create a commit. pub git_commit: Arc, /// List branches. pub git_branches: Arc, /// Check out a branch. pub git_checkout: Arc, /// Return the recent commit log. pub git_log: Arc, /// Initialise a repository. pub git_init: Arc, /// Return the commit graph for all local branches. pub git_graph: Arc, // --- Skills (L12) --- /// Create a skill in a scope's store. pub create_skill: Arc, /// Update a skill's content. pub update_skill: Arc, /// List skills in a scope. pub list_skills: Arc, /// Delete a skill from its scope's store. pub delete_skill: Arc, /// Assign a skill to an agent (records a `SkillRef`). pub assign_skill: Arc, /// Unassign a skill from an agent. pub unassign_skill: Arc, // --- Memory (LOT A — §14.5.1) --- /// Create a memory note in the project's store. pub create_memory: Arc, /// Replace an existing memory note. pub update_memory: Arc, /// List the project's memory notes. pub list_memories: Arc, /// Read one memory note by slug. pub get_memory: Arc, /// Delete a memory note. pub delete_memory: Arc, /// Read the structured `MEMORY.md` index. pub read_memory_index: Arc, /// Resolve a note's outgoing `[[slug]]` links. pub resolve_memory_links: Arc, /// Recall the most relevant memory entries for a query within a budget /// (LOT B — §14.5.2). pub recall_memory: Arc, // --- Orchestrator (§14.3) --- /// Dispatches validated orchestrator requests to the agent/skill use cases. /// Shared by every per-project filesystem watcher. pub orchestrator_service: Arc, /// Live orchestrator request watchers, keyed by project. One watcher per open /// project tails its `.ideai/requests/` tree; dropping the handle stops it. /// Guarded by a `Mutex` so the open/close commands can register/unregister /// watchers concurrently. pub orchestrator_watchers: Mutex>, } impl AppState { /// **Composition root.** Builds all adapters and use cases. /// /// `app_data_dir` is the machine-local IDE data directory (ARCHITECTURE /// §9.2), resolved by the caller via the Tauri path API and injected here so /// the stores never touch Tauri themselves (Dependency Inversion). /// /// This is the only function that constructs concrete adapters; every other /// layer depends on ports. Adapters added in later lots (PTY, git, remote) /// are wired in here. #[must_use] pub fn build(app_data_dir: PathBuf) -> Self { // --- Concrete adapters (driven adapters) --- let event_bus = Arc::new(TokioBroadcastEventBus::new()); let clock = Arc::new(SystemClock::new()); let ids = Arc::new(UuidGenerator::new()); let fs = Arc::new(LocalFileSystem::new()); let store = Arc::new(FsProjectStore::new( Arc::clone(&fs) as Arc, app_data_dir.to_string_lossy().into_owned(), )); // Port-typed handles for injection. let fs_port = Arc::clone(&fs) as Arc; let store_port = Arc::clone(&store) as Arc; let events_port = Arc::clone(&event_bus) as Arc; // --- Use cases (ports injected as Arc) --- let health = Arc::new(HealthUseCase::new( Arc::clone(&clock) as Arc, Arc::clone(&ids) as Arc, Arc::clone(&events_port), )); let create_project = Arc::new(CreateProject::new( Arc::clone(&store_port), Arc::clone(&fs_port), Arc::clone(&ids) as Arc, Arc::clone(&clock) as Arc, Arc::clone(&events_port), )); let open_project = Arc::new(OpenProject::new( Arc::clone(&store_port), Arc::clone(&fs_port), )); let close_project = Arc::new(CloseProject::new(Arc::clone(&store_port))); let close_tab = Arc::new(CloseTab::new(Arc::clone(&store_port))); let list_projects = Arc::new(ListProjects::new(Arc::clone(&store_port))); // --- PTY adapter + terminal use cases (L3) --- let pty = Arc::new(PortablePtyAdapter::new()); let pty_port = Arc::clone(&pty) as Arc; let terminal_sessions = Arc::new(TerminalSessions::new()); let open_terminal = Arc::new(OpenTerminal::new( Arc::clone(&pty_port), Arc::clone(&terminal_sessions), Arc::clone(&events_port), )); let write_terminal = Arc::new(WriteToTerminal::new( Arc::clone(&pty_port), Arc::clone(&terminal_sessions), )); let resize_terminal = Arc::new(ResizeTerminal::new( Arc::clone(&pty_port), Arc::clone(&terminal_sessions), )); let close_terminal = Arc::new(CloseTerminal::new( Arc::clone(&pty_port), Arc::clone(&terminal_sessions), )); // --- Layout use cases (L4 + #4) --- let load_layout = Arc::new(LoadLayout::new( Arc::clone(&store_port), Arc::clone(&fs_port), )); let mutate_layout = Arc::new(MutateLayout::new( Arc::clone(&store_port), Arc::clone(&fs_port), Arc::clone(&events_port), )); let list_layouts = Arc::new(ListLayouts::new( Arc::clone(&store_port), Arc::clone(&fs_port), )); let create_layout = Arc::new(CreateLayout::new( Arc::clone(&store_port), Arc::clone(&fs_port), Arc::clone(&ids) as Arc, Arc::clone(&events_port), )); let rename_layout = Arc::new(RenameLayout::new( Arc::clone(&store_port), Arc::clone(&fs_port), Arc::clone(&events_port), )); let delete_layout = Arc::new(DeleteLayout::new( Arc::clone(&store_port), Arc::clone(&fs_port), Arc::clone(&events_port), )); let set_active_layout = Arc::new(SetActiveLayout::new( Arc::clone(&store_port), Arc::clone(&fs_port), Arc::clone(&events_port), )); // Close-time snapshot of running agents (T5). Shares the SAME live-session // registry as the terminal/agent use cases, so its liveness check reflects // the very PTYs the shutdown hook is about to kill — it must run *before*. let snapshot_running_agents = Arc::new(SnapshotRunningAgents::new( Arc::clone(&store_port), Arc::clone(&fs_port), Arc::clone(&terminal_sessions) as Arc, )); // --- Profiles & AI runtime (L5) --- // One generic, profile-driven runtime adapter (Open/Closed): it holds the // process spawner used for detection. The profile store persists // `profiles.json` in the same machine-local app-data dir as the project // registry. let spawner = Arc::new(LocalProcessSpawner::new()); let spawner_port = Arc::clone(&spawner) as Arc; let runtime = Arc::new(CliAgentRuntime::new(Arc::clone(&spawner_port))); let runtime_port = Arc::clone(&runtime) as Arc; let profile_store = Arc::new(FsProfileStore::new( Arc::clone(&fs_port), app_data_dir.to_string_lossy().into_owned(), )); let profile_store_port = Arc::clone(&profile_store) as Arc; let detect_profiles = Arc::new(DetectProfiles::new(Arc::clone(&runtime_port))); let list_profiles = Arc::new(ListProfiles::new(Arc::clone(&profile_store_port))); let save_profile = Arc::new(SaveProfile::new(Arc::clone(&profile_store_port))); let delete_profile = Arc::new(DeleteProfile::new(Arc::clone(&profile_store_port))); let configure_profiles = Arc::new(ConfigureProfiles::new(Arc::clone(&profile_store_port))); let reference_profiles = Arc::new(ReferenceProfiles::new()); let first_run_state = Arc::new(FirstRunState::new(Arc::clone(&profile_store_port))); let pty_bridge = Arc::new(PtyBridge::new()); // --- Agent context store + use cases (L6) --- let contexts = Arc::new(IdeaiContextStore::new(Arc::clone(&fs_port))); let contexts_port = Arc::clone(&contexts) as Arc; // --- Skill store (L12) --- // Global skills live in the machine-local app-data dir; project skills are // resolved per call from each project's `.ideai/` (so one store serves all // open projects). Shared by the skill use cases and the agent launcher // (assigned-skill injection into the convention file, §14.2). let skill_store = Arc::new(FsSkillStore::new( Arc::clone(&fs_port), app_data_dir.to_string_lossy().into_owned(), )); let skill_store_port = Arc::clone(&skill_store) as Arc; // Memory store + naïve recall (LOT A/B — §14.5.1/§14.5.4). Built here so the // recall port can be injected into LaunchAgent below (it composes the project // memory recall into the convention file at activation); the memory use cases // are wired further down from these same instances. let memory_store = Arc::new(FsMemoryStore::new(Arc::clone(&fs_port))); let memory_store_port = Arc::clone(&memory_store) as Arc; // Load the configured embedder profile (mono-profile for now: take the last // listed, fall back to `none`). A multi-profile selector is a follow-up; the // `none` default keeps recall strictly naïve and dependency-free. let embedder_store = FsEmbedderProfileStore::new( Arc::clone(&fs_port), app_data_dir.to_string_lossy().into_owned(), ); // `build` may run inside an ambient async runtime (Tauri's `setup`, or // `#[tokio::test]`), so blocking the current thread on a future panics. // Drive the one-shot load on a dedicated thread with its own runtime. let embedder_profile = std::thread::scope(|s| { s.spawn(|| { tokio::runtime::Builder::new_current_thread() .enable_all() .build() .ok() .and_then(|rt| rt.block_on(embedder_store.list()).ok()) .and_then(|mut v| v.pop()) }) .join() .ok() .flatten() }) .unwrap_or_else(EmbedderProfile::none); let onnx_cache_dir = app_data_dir.join(ONNX_CACHE_SUBDIR); let memory_recall_port = build_memory_recall( Arc::clone(&fs_port), Arc::clone(&memory_store_port), &embedder_profile, &onnx_cache_dir, ); let create_agent = Arc::new(CreateAgentFromScratch::new( Arc::clone(&contexts_port), Arc::clone(&ids) as Arc, Arc::clone(&events_port), )); let list_agents = Arc::new(ListAgents::new(Arc::clone(&contexts_port))); let read_agent_context = Arc::new(ReadAgentContext::new(Arc::clone(&contexts_port))); let update_agent_context = Arc::new(UpdateAgentContext::new(Arc::clone(&contexts_port))); let delete_agent = Arc::new(DeleteAgent::new( Arc::clone(&contexts_port), Arc::clone(&events_port), )); // LaunchAgent shares the SAME pty_port and terminal_sessions as the terminal // use cases — indispensable for the PtyBridge to work correctly. let launch_agent = Arc::new(LaunchAgent::new( Arc::clone(&contexts_port), Arc::clone(&profile_store_port), Arc::clone(&runtime_port), Arc::clone(&fs_port), Arc::clone(&pty_port), Arc::clone(&skill_store_port), Arc::clone(&terminal_sessions), Arc::clone(&events_port), Arc::clone(&ids) as Arc, Arc::clone(&memory_recall_port), )); // --- Conversation inspection (T7) --- // Best-effort, optional, extensible: a `Vec` of SessionInspectors routed // by profile. Adding an inspectable CLI = pushing one more adapter here. // The Claude inspector reads `/.claude/projects//.jsonl`; // `$HOME` is resolved from the environment (empty string if unset — the // inspector then simply finds nothing and yields empty details). let home_dir = std::env::var("HOME") .or_else(|_| std::env::var("USERPROFILE")) .unwrap_or_default(); let inspectors: Vec> = vec![Arc::new(ClaudeTranscriptInspector::new( Arc::clone(&fs_port), home_dir, ))]; let inspect_conversation = Arc::new(InspectConversation::new( Arc::clone(&contexts_port), Arc::clone(&profile_store_port), inspectors, )); let project_store = Arc::clone(&store_port); // --- Template store + use cases (L7) --- let template_store = Arc::new(FsTemplateStore::new( Arc::clone(&fs_port), app_data_dir.to_string_lossy().into_owned(), )); let template_store_port = Arc::clone(&template_store) as Arc; let create_template = Arc::new(CreateTemplate::new( Arc::clone(&template_store_port), Arc::clone(&ids) as Arc, )); let update_template = Arc::new(UpdateTemplate::new( Arc::clone(&template_store_port), Arc::clone(&events_port), )); let list_templates = Arc::new(ListTemplates::new(Arc::clone(&template_store_port))); let delete_template = Arc::new(DeleteTemplate::new(Arc::clone(&template_store_port))); let create_agent_from_template = Arc::new(CreateAgentFromTemplate::new( Arc::clone(&template_store_port), Arc::clone(&contexts_port), Arc::clone(&ids) as Arc, Arc::clone(&events_port), )); let detect_agent_drift = Arc::new(DetectAgentDrift::new( Arc::clone(&template_store_port), Arc::clone(&contexts_port), Arc::clone(&events_port), )); let sync_agent_with_template = Arc::new(SyncAgentWithTemplate::new( Arc::clone(&template_store_port), Arc::clone(&contexts_port), Arc::clone(&events_port), )); // --- Git adapter + use cases (L8) --- let git = Arc::new(Git2Repository::new()); let git_port = Arc::clone(&git) as Arc; let git_status = Arc::new(GitStatus::new(Arc::clone(&git_port))); let git_stage = Arc::new(GitStage::new(Arc::clone(&git_port))); let git_unstage = Arc::new(GitUnstage::new(Arc::clone(&git_port))); let git_commit = Arc::new(GitCommit::new(Arc::clone(&git_port), Arc::clone(&events_port))); let git_branches = Arc::new(GitBranches::new(Arc::clone(&git_port))); let git_checkout = Arc::new(GitCheckout::new( Arc::clone(&git_port), Arc::clone(&events_port), )); let git_log = Arc::new(GitLog::new(Arc::clone(&git_port))); let git_init = Arc::new(GitInit::new(Arc::clone(&git_port), Arc::clone(&events_port))); let git_graph = Arc::new(GitGraph::new(Arc::clone(&git_port))); // --- Skill use cases (L12) --- // Reuse the skill store (built above for the launcher) and the shared // agent context store for the agent↔skill assignment. let create_skill = Arc::new(CreateSkill::new( Arc::clone(&skill_store_port), Arc::clone(&ids) as Arc, )); let update_skill = Arc::new(UpdateSkill::new(Arc::clone(&skill_store_port))); let list_skills = Arc::new(ListSkills::new(Arc::clone(&skill_store_port))); let delete_skill = Arc::new(DeleteSkill::new(Arc::clone(&skill_store_port))); let assign_skill = Arc::new(AssignSkillToAgent::new( Arc::clone(&contexts_port), Arc::clone(&events_port), )); let unassign_skill = Arc::new(UnassignSkillFromAgent::new( Arc::clone(&contexts_port), Arc::clone(&events_port), )); // --- Memory use cases (LOT A — §14.5.1) --- // `memory_store` / `memory_store_port` / `memory_recall_port` are built // earlier (above LaunchAgent, which needs the recall port). A single // FsMemoryStore takes the project root per call (like the skill store), so // one instance serves every open project. The mutating use cases share the // event bus to announce Memory{Saved,Deleted}. let create_memory = Arc::new(CreateMemory::new( Arc::clone(&memory_store_port), Arc::clone(&events_port), )); let update_memory = Arc::new(UpdateMemory::new( Arc::clone(&memory_store_port), Arc::clone(&events_port), )); let list_memories = Arc::new(ListMemories::new(Arc::clone(&memory_store_port))); let get_memory = Arc::new(GetMemory::new(Arc::clone(&memory_store_port))); let delete_memory = Arc::new(DeleteMemory::new( Arc::clone(&memory_store_port), Arc::clone(&events_port), )); let read_memory_index = Arc::new(ReadMemoryIndex::new(Arc::clone(&memory_store_port))); let resolve_memory_links = Arc::new(ResolveMemoryLinks::new(Arc::clone(&memory_store_port))); // Naïve recall (LOT B): the same instance injected into LaunchAgent above — // composes the store, returns index entries in order truncated to the token // budget. The default, dependency-free MemoryRecall; substitutable by a // VectorMemoryRecall (LOT C). let recall_memory = Arc::new(RecallMemory::new(Arc::clone(&memory_recall_port))); // --- Orchestrator service (§14.3) --- // Dispatches file-based orchestrator requests through the *same* use cases // the UI drives (IdeA stays the single source of truth for the agent/skill // lifecycle). The per-project watcher that feeds it is started lazily when // a project is opened (see `ensure_orchestrator_watch`). let orchestrator_service = Arc::new(OrchestratorService::new( Arc::clone(&create_agent), Arc::clone(&launch_agent), Arc::clone(&list_agents), Arc::clone(&close_terminal), Arc::clone(&update_agent_context), Arc::clone(&create_skill), Arc::clone(&profile_store_port), Arc::clone(&terminal_sessions), )); // --- Windows (L10) --- let move_tab = Arc::new(MoveTabToNewWindow::new( Arc::clone(&store_port), Arc::clone(&ids) as Arc, )); Self { health, create_project, open_project, close_project, close_tab, list_projects, open_terminal, write_terminal, resize_terminal, close_terminal, load_layout, mutate_layout, list_layouts, create_layout, rename_layout, delete_layout, set_active_layout, snapshot_running_agents, detect_profiles, list_profiles, save_profile, delete_profile, configure_profiles, reference_profiles, first_run_state, pty_port, terminal_sessions, event_bus, pty_bridge, create_agent, list_agents, read_agent_context, update_agent_context, delete_agent, launch_agent, inspect_conversation, project_store, create_template, update_template, list_templates, delete_template, create_agent_from_template, detect_agent_drift, sync_agent_with_template, git_status, git_stage, git_unstage, git_commit, git_branches, git_checkout, git_log, git_init, git_graph, create_skill, update_skill, list_skills, delete_skill, assign_skill, unassign_skill, create_memory, update_memory, list_memories, get_memory, delete_memory, read_memory_index, resolve_memory_links, recall_memory, orchestrator_service, orchestrator_watchers: Mutex::new(HashMap::new()), move_tab, } } /// Starts a [`FsOrchestratorWatcher`] for `project` if one is not already /// running for it (idempotent). The watcher tails the project's /// `.ideai/requests/` tree and dispatches each request through the shared /// [`OrchestratorService`]; processed-request events are republished on the /// domain event bus so the relay surfaces them to the frontend. /// /// Called from the `open_project` / `create_project` commands. Must run inside /// the Tokio runtime (the watcher spawns a background task) — Tauri async /// commands satisfy this. pub fn ensure_orchestrator_watch(&self, project: &Project) { let mut watchers = self .orchestrator_watchers .lock() .expect("orchestrator watcher registry poisoned"); if watchers.contains_key(&project.id) { return; } let bus = Arc::clone(&self.event_bus); let events: Arc = Arc::new(move |event| bus.publish(event)); let handle = FsOrchestratorWatcher::start( project.clone(), Arc::clone(&self.orchestrator_service), events, ); watchers.insert(project.id, handle); } /// Returns the ids of every currently-open project. /// /// Derived from the orchestrator watcher registry, which holds exactly one /// entry per open project (started on open/create, dropped on close). Used by /// the shutdown hook to snapshot running agents across all open projects /// before the global PTY kill. #[must_use] pub fn open_project_ids(&self) -> Vec { self.orchestrator_watchers .lock() .map(|w| w.keys().copied().collect()) .unwrap_or_default() } /// Stops and removes the orchestrator watcher for `project_id`, if any. /// Called from `close_project` so a closed project stops consuming requests. pub fn stop_orchestrator_watch(&self, project_id: &ProjectId) { if let Some(handle) = self .orchestrator_watchers .lock() .expect("orchestrator watcher registry poisoned") .remove(project_id) { handle.stop(); } } } /// Build the project memory recall port from an embedder profile. /// /// This is the only place that knows the concrete recall adapters (DIP): callers /// only ever see `Arc`. With the default `none` profile, /// `embedder_from_profile` returns `None` and we hand back the plain /// `NaiveMemoryRecall` — strictly identical, dependency-free behaviour. When an /// embedder is configured, we wrap naïve + vector recall in an /// `AdaptiveMemoryRecall` that switches stages live per the profile strategy. pub(crate) fn build_memory_recall( fs: Arc, store: Arc, profile: &EmbedderProfile, onnx_cache_dir: &std::path::Path, ) -> Arc { let naive = Arc::new(NaiveMemoryRecall::new(Arc::clone(&store))) as Arc; match embedder_from_profile(profile, onnx_cache_dir) { None => naive, Some(embedder) => { let embedder: Arc = Arc::from(embedder); let vector: Arc = Arc::new(VectorMemoryRecall::new( embedder, Arc::clone(&store), Arc::clone(&fs), )); Arc::new(AdaptiveMemoryRecall::new( naive, vector, Arc::clone(&store), profile.strategy, )) } } } #[cfg(test)] mod tests { //! Wiring contract for [`build_memory_recall`] (Pièce 1, §14.5.5). //! //! The *behaviour* of `NaiveMemoryRecall` / `VectorMemoryRecall` / //! `AdaptiveMemoryRecall` is covered exhaustively in //! `infrastructure/tests/vector_recall.rs`. Here we only pin the **composition //! root contract**: with the default `EmbedderProfile::none()` profile (the //! dependency-free default), `build_memory_recall` must hand back a recall whose //! observable behaviour is *identical* to a bare `NaiveMemoryRecall` over the //! same store — same index ordering, same budget truncation. //! //! `build_memory_recall` is `pub(crate)`, so this lives in `state.rs` (it is not //! reachable from the `tests/` integration crate). Everything is in-memory and //! deterministic. use std::collections::HashMap; use std::sync::Mutex; use super::*; use async_trait::async_trait; use domain::markdown::MarkdownDoc; use domain::memory::{Memory, MemoryFrontmatter, MemorySlug, MemoryType}; use domain::ports::{DirEntry, FsError, MemoryQuery, RemotePath}; use domain::project::ProjectPath; // In-memory FileSystem (same minimal shape as the infrastructure test fixtures). #[derive(Default)] struct MemFs { files: Mutex>>, } #[async_trait] impl FileSystem for MemFs { async fn read(&self, path: &RemotePath) -> Result, FsError> { self.files .lock() .unwrap() .get(path.as_str()) .cloned() .ok_or_else(|| FsError::NotFound(path.as_str().to_string())) } async fn write(&self, path: &RemotePath, data: &[u8]) -> Result<(), FsError> { self.files .lock() .unwrap() .insert(path.as_str().to_string(), data.to_vec()); Ok(()) } async fn exists(&self, path: &RemotePath) -> Result { Ok(self.files.lock().unwrap().contains_key(path.as_str())) } async fn create_dir_all(&self, _path: &RemotePath) -> Result<(), FsError> { Ok(()) } async fn list(&self, _path: &RemotePath) -> Result, FsError> { Ok(Vec::new()) } async fn symlink(&self, _src: &RemotePath, _dst: &RemotePath) -> Result<(), FsError> { Ok(()) } } fn root() -> ProjectPath { ProjectPath::new("/proj").unwrap() } fn note(slug: &str, hook: &str) -> Memory { Memory::new( MemoryFrontmatter { name: MemorySlug::new(slug).unwrap(), description: hook.to_string(), r#type: MemoryType::Project, }, MarkdownDoc::new("# body"), ) .unwrap() } fn query(text: &str, budget: usize) -> MemoryQuery { MemoryQuery { text: text.to_string(), token_budget: budget, } } async fn seed_store() -> (Arc, Arc) { let fs: Arc = Arc::new(MemFs::default()); let store_concrete = Arc::new(FsMemoryStore::new(Arc::clone(&fs))); for n in [ note("alpha", "apple orange grape"), note("beta", "kiwi mango papaya"), note("gamma", "carrot potato onion"), ] { store_concrete.save(&root(), &n).await.unwrap(); } let store: Arc = store_concrete; (fs, store) } /// With the default `none` profile, `build_memory_recall` is observationally a /// plain `NaiveMemoryRecall`: same index ordering and same budget truncation, /// entry-for-entry, across a representative spread of budgets. #[tokio::test] async fn build_memory_recall_none_profile_matches_naive_recall() { let (fs, store) = seed_store().await; let wired = build_memory_recall( Arc::clone(&fs), Arc::clone(&store), &EmbedderProfile::none(), std::path::Path::new("/unused-onnx-cache"), ); let naive = NaiveMemoryRecall::new(Arc::clone(&store)); // 0 ⇒ empty; mid budgets ⇒ partial truncation; huge ⇒ full index order. for budget in [0usize, 3, 6, 100, 100_000] { let q = query("kiwi mango papaya", budget); let expected = naive.recall(&root(), &q).await.unwrap(); let got = wired.recall(&root(), &q).await.unwrap(); assert_eq!( got, expected, "none profile must equal bare NaiveMemoryRecall at budget {budget}" ); } } }