1use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
21use std::sync::{Mutex, OnceLock};
22
23use serde::{Deserialize, Serialize};
24
25use crate::core::context_kernel::bounded::BoundedQueue;
26
27#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
31#[serde(tag = "type")]
32pub enum OclaEvent {
33 RequestCompleted {
35 model: String,
36 input_tokens: u64,
37 output_tokens: u64,
38 duration_ms: u64,
39 session_id: Option<String>,
40 },
41 FeedbackRecorded {
43 session_id: String,
44 outcome: FeedbackOutcome,
45 tool: Option<String>,
46 },
47 ThresholdShift {
49 language: String,
50 old_value: f64,
51 new_value: f64,
52 metric: ThresholdMetric,
53 },
54 CompressionApplied {
56 path: Option<String>,
57 before_tokens: u64,
58 after_tokens: u64,
59 strategy: String,
60 },
61 SavingsRecorded {
63 input_saved: u64,
64 output_saved: u64,
65 source: SavingsSource,
66 attribution_id: Option<String>,
67 evidence_class: Option<String>,
68 measurement_method: Option<String>,
69 },
70 IntentClassified {
72 tier: String,
73 confidence: f64,
74 reasoning: String,
75 },
76 OutcomeRecorded {
78 session_id: String,
79 accepted: bool,
80 implicit: bool,
81 },
82 ResponseOptimized {
84 cache_hit: bool,
85 is_duplicate: bool,
86 tokens_saved: u64,
87 },
88 ModelRouted {
90 requested_model: String,
91 routed_model: String,
92 tier: String,
93 model_changed: bool,
94 },
95 AgentChainEvent {
97 agent_id: String,
98 action: String,
99 parent_agent: Option<String>,
100 },
101}
102
103#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
105#[serde(rename_all = "snake_case")]
106pub enum FeedbackOutcome {
107 Accept,
108 Reject,
109 Partial,
110}
111
112#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
114#[serde(rename_all = "snake_case")]
115pub enum ThresholdMetric {
116 Entropy,
117 Jaccard,
118}
119
120#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
122#[serde(rename_all = "snake_case")]
123pub enum SavingsSource {
124 Compression,
125 Cache,
126 Routing,
127 Verbosity,
128 ResponseCache,
129}
130
131#[derive(Clone, Debug, Serialize, Deserialize)]
135pub struct OclaBusRecord {
136 pub id: u64,
137 pub timestamp_ms: u64,
138 pub event: OclaEvent,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
143#[serde(rename_all = "snake_case")]
144pub enum OverflowPolicy {
145 DropOldest,
147 DropNewest,
149 Backpressure,
151}
152
153impl OverflowPolicy {
154 fn from_env() -> Self {
155 match std::env::var("LEANCTX_BUS_OVERFLOW").as_deref() {
156 Ok("drop_newest") => Self::DropNewest,
157 Ok("backpressure") => Self::Backpressure,
158 Ok("drop_oldest") | Err(_) => Self::DropOldest,
159 Ok(value) => {
160 tracing::warn!(
161 value,
162 "invalid LEANCTX_BUS_OVERFLOW value; using drop_oldest"
163 );
164 Self::DropOldest
165 }
166 }
167 }
168}
169
170#[derive(Debug, Clone)]
172pub struct OverflowEvent {
173 pub policy: OverflowPolicy,
175 pub dropped_count: usize,
177 pub queue_capacity: usize,
179}
180
181pub struct OclaBus {
188 enabled: AtomicBool,
189 ring: Mutex<BoundedQueue<OclaBusRecord>>,
190 capacity: usize,
191 next_id: AtomicU64,
192 overflow_policy: OverflowPolicy,
193 overflow_count: AtomicUsize,
194}
195
196impl OclaBus {
197 fn new(capacity: usize) -> Self {
199 Self::new_with_policy(capacity, OverflowPolicy::from_env())
200 }
201
202 fn new_with_policy(capacity: usize, overflow_policy: OverflowPolicy) -> Self {
203 Self {
204 enabled: AtomicBool::new(false),
205 ring: Mutex::new(BoundedQueue::new(capacity)),
206 capacity,
207 next_id: AtomicU64::new(1),
208 overflow_policy,
209 overflow_count: AtomicUsize::new(0),
210 }
211 }
212
213 pub fn scoped(capacity: usize) -> Self {
215 let bus = Self::new_with_policy(capacity, OverflowPolicy::DropOldest);
216 bus.enabled.store(true, Ordering::Relaxed);
217 bus
218 }
219
220 #[cfg(test)]
221 fn scoped_with_policy(capacity: usize, overflow_policy: OverflowPolicy) -> Self {
222 let bus = Self::new_with_policy(capacity, overflow_policy);
223 bus.enabled.store(true, Ordering::Relaxed);
224 bus
225 }
226
227 pub fn enable(&self) {
229 self.enabled.store(true, Ordering::Release);
230 }
231
232 pub fn disable(&self) {
234 self.enabled.store(false, Ordering::Release);
235 }
236
237 #[inline]
239 pub fn is_enabled(&self) -> bool {
240 self.enabled.load(Ordering::Acquire)
241 }
242
243 #[inline]
245 pub fn emit_if_enabled(&self, event: OclaEvent) -> u64 {
246 if !self.is_enabled() {
247 return 0;
248 }
249 self.emit_unconditional(event)
250 }
251
252 fn emit_unconditional(&self, event: OclaEvent) -> u64 {
255 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
256 let record = OclaBusRecord {
257 id,
258 timestamp_ms: current_timestamp_ms(),
259 event,
260 };
261
262 let mut ring = self
263 .ring
264 .lock()
265 .unwrap_or_else(std::sync::PoisonError::into_inner);
266 let _dropped = if ring.is_full() {
267 let dropped_count = self.overflow_count.fetch_add(1, Ordering::Relaxed) + 1;
268 let overflow = OverflowEvent {
269 policy: self.overflow_policy,
270 dropped_count,
271 queue_capacity: self.capacity,
272 };
273
274 match self.overflow_policy {
275 OverflowPolicy::DropOldest => ring.push(record),
276 OverflowPolicy::DropNewest => Some(record),
277 OverflowPolicy::Backpressure => {
278 tracing::warn!(
279 dropped_count = overflow.dropped_count,
280 queue_capacity = overflow.queue_capacity,
281 "OCLA bus backpressure requested; dropping newest event"
282 );
283 Some(record)
284 }
285 }
286 } else {
287 ring.push(record)
288 };
289
290 id
291 }
292
293 pub fn drain(&self) -> Vec<OclaBusRecord> {
295 let mut ring = self
296 .ring
297 .lock()
298 .unwrap_or_else(std::sync::PoisonError::into_inner);
299 let count = ring.len();
300 ring.drain_oldest(count)
301 }
302
303 pub fn events_since(&self, after_id: u64) -> Vec<OclaBusRecord> {
305 let ring = self
306 .ring
307 .lock()
308 .unwrap_or_else(std::sync::PoisonError::into_inner);
309 ring.iter().filter(|r| r.id > after_id).cloned().collect()
310 }
311
312 pub fn latest(&self, n: usize) -> Vec<OclaBusRecord> {
314 let ring = self
315 .ring
316 .lock()
317 .unwrap_or_else(std::sync::PoisonError::into_inner);
318 let start = ring.len().saturating_sub(n);
319 ring.iter().skip(start).cloned().collect()
320 }
321
322 pub fn len(&self) -> usize {
324 self.ring
325 .lock()
326 .unwrap_or_else(std::sync::PoisonError::into_inner)
327 .len()
328 }
329
330 pub fn is_empty(&self) -> bool {
332 self.len() == 0
333 }
334
335 pub fn total_emitted(&self) -> u64 {
337 self.next_id.load(Ordering::Relaxed) - 1
338 }
339
340 pub fn overflow_count(&self) -> usize {
342 self.overflow_count.load(Ordering::Relaxed)
343 }
344
345 pub fn overflow_policy(&self) -> OverflowPolicy {
347 self.overflow_policy
348 }
349}
350
351impl std::fmt::Debug for OclaBus {
352 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353 let ring_len = self.ring.lock().map_or(0, |r| r.len());
354 f.debug_struct("OclaBus")
355 .field("enabled", &self.is_enabled())
356 .field("ring", &format_args!("[{ring_len} events]"))
357 .field("capacity", &self.capacity)
358 .field("next_id", &self.next_id.load(Ordering::Relaxed))
359 .field("overflow_policy", &self.overflow_policy)
360 .field("overflow_count", &self.overflow_count())
361 .finish()
362 }
363}
364
365const DEFAULT_CAPACITY: usize = 1000;
368
369fn global_bus() -> &'static OclaBus {
370 static INSTANCE: OnceLock<OclaBus> = OnceLock::new();
371 INSTANCE.get_or_init(|| OclaBus::new(DEFAULT_CAPACITY))
372}
373
374#[inline]
376pub fn emit(event: OclaEvent) -> u64 {
377 global_bus().emit_if_enabled(event)
378}
379
380pub fn enable() {
382 global_bus().enable();
383}
384
385pub fn disable() {
387 global_bus().disable();
388}
389
390#[inline]
392pub fn is_enabled() -> bool {
393 global_bus().is_enabled()
394}
395
396pub fn events_since(after_id: u64) -> Vec<OclaBusRecord> {
398 global_bus().events_since(after_id)
399}
400
401pub fn latest(n: usize) -> Vec<OclaBusRecord> {
403 global_bus().latest(n)
404}
405
406pub fn total_emitted() -> u64 {
408 global_bus().total_emitted()
409}
410
411pub fn overflow_count() -> usize {
413 global_bus().overflow_count()
414}
415
416pub fn overflow_policy() -> OverflowPolicy {
418 global_bus().overflow_policy()
419}
420
421pub fn emit_and_bridge(event: OclaEvent) -> u64 {
427 bridge_to_legacy(&event);
428 emit(event)
429}
430
431fn bridge_to_legacy(event: &OclaEvent) {
433 use super::events::{EventKind, emit as legacy_emit};
434
435 let kind = match event {
436 OclaEvent::CompressionApplied {
437 path,
438 before_tokens,
439 after_tokens,
440 strategy,
441 } => EventKind::Compression {
442 path: path.clone().unwrap_or_default(),
443 before_lines: *before_tokens as u32,
444 after_lines: *after_tokens as u32,
445 strategy: strategy.clone(),
446 kept_line_count: *after_tokens as u32,
447 removed_line_count: before_tokens.saturating_sub(*after_tokens) as u32,
448 },
449 OclaEvent::ThresholdShift {
450 language,
451 old_value,
452 new_value,
453 metric,
454 } => EventKind::ThresholdShift {
455 language: language.clone(),
456 old_entropy: if *metric == ThresholdMetric::Entropy {
457 *old_value
458 } else {
459 0.0
460 },
461 new_entropy: if *metric == ThresholdMetric::Entropy {
462 *new_value
463 } else {
464 0.0
465 },
466 old_jaccard: if *metric == ThresholdMetric::Jaccard {
467 *old_value
468 } else {
469 0.0
470 },
471 new_jaccard: if *metric == ThresholdMetric::Jaccard {
472 *new_value
473 } else {
474 0.0
475 },
476 },
477 OclaEvent::RequestCompleted { .. }
478 | OclaEvent::FeedbackRecorded { .. }
479 | OclaEvent::SavingsRecorded { .. }
480 | OclaEvent::IntentClassified { .. }
481 | OclaEvent::OutcomeRecorded { .. }
482 | OclaEvent::ResponseOptimized { .. }
483 | OclaEvent::ModelRouted { .. }
484 | OclaEvent::AgentChainEvent { .. } => {
485 return;
486 }
487 };
488
489 legacy_emit(kind);
490}
491
492fn current_timestamp_ms() -> u64 {
495 use std::time::{SystemTime, UNIX_EPOCH};
496 SystemTime::now()
497 .duration_since(UNIX_EPOCH)
498 .unwrap_or_default()
499 .as_millis() as u64
500}
501
502#[cfg(test)]
505mod tests {
506 use super::*;
507
508 fn savings_event(input_saved: u64) -> OclaEvent {
509 OclaEvent::SavingsRecorded {
510 input_saved,
511 output_saved: 0,
512 source: SavingsSource::Compression,
513 attribution_id: None,
514 evidence_class: None,
515 measurement_method: None,
516 }
517 }
518
519 #[test]
520 fn disabled_bus_returns_zero() {
521 let bus = OclaBus::new(16);
522 assert!(!bus.is_enabled());
523 let id = bus.emit_if_enabled(OclaEvent::RequestCompleted {
524 model: "gpt-4o".into(),
525 input_tokens: 100,
526 output_tokens: 50,
527 duration_ms: 200,
528 session_id: None,
529 });
530 assert_eq!(id, 0);
531 assert!(bus.is_empty());
532 }
533
534 #[test]
535 fn enabled_bus_records_events() {
536 let bus = OclaBus::scoped(16);
537 let id = bus.emit_if_enabled(OclaEvent::ModelRouted {
538 requested_model: "gpt-4o".into(),
539 routed_model: "gpt-4o-mini".into(),
540 tier: "fast".into(),
541 model_changed: true,
542 });
543 assert!(id > 0);
544 assert_eq!(bus.len(), 1);
545 }
546
547 #[test]
548 fn scoped_bus_is_isolated() {
549 let bus1 = OclaBus::scoped(8);
550 let bus2 = OclaBus::scoped(8);
551
552 bus1.emit_if_enabled(OclaEvent::ResponseOptimized {
553 cache_hit: true,
554 is_duplicate: false,
555 tokens_saved: 42,
556 });
557
558 assert_eq!(bus1.len(), 1);
559 assert_eq!(bus2.len(), 0, "scoped buses are isolated");
560 }
561
562 #[test]
563 fn test_bounded_queue_replaces_vecdeque() {
564 let bus = OclaBus::scoped(2);
565 bus.emit_if_enabled(savings_event(1));
566 bus.emit_if_enabled(savings_event(2));
567
568 assert_eq!(bus.len(), 2);
569 assert_eq!(bus.drain().len(), 2);
570 assert!(bus.is_empty());
571 }
572
573 #[test]
574 fn test_overflow_drop_oldest() {
575 let bus = OclaBus::scoped_with_policy(3, OverflowPolicy::DropOldest);
576 let id1 = bus.emit_if_enabled(OclaEvent::SavingsRecorded {
577 input_saved: 10,
578 output_saved: 5,
579 source: SavingsSource::Compression,
580 attribution_id: None,
581 evidence_class: None,
582 measurement_method: None,
583 });
584 bus.emit_if_enabled(OclaEvent::SavingsRecorded {
585 input_saved: 20,
586 output_saved: 10,
587 source: SavingsSource::Cache,
588 attribution_id: None,
589 evidence_class: None,
590 measurement_method: None,
591 });
592 bus.emit_if_enabled(OclaEvent::SavingsRecorded {
593 input_saved: 30,
594 output_saved: 15,
595 source: SavingsSource::Routing,
596 attribution_id: None,
597 evidence_class: None,
598 measurement_method: None,
599 });
600 bus.emit_if_enabled(OclaEvent::SavingsRecorded {
602 input_saved: 40,
603 output_saved: 20,
604 source: SavingsSource::Verbosity,
605 attribution_id: None,
606 evidence_class: None,
607 measurement_method: None,
608 });
609
610 assert_eq!(bus.len(), 3);
611 let events = bus.events_since(0);
612 assert!(events.iter().all(|r| r.id > id1), "oldest evicted");
613 }
614
615 #[test]
616 fn test_overflow_drop_newest() {
617 let bus = OclaBus::scoped_with_policy(2, OverflowPolicy::DropNewest);
618 bus.emit_if_enabled(savings_event(1));
619 bus.emit_if_enabled(savings_event(2));
620 let rejected_id = bus.emit_if_enabled(savings_event(3));
621
622 let retained_ids = bus
623 .events_since(0)
624 .into_iter()
625 .map(|record| record.id)
626 .collect::<Vec<_>>();
627 assert_eq!(retained_ids, vec![1, 2]);
628 assert_eq!(rejected_id, 3);
629 }
630
631 #[test]
632 fn test_overflow_metrics() {
633 let bus = OclaBus::scoped_with_policy(1, OverflowPolicy::DropNewest);
634 bus.emit_if_enabled(savings_event(1));
635 bus.emit_if_enabled(savings_event(2));
636 bus.emit_if_enabled(savings_event(3));
637
638 assert_eq!(bus.overflow_count(), 2);
639 assert_eq!(bus.overflow_policy(), OverflowPolicy::DropNewest);
640 }
641
642 #[test]
643 fn test_overflow_policy_from_env() {
644 let _env_lock = crate::core::data_dir::test_env_lock();
645 let previous = std::env::var_os("LEANCTX_BUS_OVERFLOW");
646 crate::test_env::set_var("LEANCTX_BUS_OVERFLOW", "backpressure");
647
648 let bus = OclaBus::new(1);
649 assert_eq!(bus.overflow_policy(), OverflowPolicy::Backpressure);
650
651 if let Some(value) = previous {
652 crate::test_env::set_var("LEANCTX_BUS_OVERFLOW", value);
653 } else {
654 crate::test_env::remove_var("LEANCTX_BUS_OVERFLOW");
655 }
656 }
657
658 #[test]
659 fn drain_consumes_all_events() {
660 let bus = OclaBus::scoped(16);
661 bus.emit_if_enabled(OclaEvent::IntentClassified {
662 tier: "fast".into(),
663 confidence: 0.9,
664 reasoning: "simple query".into(),
665 });
666 bus.emit_if_enabled(OclaEvent::IntentClassified {
667 tier: "premium".into(),
668 confidence: 0.8,
669 reasoning: "complex architecture".into(),
670 });
671
672 let drained = bus.drain();
673 assert_eq!(drained.len(), 2);
674 assert!(bus.is_empty(), "drain consumes events");
675 }
676
677 #[test]
678 fn events_since_filters_by_id() {
679 let bus = OclaBus::scoped(16);
680 let id1 = bus.emit_if_enabled(OclaEvent::FeedbackRecorded {
681 session_id: "s1".into(),
682 outcome: FeedbackOutcome::Accept,
683 tool: Some("ctx_read".into()),
684 });
685 let id2 = bus.emit_if_enabled(OclaEvent::FeedbackRecorded {
686 session_id: "s2".into(),
687 outcome: FeedbackOutcome::Reject,
688 tool: None,
689 });
690
691 let after = bus.events_since(id1);
692 assert_eq!(after.len(), 1);
693 assert_eq!(after[0].id, id2);
694 }
695
696 #[test]
697 fn latest_returns_tail() {
698 let bus = OclaBus::scoped(16);
699 for i in 0..5 {
700 bus.emit_if_enabled(OclaEvent::CompressionApplied {
701 path: Some(format!("file_{i}.rs")),
702 before_tokens: 100,
703 after_tokens: 50,
704 strategy: "treesitter".into(),
705 });
706 }
707
708 let tail = bus.latest(2);
709 assert_eq!(tail.len(), 2);
710 assert_eq!(tail[0].id, 4);
711 assert_eq!(tail[1].id, 5);
712 }
713
714 #[test]
715 fn enable_disable_toggle() {
716 let bus = OclaBus::new(16);
717 assert!(!bus.is_enabled());
718
719 bus.enable();
720 assert!(bus.is_enabled());
721 let id = bus.emit_if_enabled(OclaEvent::AgentChainEvent {
722 agent_id: "a1".into(),
723 action: "start".into(),
724 parent_agent: None,
725 });
726 assert!(id > 0);
727
728 bus.disable();
729 let id2 = bus.emit_if_enabled(OclaEvent::AgentChainEvent {
730 agent_id: "a2".into(),
731 action: "stop".into(),
732 parent_agent: Some("a1".into()),
733 });
734 assert_eq!(id2, 0);
735 assert_eq!(bus.len(), 1, "disabled emit is a no-op");
736 }
737
738 #[test]
739 fn total_emitted_counts_all() {
740 let bus = OclaBus::scoped(3);
741 bus.emit_if_enabled(OclaEvent::OutcomeRecorded {
742 session_id: "s1".into(),
743 accepted: true,
744 implicit: false,
745 });
746 bus.emit_if_enabled(OclaEvent::OutcomeRecorded {
747 session_id: "s2".into(),
748 accepted: false,
749 implicit: true,
750 });
751 bus.emit_if_enabled(OclaEvent::OutcomeRecorded {
752 session_id: "s3".into(),
753 accepted: true,
754 implicit: true,
755 });
756 bus.emit_if_enabled(OclaEvent::OutcomeRecorded {
758 session_id: "s4".into(),
759 accepted: true,
760 implicit: false,
761 });
762
763 assert_eq!(bus.total_emitted(), 4, "counts all, even evicted");
764 assert_eq!(bus.len(), 3, "ring only holds capacity");
765 }
766
767 #[test]
768 fn event_serialization_roundtrip() {
769 let event = OclaEvent::ModelRouted {
770 requested_model: "claude-sonnet-4-20250514".into(),
771 routed_model: "claude-haiku-3".into(),
772 tier: "fast".into(),
773 model_changed: true,
774 };
775 let json = serde_json::to_string(&event).unwrap();
776 let deserialized: OclaEvent = serde_json::from_str(&json).unwrap();
777 assert_eq!(event, deserialized);
778 }
779
780 #[test]
781 fn all_10_event_types_serialize() {
782 let events = vec![
783 OclaEvent::RequestCompleted {
784 model: "m".into(),
785 input_tokens: 1,
786 output_tokens: 1,
787 duration_ms: 1,
788 session_id: None,
789 },
790 OclaEvent::FeedbackRecorded {
791 session_id: "s".into(),
792 outcome: FeedbackOutcome::Partial,
793 tool: None,
794 },
795 OclaEvent::ThresholdShift {
796 language: "rust".into(),
797 old_value: 0.5,
798 new_value: 0.6,
799 metric: ThresholdMetric::Entropy,
800 },
801 OclaEvent::CompressionApplied {
802 path: None,
803 before_tokens: 100,
804 after_tokens: 50,
805 strategy: "s".into(),
806 },
807 OclaEvent::SavingsRecorded {
808 input_saved: 10,
809 output_saved: 5,
810 source: SavingsSource::ResponseCache,
811 attribution_id: None,
812 evidence_class: None,
813 measurement_method: None,
814 },
815 OclaEvent::IntentClassified {
816 tier: "standard".into(),
817 confidence: 0.7,
818 reasoning: "r".into(),
819 },
820 OclaEvent::OutcomeRecorded {
821 session_id: "s".into(),
822 accepted: true,
823 implicit: false,
824 },
825 OclaEvent::ResponseOptimized {
826 cache_hit: false,
827 is_duplicate: true,
828 tokens_saved: 0,
829 },
830 OclaEvent::ModelRouted {
831 requested_model: "a".into(),
832 routed_model: "b".into(),
833 tier: "premium".into(),
834 model_changed: true,
835 },
836 OclaEvent::AgentChainEvent {
837 agent_id: "x".into(),
838 action: "spawn".into(),
839 parent_agent: Some("y".into()),
840 },
841 ];
842
843 for event in &events {
844 let json = serde_json::to_string(event).unwrap();
845 assert!(!json.is_empty());
846 let _: OclaEvent = serde_json::from_str(&json).unwrap();
847 }
848 assert_eq!(events.len(), 10, "exactly 10 event types");
849 }
850
851 #[test]
852 fn savings_recorded_p5_fields_roundtrip() {
853 let event = OclaEvent::SavingsRecorded {
854 input_saved: 100,
855 output_saved: 25,
856 source: SavingsSource::Routing,
857 attribution_id: Some("attr-42".into()),
858 evidence_class: Some("measured".into()),
859 measurement_method: Some("provider_reconciled".into()),
860 };
861 let json = serde_json::to_string(&event).unwrap();
862 assert!(json.contains("attribution_id"));
863 assert!(json.contains("evidence_class"));
864 assert!(json.contains("measurement_method"));
865 let deserialized: OclaEvent = serde_json::from_str(&json).unwrap();
866 assert_eq!(event, deserialized);
867 }
868}