1use hirn_core::id::MemoryId;
14use hirn_core::revision::{LogicalMemoryId, RevisionId};
15use hirn_core::types::{EdgeRelation, Layer};
16
17#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
26pub enum MemoryEvent {
27 EpisodeCreated {
29 id: MemoryId,
30 content_preview: String,
31 },
32 SemanticCreated { id: MemoryId, concept_name: String },
34 ProceduralCreated {
36 id: MemoryId,
37 procedure_name: String,
38 },
39 MemoryCorrected {
41 logical_memory_id: LogicalMemoryId,
42 old_revision_id: RevisionId,
43 new_revision_id: RevisionId,
44 #[serde(default)]
45 reason: Option<String>,
46 },
47 MemorySuperseded {
49 logical_memory_id: LogicalMemoryId,
50 prior_revision_id: RevisionId,
51 new_revision_id: RevisionId,
52 #[serde(default)]
53 reason: Option<String>,
54 },
55 MemoryOverridden {
57 logical_memory_id: LogicalMemoryId,
58 prior_revision_id: RevisionId,
59 override_revision_id: RevisionId,
60 #[serde(default)]
61 reason: Option<String>,
62 },
63 MemoryMerged {
65 target_logical_memory_id: LogicalMemoryId,
66 prior_target_revision_id: RevisionId,
67 new_target_revision_id: RevisionId,
68 source_logical_memory_ids: Vec<LogicalMemoryId>,
69 source_revision_ids: Vec<RevisionId>,
70 #[serde(default)]
71 reason: Option<String>,
72 },
73 MemoryRetracted {
75 logical_memory_id: LogicalMemoryId,
76 prior_revision_id: RevisionId,
77 tombstone_revision_id: RevisionId,
78 #[serde(default)]
79 reason: Option<String>,
80 },
81 WorkingPushed { id: MemoryId },
83 ImportanceUpdated {
85 id: MemoryId,
86 old_value: f32,
87 new_value: f32,
88 },
89 Reconsolidated { id: MemoryId, reason: String },
91 EdgeCreated {
93 source: MemoryId,
94 target: MemoryId,
95 relation: EdgeRelation,
96 weight: f32,
97 },
98 EdgeWeightUpdated {
100 source: MemoryId,
101 target: MemoryId,
102 relation: EdgeRelation,
103 old_weight: f32,
104 new_weight: f32,
105 },
106 Archived { id: MemoryId },
108 Forgotten { id: MemoryId },
110 Consolidated { records_processed: usize },
112 SnapshotTaken { seq: u64, tag: String },
114 CompactionCompleted {
116 before_seq: u64,
117 events_removed: u64,
118 },
119 AdmissionEvaluated {
121 candidate_id: MemoryId,
122 decision: String,
123 controllers_consulted: Vec<String>,
124 },
125 HypothesisGenerated {
127 id: MemoryId,
128 source_a: MemoryId,
129 source_b: MemoryId,
130 batch_id: String,
131 },
132 HypothesisValidated {
134 id: MemoryId,
135 new_confidence: f32,
136 evidence_count: u32,
137 batch_id: String,
138 },
139 HypothesisDiscarded {
141 id: MemoryId,
142 reason: String,
143 batch_id: String,
144 },
145 AccessGranted {
147 action: String,
148 realm: String,
149 namespace: String,
150 policy_ids: Vec<String>,
151 },
152 AccessDenied {
154 action: String,
155 realm: String,
156 namespace: String,
157 reasons: Vec<String>,
158 policy_ids: Vec<String>,
159 },
160 PolicyChanged {
162 policy_name: String,
163 change_type: String,
164 #[serde(default)]
165 policy_content: String,
166 },
167 MemoryRecalled {
169 query_preview: String,
170 results_count: usize,
171 },
172 ContradictionDetected {
174 memory_a: MemoryId,
175 memory_b: MemoryId,
176 confidence: f32,
177 },
178 CausalEdgeDiscovered {
180 cause: MemoryId,
181 effect: MemoryId,
182 strength: f32,
183 },
184 Error { operation: String, message: String },
186 #[serde(other)]
188 Unknown,
189}
190
191impl MemoryEvent {
192 pub fn event_type(&self) -> &'static str {
194 match self {
195 Self::EpisodeCreated { .. } => "episode_created",
196 Self::SemanticCreated { .. } => "semantic_created",
197 Self::ProceduralCreated { .. } => "procedural_created",
198 Self::MemoryCorrected { .. } => "memory_corrected",
199 Self::MemorySuperseded { .. } => "memory_superseded",
200 Self::MemoryOverridden { .. } => "memory_overridden",
201 Self::MemoryMerged { .. } => "memory_merged",
202 Self::MemoryRetracted { .. } => "memory_retracted",
203 Self::WorkingPushed { .. } => "working_pushed",
204 Self::ImportanceUpdated { .. } => "importance_updated",
205 Self::Reconsolidated { .. } => "reconsolidated",
206 Self::EdgeCreated { .. } => "edge_created",
207 Self::EdgeWeightUpdated { .. } => "edge_weight_updated",
208 Self::Archived { .. } => "archived",
209 Self::Forgotten { .. } => "forgotten",
210 Self::Consolidated { .. } => "consolidated",
211 Self::SnapshotTaken { .. } => "snapshot_taken",
212 Self::CompactionCompleted { .. } => "compaction_completed",
213 Self::AdmissionEvaluated { .. } => "admission_evaluated",
214 Self::HypothesisGenerated { .. } => "hypothesis_generated",
215 Self::HypothesisValidated { .. } => "hypothesis_validated",
216 Self::HypothesisDiscarded { .. } => "hypothesis_discarded",
217 Self::AccessGranted { .. } => "access_granted",
218 Self::AccessDenied { .. } => "access_denied",
219 Self::PolicyChanged { .. } => "policy_changed",
220 Self::MemoryRecalled { .. } => "memory_recalled",
221 Self::ContradictionDetected { .. } => "contradiction_detected",
222 Self::CausalEdgeDiscovered { .. } => "causal_edge_discovered",
223 Self::Error { .. } => "error",
224 Self::Unknown => "unknown",
225 }
226 }
227
228 pub fn should_persist(&self) -> bool {
230 !matches!(self, Self::MemoryRecalled { .. })
231 }
232
233 pub fn layer(&self) -> Option<Layer> {
235 match self {
236 Self::EpisodeCreated { .. } => Some(Layer::Episodic),
237 Self::SemanticCreated { .. } => Some(Layer::Semantic),
238 Self::ProceduralCreated { .. } => Some(Layer::Procedural),
239 Self::MemoryCorrected { .. } => Some(Layer::Semantic),
240 Self::MemorySuperseded { .. } => Some(Layer::Semantic),
241 Self::MemoryOverridden { .. } => Some(Layer::Semantic),
242 Self::MemoryMerged { .. } => Some(Layer::Semantic),
243 Self::MemoryRetracted { .. } => Some(Layer::Semantic),
244 Self::WorkingPushed { .. } => Some(Layer::Working),
245 _ => None,
246 }
247 }
248}
249
250#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
261pub struct EventEnvelope {
262 pub seq: u64,
264 pub timestamp_us: i64,
266 pub realm: String,
268 pub namespace: String,
270 pub agent_id: String,
272 pub event: MemoryEvent,
274 #[serde(default)]
277 pub hmac: Option<String>,
278}
279
280impl EventEnvelope {
281 pub fn new(
283 seq: u64,
284 realm: impl Into<String>,
285 namespace: impl Into<String>,
286 agent_id: impl Into<String>,
287 event: MemoryEvent,
288 ) -> Self {
289 let now = chrono::Utc::now();
290 Self {
291 seq,
292 timestamp_us: now.timestamp_micros(),
293 realm: realm.into(),
294 namespace: namespace.into(),
295 agent_id: agent_id.into(),
296 event,
297 hmac: None,
298 }
299 }
300
301 pub fn sign(&mut self, secret: &[u8]) {
308 let bytes = self.signable_bytes();
309 let tag = Self::compute_hmac(secret, &bytes);
310 self.hmac = Some(tag);
311 }
312
313 pub fn verify_hmac(&self, secret: &[u8]) -> bool {
317 let Some(ref stored_hmac) = self.hmac else {
318 return false;
319 };
320 let bytes = self.signable_bytes();
321 let expected = Self::compute_hmac(secret, &bytes);
322 constant_time_eq(stored_hmac.as_bytes(), expected.as_bytes())
324 }
325
326 fn signable_bytes(&self) -> Vec<u8> {
328 let mut buf = Vec::with_capacity(256);
329 buf.extend_from_slice(&self.seq.to_le_bytes());
330 buf.extend_from_slice(&self.timestamp_us.to_le_bytes());
331 buf.extend_from_slice(self.realm.as_bytes());
332 buf.push(0); buf.extend_from_slice(self.namespace.as_bytes());
334 buf.push(0);
335 buf.extend_from_slice(self.agent_id.as_bytes());
336 buf.push(0);
337 if let Ok(payload) = bincode::serialize(&self.event) {
338 buf.extend_from_slice(&payload);
339 }
340 buf
341 }
342
343 fn compute_hmac(secret: &[u8], data: &[u8]) -> String {
346 let key = blake3::derive_key("hirn event hmac v1", secret);
348 let hash = blake3::keyed_hash(&key, data);
349 hash.to_hex().to_string()
350 }
351
352 pub fn event_type(&self) -> &'static str {
354 self.event.event_type()
355 }
356
357 pub fn bincode_size(&self) -> usize {
359 bincode::serialized_size(self).unwrap_or(0) as usize
360 }
361}
362
363fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
365 if a.len() != b.len() {
366 return false;
367 }
368 let mut diff = 0u8;
369 for (x, y) in a.iter().zip(b.iter()) {
370 diff |= x ^ y;
371 }
372 diff == 0
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 fn sample_id() -> MemoryId {
380 MemoryId::new()
381 }
382
383 #[test]
386 fn bincode_round_trip_all_variants() {
387 let variants = vec![
388 MemoryEvent::EpisodeCreated {
389 id: sample_id(),
390 content_preview: "hello world".into(),
391 },
392 MemoryEvent::SemanticCreated {
393 id: sample_id(),
394 concept_name: "Rust".into(),
395 },
396 MemoryEvent::ProceduralCreated {
397 id: sample_id(),
398 procedure_name: "deploy-to-staging".into(),
399 },
400 MemoryEvent::WorkingPushed { id: sample_id() },
401 MemoryEvent::ImportanceUpdated {
402 id: sample_id(),
403 old_value: 0.3,
404 new_value: 0.7,
405 },
406 MemoryEvent::Reconsolidated {
407 id: sample_id(),
408 reason: "new evidence".into(),
409 },
410 MemoryEvent::EdgeCreated {
411 source: sample_id(),
412 target: sample_id(),
413 relation: EdgeRelation::Causes,
414 weight: 0.8,
415 },
416 MemoryEvent::EdgeWeightUpdated {
417 source: sample_id(),
418 target: sample_id(),
419 relation: EdgeRelation::SimilarTo,
420 old_weight: 0.5,
421 new_weight: 0.9,
422 },
423 MemoryEvent::Archived { id: sample_id() },
424 MemoryEvent::Forgotten { id: sample_id() },
425 MemoryEvent::Consolidated {
426 records_processed: 42,
427 },
428 MemoryEvent::SnapshotTaken {
429 seq: 100,
430 tag: "snapshot-100".into(),
431 },
432 MemoryEvent::CompactionCompleted {
433 before_seq: 50,
434 events_removed: 50,
435 },
436 MemoryEvent::MemoryRecalled {
437 query_preview: "test query".into(),
438 results_count: 5,
439 },
440 MemoryEvent::ContradictionDetected {
441 memory_a: sample_id(),
442 memory_b: sample_id(),
443 confidence: 0.92,
444 },
445 MemoryEvent::CausalEdgeDiscovered {
446 cause: sample_id(),
447 effect: sample_id(),
448 strength: 0.75,
449 },
450 MemoryEvent::Error {
451 operation: "remember".into(),
452 message: "embedding failed".into(),
453 },
454 ];
455
456 for event in &variants {
457 let bytes = bincode::serialize(event).expect("serialize");
458 let decoded: MemoryEvent = bincode::deserialize(&bytes).expect("deserialize");
459 assert_eq!(event.event_type(), decoded.event_type());
460 }
461 }
462
463 #[test]
464 fn json_round_trip_all_variants() {
465 let variants = vec![
466 MemoryEvent::EpisodeCreated {
467 id: sample_id(),
468 content_preview: "test".into(),
469 },
470 MemoryEvent::SemanticCreated {
471 id: sample_id(),
472 concept_name: "concept".into(),
473 },
474 MemoryEvent::ProceduralCreated {
475 id: sample_id(),
476 procedure_name: "deploy-to-staging".into(),
477 },
478 MemoryEvent::WorkingPushed { id: sample_id() },
479 MemoryEvent::ImportanceUpdated {
480 id: sample_id(),
481 old_value: 0.1,
482 new_value: 0.9,
483 },
484 MemoryEvent::Reconsolidated {
485 id: sample_id(),
486 reason: "updated".into(),
487 },
488 MemoryEvent::EdgeCreated {
489 source: sample_id(),
490 target: sample_id(),
491 relation: EdgeRelation::DerivedFrom,
492 weight: 0.5,
493 },
494 MemoryEvent::EdgeWeightUpdated {
495 source: sample_id(),
496 target: sample_id(),
497 relation: EdgeRelation::Contradicts,
498 old_weight: 0.2,
499 new_weight: 0.8,
500 },
501 MemoryEvent::Archived { id: sample_id() },
502 MemoryEvent::Forgotten { id: sample_id() },
503 MemoryEvent::Consolidated {
504 records_processed: 10,
505 },
506 MemoryEvent::SnapshotTaken {
507 seq: 200,
508 tag: "snap-200".into(),
509 },
510 MemoryEvent::CompactionCompleted {
511 before_seq: 100,
512 events_removed: 100,
513 },
514 MemoryEvent::MemoryRecalled {
515 query_preview: "recall test".into(),
516 results_count: 3,
517 },
518 MemoryEvent::ContradictionDetected {
519 memory_a: sample_id(),
520 memory_b: sample_id(),
521 confidence: 0.85,
522 },
523 MemoryEvent::CausalEdgeDiscovered {
524 cause: sample_id(),
525 effect: sample_id(),
526 strength: 0.6,
527 },
528 MemoryEvent::Error {
529 operation: "consolidation".into(),
530 message: "timeout".into(),
531 },
532 ];
533
534 for event in &variants {
535 let json = serde_json::to_string(event).expect("to json");
536 let decoded: MemoryEvent = serde_json::from_str(&json).expect("from json");
537 assert_eq!(event.event_type(), decoded.event_type());
538 }
539 }
540
541 #[test]
542 fn envelope_seq_monotonic() {
543 let envelopes: Vec<EventEnvelope> = (0..100)
544 .map(|seq| {
545 EventEnvelope::new(
546 seq,
547 "default",
548 "shared",
549 "agent-1",
550 MemoryEvent::WorkingPushed { id: sample_id() },
551 )
552 })
553 .collect();
554
555 for pair in envelopes.windows(2) {
556 assert!(
557 pair[1].seq > pair[0].seq,
558 "seq must be monotonically increasing"
559 );
560 }
561 }
562
563 #[test]
564 fn unknown_variant_forward_compatibility() {
565 let event = MemoryEvent::Unknown;
568 let bytes = bincode::serialize(&event).expect("serialize");
569 let decoded: MemoryEvent = bincode::deserialize(&bytes).expect("deserialize");
570 assert_eq!(decoded.event_type(), "unknown");
571
572 let json = serde_json::to_string(&event).expect("to json");
574 let decoded: MemoryEvent = serde_json::from_str(&json).expect("from json");
575 assert_eq!(decoded.event_type(), "unknown");
576 }
577
578 #[test]
579 fn envelope_bincode_round_trip() {
580 let envelope = EventEnvelope::new(
581 42,
582 "prod",
583 "default",
584 "agent-x",
585 MemoryEvent::EpisodeCreated {
586 id: sample_id(),
587 content_preview: "test episode".into(),
588 },
589 );
590
591 let bytes = bincode::serialize(&envelope).expect("serialize");
592 let decoded: EventEnvelope = bincode::deserialize(&bytes).expect("deserialize");
593 assert_eq!(decoded.seq, 42);
594 assert_eq!(decoded.realm, "prod");
595 assert_eq!(decoded.namespace, "default");
596 assert_eq!(decoded.agent_id, "agent-x");
597 assert_eq!(decoded.event.event_type(), "episode_created");
598 }
599
600 #[test]
601 fn envelope_json_round_trip() {
602 let envelope = EventEnvelope::new(
603 7,
604 "staging",
605 "team-a",
606 "agent-y",
607 MemoryEvent::Consolidated {
608 records_processed: 99,
609 },
610 );
611
612 let json = serde_json::to_string(&envelope).expect("to json");
613 let decoded: EventEnvelope = serde_json::from_str(&json).expect("from json");
614 assert_eq!(decoded.seq, 7);
615 assert_eq!(decoded.realm, "staging");
616 }
617
618 #[test]
619 fn typical_episode_created_envelope_under_2kb() {
620 let envelope = EventEnvelope::new(
621 1,
622 "default",
623 "shared",
624 "test-agent",
625 MemoryEvent::EpisodeCreated {
626 id: sample_id(),
627 content_preview: "A moderately long preview of an episodic memory entry that contains enough text to be representative of real-world usage".into(),
628 },
629 );
630
631 let size = envelope.bincode_size();
632 assert!(
633 size < 2048,
634 "EpisodeCreated envelope should be < 2KB, got {size}"
635 );
636 }
637
638 #[test]
641 fn access_granted_event_serde() {
642 let event = MemoryEvent::AccessGranted {
643 action: "remember".into(),
644 realm: "production".into(),
645 namespace: "shared".into(),
646 policy_ids: vec!["policy0".into()],
647 };
648 assert_eq!(event.event_type(), "access_granted");
649
650 let bytes = bincode::serialize(&event).unwrap();
651 let decoded: MemoryEvent = bincode::deserialize(&bytes).unwrap();
652 assert_eq!(decoded.event_type(), "access_granted");
653
654 let json = serde_json::to_string(&event).unwrap();
655 let decoded: MemoryEvent = serde_json::from_str(&json).unwrap();
656 assert_eq!(decoded.event_type(), "access_granted");
657 }
658
659 #[test]
660 fn access_denied_event_serde() {
661 let event = MemoryEvent::AccessDenied {
662 action: "consolidate".into(),
663 realm: "production".into(),
664 namespace: "restricted".into(),
665 reasons: vec!["denied: agent cannot consolidate".into()],
666 policy_ids: vec!["forbid0".into()],
667 };
668 assert_eq!(event.event_type(), "access_denied");
669
670 let json = serde_json::to_string(&event).unwrap();
671 let decoded: MemoryEvent = serde_json::from_str(&json).unwrap();
672 if let MemoryEvent::AccessDenied { reasons, .. } = decoded {
673 assert_eq!(reasons.len(), 1);
674 assert!(reasons[0].contains("cannot consolidate"));
675 } else {
676 panic!("expected AccessDenied");
677 }
678 }
679
680 #[test]
681 fn policy_changed_event_serde() {
682 let event = MemoryEvent::PolicyChanged {
683 policy_name: "acl.cedar".into(),
684 change_type: "added".into(),
685 policy_content: "permit(principal, action, resource);".into(),
686 };
687 assert_eq!(event.event_type(), "policy_changed");
688
689 let bytes = bincode::serialize(&event).unwrap();
690 let decoded: MemoryEvent = bincode::deserialize(&bytes).unwrap();
691 assert_eq!(decoded.event_type(), "policy_changed");
692 }
693
694 #[test]
697 fn hmac_sign_and_verify() {
698 let secret = b"realm-secret-key-for-testing";
699 let mut envelope = EventEnvelope::new(
700 1,
701 "production",
702 "shared",
703 "agent-007",
704 MemoryEvent::EpisodeCreated {
705 id: sample_id(),
706 content_preview: "classified intel".into(),
707 },
708 );
709
710 assert!(envelope.hmac.is_none());
711
712 envelope.sign(secret);
713 assert!(envelope.hmac.is_some());
714 assert!(envelope.verify_hmac(secret));
715 }
716
717 #[test]
718 fn hmac_detects_tampered_payload() {
719 let secret = b"realm-secret";
720 let mut envelope = EventEnvelope::new(
721 1,
722 "prod",
723 "shared",
724 "agent-007",
725 MemoryEvent::EpisodeCreated {
726 id: sample_id(),
727 content_preview: "original content".into(),
728 },
729 );
730
731 envelope.sign(secret);
732 assert!(envelope.verify_hmac(secret));
733
734 envelope.event = MemoryEvent::EpisodeCreated {
736 id: sample_id(),
737 content_preview: "TAMPERED content".into(),
738 };
739
740 assert!(
741 !envelope.verify_hmac(secret),
742 "tampered payload should fail HMAC"
743 );
744 }
745
746 #[test]
747 fn hmac_detects_tampered_metadata() {
748 let secret = b"realm-secret";
749 let mut envelope = EventEnvelope::new(
750 1,
751 "production",
752 "shared",
753 "agent-007",
754 MemoryEvent::Consolidated {
755 records_processed: 10,
756 },
757 );
758
759 envelope.sign(secret);
760 assert!(envelope.verify_hmac(secret));
761
762 envelope.agent_id = "impersonator".into();
764 assert!(
765 !envelope.verify_hmac(secret),
766 "tampered agent_id should fail HMAC"
767 );
768 }
769
770 #[test]
771 fn hmac_wrong_secret_fails() {
772 let secret = b"correct-secret";
773 let wrong = b"wrong-secret";
774 let mut envelope = EventEnvelope::new(
775 1,
776 "prod",
777 "shared",
778 "agent",
779 MemoryEvent::Forgotten { id: sample_id() },
780 );
781
782 envelope.sign(secret);
783 assert!(envelope.verify_hmac(secret));
784 assert!(!envelope.verify_hmac(wrong), "wrong secret should fail");
785 }
786
787 #[test]
788 fn hmac_missing_returns_false() {
789 let envelope = EventEnvelope::new(
790 1,
791 "prod",
792 "shared",
793 "agent",
794 MemoryEvent::WorkingPushed { id: sample_id() },
795 );
796
797 assert!(
798 !envelope.verify_hmac(b"any-secret"),
799 "missing HMAC should return false"
800 );
801 }
802
803 #[test]
804 fn hmac_on_authorization_events() {
805 let secret = b"audit-secret";
806
807 let mut granted = EventEnvelope::new(
808 10,
809 "production",
810 "shared",
811 "agent-007",
812 MemoryEvent::AccessGranted {
813 action: "recall".into(),
814 realm: "production".into(),
815 namespace: "shared".into(),
816 policy_ids: vec!["policy0".into()],
817 },
818 );
819 granted.sign(secret);
820 assert!(granted.verify_hmac(secret));
821
822 let mut denied = EventEnvelope::new(
823 11,
824 "production",
825 "restricted",
826 "intern-bot",
827 MemoryEvent::AccessDenied {
828 action: "remember".into(),
829 realm: "production".into(),
830 namespace: "restricted".into(),
831 reasons: vec!["denied by policy".into()],
832 policy_ids: vec!["forbid0".into()],
833 },
834 );
835 denied.sign(secret);
836 assert!(denied.verify_hmac(secret));
837 }
838
839 #[test]
840 fn hmac_envelope_serde_preserves_tag() {
841 let secret = b"serde-test-secret";
842 let mut envelope = EventEnvelope::new(
843 42,
844 "realm-a",
845 "ns-1",
846 "agent-x",
847 MemoryEvent::Consolidated {
848 records_processed: 5,
849 },
850 );
851 envelope.sign(secret);
852
853 let json = serde_json::to_string(&envelope).unwrap();
855 let decoded: EventEnvelope = serde_json::from_str(&json).unwrap();
856 assert!(decoded.verify_hmac(secret));
857
858 let bytes = bincode::serialize(&envelope).unwrap();
860 let decoded: EventEnvelope = bincode::deserialize(&bytes).unwrap();
861 assert!(decoded.verify_hmac(secret));
862 }
863}