lean_ctx/core/context_os/
mod.rs1use std::path::PathBuf;
2use std::sync::{Arc, Mutex, OnceLock};
3
4mod shared_sessions;
5pub use shared_sessions::{SharedSessionKey, SharedSessionStore};
6
7mod context_bus;
8pub mod observation;
9pub(crate) use context_bus::AdmittedLegacyObservation;
10pub use context_bus::{
11 ConsistencyLevel, ContextBus, ContextEventKindV1, ContextEventV1, FilteredSubscription,
12 TopicFilter,
13};
14pub use observation::{
15 LocalObservationPayload, LocalObservationSource, LocalObservationV1,
16 ObservationPersistenceError,
17};
18
19pub mod redaction;
20pub use redaction::{RedactionLevel, redact_event_payload, redact_payload_value};
21
22pub enum SubscriptionKind {
25 Unfiltered(tokio::sync::broadcast::Receiver<ContextEventV1>),
26 Filtered(FilteredSubscription),
27}
28
29impl SubscriptionKind {
30 pub async fn recv(
31 &mut self,
32 ) -> Result<ContextEventV1, tokio::sync::broadcast::error::RecvError> {
33 match self {
34 Self::Unfiltered(rx) => rx.recv().await,
35 Self::Filtered(fs) => fs.recv_filtered().await,
36 }
37 }
38}
39
40mod metrics;
41pub use metrics::{ContextOsMetrics, MetricsSnapshot};
42
43#[derive(Clone)]
48pub struct ContextOsRuntime {
49 pub shared_sessions: Arc<SharedSessionStore>,
50 pub bus: Arc<ContextBus>,
51 pub metrics: Arc<ContextOsMetrics>,
52}
53
54impl Default for ContextOsRuntime {
55 fn default() -> Self {
56 Self::try_new().expect("open context-os runtime")
57 }
58}
59
60impl ContextOsRuntime {
61 fn try_new() -> Result<Self, ObservationPersistenceError> {
62 let base = if crate::core::data_dir::sandboxed() {
63 crate::core::data_dir::test_sandbox_dir()
64 } else {
65 crate::core::data_dir::lean_ctx_data_dir().map_err(std::io::Error::other)?
66 };
67 let path = base.join("context-os").join("context-os.db");
68 let bus = ContextBus::try_open_at(path)?;
69
70 Ok(Self {
71 shared_sessions: Arc::new(SharedSessionStore::new()),
72 bus: Arc::new(bus),
73 metrics: Arc::new(ContextOsMetrics::default()),
74 })
75 }
76
77 pub fn new() -> Self {
78 Self::default()
79 }
80
81 pub fn data_dir() -> Option<PathBuf> {
82 crate::core::data_dir::lean_ctx_data_dir().ok()
83 }
84}
85
86static RUNTIME: OnceLock<Arc<ContextOsRuntime>> = OnceLock::new();
87static RUNTIME_INIT: Mutex<()> = Mutex::new(());
88
89pub fn runtime() -> Arc<ContextOsRuntime> {
90 try_runtime().expect("open context-os runtime")
91}
92
93pub fn try_runtime() -> Result<Arc<ContextOsRuntime>, ObservationPersistenceError> {
96 if let Some(runtime) = RUNTIME.get() {
97 return Ok(Arc::clone(runtime));
98 }
99 let _initialization = RUNTIME_INIT
103 .lock()
104 .unwrap_or_else(std::sync::PoisonError::into_inner);
105 if let Some(runtime) = RUNTIME.get() {
106 return Ok(Arc::clone(runtime));
107 }
108 let candidate = Arc::new(ContextOsRuntime::try_new()?);
109 let _ = RUNTIME.set(candidate);
110 RUNTIME.get().cloned().ok_or_else(|| {
111 ObservationPersistenceError::InvalidRow("runtime initialization did not complete".into())
112 })
113}
114
115pub fn emit_event(
117 workspace_id: &str,
118 channel_id: &str,
119 kind: &ContextEventKindV1,
120 actor: Option<&str>,
121 payload: serde_json::Value,
122) {
123 let rt = runtime();
124 if rt
125 .bus
126 .append(workspace_id, channel_id, kind, actor, payload)
127 .is_some()
128 {
129 rt.metrics.record_event_appended();
130 rt.metrics.record_event_broadcast();
131 rt.metrics.record_workspace_active(workspace_id);
132 }
133}
134
135pub fn emit_directed_event(
137 workspace_id: &str,
138 channel_id: &str,
139 kind: &ContextEventKindV1,
140 actor: Option<&str>,
141 payload: serde_json::Value,
142 target_agents: Vec<String>,
143) {
144 let rt = runtime();
145 if rt
146 .bus
147 .append_directed(
148 workspace_id,
149 channel_id,
150 kind,
151 actor,
152 payload,
153 target_agents,
154 )
155 .is_some()
156 {
157 rt.metrics.record_event_appended();
158 rt.metrics.record_event_broadcast();
159 rt.metrics.record_workspace_active(workspace_id);
160 }
161}
162
163pub fn secondary_event_kind(tool: &str, action: Option<&str>) -> Option<ContextEventKindV1> {
165 match tool {
166 "ctx_session" => {
167 let a = action.unwrap_or("");
168 if matches!(
169 a,
170 "save"
171 | "set_task"
172 | "task"
173 | "checkpoint"
174 | "finding"
175 | "decision"
176 | "reset"
177 | "import"
178 | "export"
179 ) {
180 Some(ContextEventKindV1::SessionMutated)
181 } else {
182 None
183 }
184 }
185 "ctx_handoff" | "ctx_workflow" | "ctx_share" => Some(ContextEventKindV1::SessionMutated),
186 "ctx_knowledge" | "ctx_knowledge_relations" => {
187 let a = action.unwrap_or("");
188 if matches!(
189 a,
190 "remember"
191 | "relate"
192 | "unrelate"
193 | "feedback"
194 | "remove"
195 | "consolidate"
196 | "import"
197 ) {
198 Some(ContextEventKindV1::KnowledgeRemembered)
199 } else {
200 None
201 }
202 }
203 "ctx_artifacts" => {
204 let a = action.unwrap_or("");
205 if matches!(a, "reindex" | "remove") {
206 Some(ContextEventKindV1::ArtifactStored)
207 } else {
208 None
209 }
210 }
211 "ctx_graph" => {
212 let a = action.unwrap_or("");
213 if matches!(
214 a,
215 "index-build"
216 | "index-build-full"
217 | "index-build-background"
218 | "index-build-full-background"
219 ) {
220 Some(ContextEventKindV1::GraphBuilt)
221 } else {
222 None
223 }
224 }
225 "ctx_proof" | "ctx_verify" => {
226 let a = action.unwrap_or("");
227 if matches!(a, "generate" | "export" | "verify") {
228 Some(ContextEventKindV1::ProofAdded)
229 } else {
230 None
231 }
232 }
233 _ => None,
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn test_runtime_bus_uses_stable_process_sandbox() {
243 let _isolated = crate::core::data_dir::isolated_data_dir();
244 let runtime = runtime();
248 let expected = crate::core::data_dir::test_sandbox_dir()
249 .join("context-os")
250 .join("context-os.db");
251
252 assert_eq!(runtime.bus.db_path(), expected);
253 }
254}