Skip to main content

khive_pack_brain/
pack.rs

1//! `BrainPack` struct and inventory factory.
2
3use std::sync::Mutex;
4
5use khive_runtime::{KhiveRuntime, NamespaceToken, RuntimeError};
6use khive_types::{HandlerDef, Pack};
7
8use khive_brain_core::BrainState;
9
10use crate::handlers::BRAIN_HANDLERS;
11use crate::persist;
12
13/// Default entity cache capacity for the `balanced-recall-v1` per-entity posterior state.
14pub const ENTITY_CACHE_CAPACITY: usize = 10_000;
15
16// Test-only hook that fires inside dispatch(), after ensure_loaded returns and
17// before the handler acquires self.state.  Lets tests inject a concurrent
18// namespace swap to prove the dispatch gate prevents cross-namespace pollution.
19#[cfg(test)]
20pub(crate) struct DispatchHook {
21    pub reached_tx: tokio::sync::oneshot::Sender<()>,
22    pub proceed_rx: tokio::sync::oneshot::Receiver<()>,
23}
24
25#[cfg(test)]
26pub(crate) static DISPATCH_INTERLEAVE_HOOK: std::sync::Mutex<Option<DispatchHook>> =
27    std::sync::Mutex::new(None);
28
29#[cfg(test)]
30pub(crate) fn set_dispatch_interleave_hook(hook: DispatchHook) {
31    *DISPATCH_INTERLEAVE_HOOK.lock().unwrap() = Some(hook);
32}
33
34#[cfg(test)]
35pub(crate) fn clear_dispatch_interleave_hook() {
36    *DISPATCH_INTERLEAVE_HOOK.lock().unwrap() = None;
37}
38
39/// Sync the `balanced-recall-v1` profile record to match the live `balanced_recall` state.
40pub(crate) fn sync_balanced_recall_record(state: &mut BrainState) {
41    let total_ev = state.balanced_recall.total_events;
42    let snap_val = serde_json::to_value(state.balanced_recall.to_snapshot()).ok();
43    if let Some(record) = state.profiles.get_mut("balanced-recall-v1") {
44        record.total_events = total_ev;
45        record.state_snapshot = snap_val;
46    }
47}
48
49/// Brain pack — profile-management registry.
50pub struct BrainPack {
51    pub(crate) runtime: KhiveRuntime,
52    /// Profile registry + active balanced-recall state.
53    pub(crate) state: Mutex<BrainState>,
54    /// Tracks which namespaces are loaded from DB and dirty event counts.
55    pub(crate) persistence: Mutex<persist::PersistenceTracker>,
56    /// Serialises the (ensure_loaded → handler) pair so no namespace swap can
57    /// occur between the two steps.  Must be a tokio async mutex because the
58    /// guard is held across .await points inside dispatch().
59    ///
60    /// Lock order: dispatch_gate (outermost) → persistence → state.
61    /// Nothing inside ensure_loaded or any handler acquires dispatch_gate,
62    /// so there is no cycle and no deadlock risk.
63    pub(crate) dispatch_gate: tokio::sync::Mutex<()>,
64}
65
66impl Pack for BrainPack {
67    const NAME: &'static str = "brain";
68    const NOTE_KINDS: &'static [&'static str] = &[];
69    const ENTITY_KINDS: &'static [&'static str] = &[];
70    const HANDLERS: &'static [HandlerDef] = BRAIN_HANDLERS;
71    const REQUIRES: &'static [&'static str] = &["kg"];
72}
73
74impl BrainPack {
75    /// Create a new pack bound to the given runtime.
76    pub fn new(runtime: KhiveRuntime) -> Self {
77        let state = BrainState::new(ENTITY_CACHE_CAPACITY);
78        Self {
79            runtime,
80            state: Mutex::new(state),
81            persistence: Mutex::new(persist::PersistenceTracker::new()),
82            dispatch_gate: tokio::sync::Mutex::new(()),
83        }
84    }
85
86    #[cfg(test)]
87    pub fn activate_namespace_for_test(&self, namespace: &str) {
88        self.persistence
89            .lock()
90            .unwrap()
91            .mark_loaded(namespace.into());
92    }
93
94    pub(crate) async fn ensure_loaded(&self, token: &NamespaceToken) -> Result<(), RuntimeError> {
95        persist::ensure_loaded(
96            &self.runtime,
97            token,
98            &self.persistence,
99            &self.state,
100            ENTITY_CACHE_CAPACITY,
101        )
102        .await
103    }
104
105    /// Public snapshot of the current `BrainState`.
106    pub fn snapshot(&self) -> khive_brain_core::BrainStateSnapshot {
107        self.state.lock().unwrap().to_snapshot()
108    }
109
110    /// Return the `total_events` counter for a namespace stored in the cold/saved
111    /// state buckets inside `PersistenceTracker`.  Returns `None` when no state
112    /// has been initialised for the given namespace.
113    ///
114    /// Intended for test verification only.  Production code should access state
115    /// via `ensure_loaded` + `snapshot()`.
116    #[cfg(test)]
117    pub fn cold_namespace_total_events(&self, namespace: &str) -> Option<u64> {
118        self.persistence.lock().unwrap().total_events_for(namespace)
119    }
120}
121
122struct BrainPackFactory;
123
124impl khive_runtime::PackFactory for BrainPackFactory {
125    fn name(&self) -> &'static str {
126        "brain"
127    }
128
129    fn requires(&self) -> &'static [&'static str] {
130        &["kg"]
131    }
132
133    fn create(&self, runtime: KhiveRuntime) -> Box<dyn khive_runtime::pack::PackRuntime> {
134        Box::new(BrainPack::new(runtime))
135    }
136
137    // Overrides the default `create`-based install so the dispatch hook
138    // observes the exact same `BrainPack` instance the runtime mutates,
139    // instead of a second, state-divergent instance.
140    fn create_install(&self, runtime: KhiveRuntime) -> khive_runtime::PackInstall {
141        let brain = std::sync::Arc::new(BrainPack::new(runtime));
142        khive_runtime::PackInstall {
143            runtime: Box::new(BrainPackRuntime(std::sync::Arc::clone(&brain))),
144            resolver: None,
145            dispatch_hook: Some(brain),
146        }
147    }
148}
149
150/// Forwards the full `PackRuntime` surface to the shared inner `BrainPack`
151/// instance so the pack registry's runtime and the registered dispatch hook
152/// (see `create_install`) observe the same state and persistence tracker.
153struct BrainPackRuntime(std::sync::Arc<BrainPack>);
154
155#[async_trait::async_trait]
156impl khive_runtime::pack::PackRuntime for BrainPackRuntime {
157    fn name(&self) -> &str {
158        self.0.name()
159    }
160
161    fn note_kinds(&self) -> &'static [&'static str] {
162        self.0.note_kinds()
163    }
164
165    fn entity_kinds(&self) -> &'static [&'static str] {
166        self.0.entity_kinds()
167    }
168
169    fn handlers(&self) -> &'static [khive_runtime::HandlerDef] {
170        self.0.handlers()
171    }
172
173    fn edge_rules(&self) -> &'static [khive_types::EdgeEndpointRule] {
174        self.0.edge_rules()
175    }
176
177    fn requires(&self) -> &'static [&'static str] {
178        self.0.requires()
179    }
180
181    fn note_kind_specs(&self) -> &'static [khive_runtime::NoteKindSpec] {
182        self.0.note_kind_specs()
183    }
184
185    fn kind_hook(&self, kind: &str) -> Option<std::sync::Arc<dyn khive_runtime::KindHook>> {
186        self.0.kind_hook(kind)
187    }
188
189    fn schema_plan(&self) -> khive_runtime::SchemaPlan {
190        self.0.schema_plan()
191    }
192
193    fn validation_rules(&self) -> &'static [khive_runtime::ValidationRule] {
194        self.0.validation_rules()
195    }
196
197    fn register_embedders(&self, runtime: &KhiveRuntime) {
198        self.0.register_embedders(runtime)
199    }
200
201    fn register_entity_type_validator_with_types(
202        &self,
203        runtime: &KhiveRuntime,
204        pack_entity_types: &[khive_types::EntityTypeDef],
205    ) {
206        self.0
207            .register_entity_type_validator_with_types(runtime, pack_entity_types)
208    }
209
210    async fn warm(&self) {
211        self.0.warm().await
212    }
213
214    async fn dispatch(
215        &self,
216        verb: &str,
217        params: serde_json::Value,
218        registry: &khive_runtime::VerbRegistry,
219        token: &NamespaceToken,
220    ) -> Result<serde_json::Value, RuntimeError> {
221        self.0.dispatch(verb, params, registry, token).await
222    }
223}
224
225inventory::submit! { khive_runtime::PackRegistration(&BrainPackFactory) }