1use thiserror::Error;
34
35use crate::confidence::Confidence;
36use crate::memory_kind::MemoryKindTag;
37use crate::source_kind::SourceKind;
38
39#[rustfmt::skip]
48const DECAY_TABLE: [u16; 256] = [
49 65535, 65358, 65181, 65005, 64829, 64654, 64479, 64305,
50 64131, 63957, 63784, 63612, 63440, 63268, 63097, 62927,
51 62757, 62587, 62418, 62249, 62081, 61913, 61745, 61578,
52 61412, 61246, 61080, 60915, 60750, 60586, 60422, 60259,
53 60096, 59933, 59771, 59610, 59449, 59288, 59127, 58968,
54 58808, 58649, 58491, 58332, 58175, 58017, 57860, 57704,
55 57548, 57392, 57237, 57082, 56928, 56774, 56621, 56468,
56 56315, 56163, 56011, 55859, 55708, 55558, 55407, 55258,
57 55108, 54959, 54811, 54662, 54515, 54367, 54220, 54074,
58 53927, 53781, 53636, 53491, 53346, 53202, 53058, 52915,
59 52772, 52629, 52487, 52345, 52203, 52062, 51921, 51781,
60 51641, 51501, 51362, 51223, 51085, 50947, 50809, 50671,
61 50534, 50398, 50261, 50126, 49990, 49855, 49720, 49586,
62 49452, 49318, 49184, 49051, 48919, 48787, 48655, 48523,
63 48392, 48261, 48131, 48000, 47871, 47741, 47612, 47483,
64 47355, 47227, 47099, 46972, 46845, 46718, 46592, 46466,
65 46340, 46215, 46090, 45965, 45841, 45717, 45593, 45470,
66 45347, 45225, 45102, 44980, 44859, 44737, 44617, 44496,
67 44376, 44256, 44136, 44017, 43898, 43779, 43660, 43542,
68 43425, 43307, 43190, 43073, 42957, 42841, 42725, 42609,
69 42494, 42379, 42265, 42150, 42036, 41923, 41809, 41696,
70 41584, 41471, 41359, 41247, 41136, 41024, 40914, 40803,
71 40693, 40583, 40473, 40363, 40254, 40145, 40037, 39929,
72 39821, 39713, 39606, 39498, 39392, 39285, 39179, 39073,
73 38967, 38862, 38757, 38652, 38548, 38443, 38339, 38236,
74 38132, 38029, 37926, 37824, 37722, 37620, 37518, 37416,
75 37315, 37214, 37114, 37013, 36913, 36813, 36714, 36615,
76 36516, 36417, 36318, 36220, 36122, 36025, 35927, 35830,
77 35733, 35637, 35540, 35444, 35348, 35253, 35157, 35062,
78 34968, 34873, 34779, 34685, 34591, 34497, 34404, 34311,
79 34218, 34126, 34033, 33941, 33850, 33758, 33667, 33576,
80 33485, 33394, 33304, 33214, 33124, 33035, 32945, 32856,
81];
82
83pub const DAY_MS: u64 = 86_400_000;
90
91pub const NO_DECAY: u64 = 0;
94
95#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
118pub struct HalfLife(u64);
119
120impl HalfLife {
121 pub const ZERO: Self = Self(NO_DECAY);
123
124 #[must_use]
126 pub const fn from_days(days: u64) -> Self {
127 Self(days.saturating_mul(DAY_MS))
128 }
129
130 #[must_use]
133 pub const fn from_millis(millis: u64) -> Self {
134 Self(millis)
135 }
136
137 #[must_use]
139 pub const fn no_decay() -> Self {
140 Self(NO_DECAY)
141 }
142
143 #[must_use]
147 pub const fn as_millis(self) -> u64 {
148 self.0
149 }
150
151 #[must_use]
154 pub const fn is_no_decay(self) -> bool {
155 self.0 == NO_DECAY
156 }
157}
158
159const MAX_EXPONENT: u32 = 16;
162
163const ELAPSED_CAP: u64 = u64::MAX / 256;
165
166#[must_use]
193pub fn decay_factor_u16(elapsed_ms: u64, half_life: HalfLife) -> u16 {
194 let half_life_ms = half_life.as_millis();
195 if half_life_ms == NO_DECAY {
196 return u16::MAX;
197 }
198 let elapsed = elapsed_ms.min(ELAPSED_CAP);
199 let k_q8 = (elapsed.saturating_mul(256)) / half_life_ms;
200 #[allow(clippy::cast_possible_truncation)]
201 let n = (k_q8 >> 8) as u32;
202 if n >= MAX_EXPONENT {
203 return 0;
204 }
205 let i = (k_q8 & 0xFF) as usize;
206 let frac = u32::from(DECAY_TABLE[i]);
207 #[allow(clippy::cast_possible_truncation)]
208 let result = (frac >> n) as u16;
209 result
210}
211
212#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
218pub struct DecayFlags {
219 pub pinned: bool,
221 pub authoritative: bool,
225}
226
227impl DecayFlags {
228 #[must_use]
230 pub const fn suspends_decay(self) -> bool {
231 self.pinned || self.authoritative
232 }
233}
234
235#[derive(Clone, Debug, PartialEq, Eq)]
249pub struct DecayConfig {
250 pub sem_profile: HalfLife,
252 pub sem_observation: HalfLife,
254 pub sem_self_report: HalfLife,
256 pub sem_participant_report: HalfLife,
259 pub sem_document: HalfLife,
261 pub sem_registry: HalfLife,
263 pub sem_policy: HalfLife,
266 pub sem_external_authority: HalfLife,
268 pub sem_agent_instruction: HalfLife,
270 pub sem_librarian_assignment: HalfLife,
272 pub sem_pending_verification: HalfLife,
274 pub epi_observation: HalfLife,
276 pub epi_self_report: HalfLife,
278 pub epi_participant_report: HalfLife,
280 pub pro_any: HalfLife,
283}
284
285impl DecayConfig {
286 #[must_use]
289 pub const fn librarian_defaults() -> Self {
290 Self {
291 sem_profile: HalfLife::from_days(730),
292 sem_observation: HalfLife::from_days(180),
293 sem_self_report: HalfLife::from_days(90),
294 sem_participant_report: HalfLife::from_days(90),
295 sem_document: HalfLife::from_days(365),
296 sem_registry: HalfLife::from_days(90),
297 sem_policy: HalfLife::from_days(730),
298 sem_external_authority: HalfLife::from_days(180),
299 sem_agent_instruction: HalfLife::from_days(730),
300 sem_librarian_assignment: HalfLife::no_decay(),
301 sem_pending_verification: HalfLife::from_days(30),
302 epi_observation: HalfLife::from_days(90),
303 epi_self_report: HalfLife::from_days(30),
304 epi_participant_report: HalfLife::from_days(60),
305 pro_any: HalfLife::no_decay(),
306 }
307 }
308
309 #[must_use]
315 #[allow(clippy::match_same_arms)]
316 pub const fn half_life_for(
317 &self,
318 memory_kind: MemoryKindTag,
319 source_kind: SourceKind,
320 ) -> Option<HalfLife> {
321 match (memory_kind, source_kind) {
322 (MemoryKindTag::Semantic, SourceKind::Profile) => Some(self.sem_profile),
323 (MemoryKindTag::Semantic, SourceKind::Observation) => Some(self.sem_observation),
324 (MemoryKindTag::Semantic, SourceKind::SelfReport) => Some(self.sem_self_report),
325 (MemoryKindTag::Semantic, SourceKind::ParticipantReport) => {
326 Some(self.sem_participant_report)
327 }
328 (MemoryKindTag::Semantic, SourceKind::Document) => Some(self.sem_document),
329 (MemoryKindTag::Semantic, SourceKind::Registry) => Some(self.sem_registry),
330 (MemoryKindTag::Semantic, SourceKind::Policy) => Some(self.sem_policy),
331 (MemoryKindTag::Semantic, SourceKind::ExternalAuthority) => {
332 Some(self.sem_external_authority)
333 }
334 (MemoryKindTag::Semantic, SourceKind::AgentInstruction) => {
335 Some(self.sem_agent_instruction)
336 }
337 (MemoryKindTag::Semantic, SourceKind::LibrarianAssignment) => {
338 Some(self.sem_librarian_assignment)
339 }
340 (MemoryKindTag::Semantic, SourceKind::PendingVerification) => {
341 Some(self.sem_pending_verification)
342 }
343 (MemoryKindTag::Episodic, SourceKind::Observation) => Some(self.epi_observation),
344 (MemoryKindTag::Episodic, SourceKind::SelfReport) => Some(self.epi_self_report),
345 (MemoryKindTag::Episodic, SourceKind::ParticipantReport) => {
346 Some(self.epi_participant_report)
347 }
348 (MemoryKindTag::Procedural, _) => Some(self.pro_any),
349 (MemoryKindTag::Inferential, _) => None,
352 (MemoryKindTag::Episodic, _) => None,
356 }
357 }
358}
359
360impl Default for DecayConfig {
361 fn default() -> Self {
362 Self::librarian_defaults()
363 }
364}
365
366#[derive(Debug, Error)]
373pub enum DecayConfigError {
374 #[error("toml parse error: {0}")]
378 Parse(#[from] toml::de::Error),
379 #[error("{path}: expected table")]
382 ExpectedTable {
383 path: &'static str,
385 },
386 #[error("{path}: expected non-negative integer (days)")]
389 ExpectedNonNegInteger {
390 path: &'static str,
392 },
393 #[error("{path}: value {value} is not a valid half-life (days ≥ 0)")]
396 InvalidDays {
397 path: &'static str,
399 value: i64,
401 },
402}
403
404impl DecayConfig {
405 pub fn from_toml(toml_str: &str) -> Result<Self, DecayConfigError> {
437 let mut cfg = Self::librarian_defaults();
438 cfg.apply_toml(toml_str)?;
439 Ok(cfg)
440 }
441
442 pub fn apply_toml(&mut self, toml_str: &str) -> Result<(), DecayConfigError> {
449 let root: toml::Table = toml_str.parse()?;
450 let Some(decay) = root.get("decay") else {
451 return Ok(());
452 };
453 let toml::Value::Table(decay) = decay else {
454 return Err(DecayConfigError::ExpectedTable { path: "decay" });
455 };
456 if let Some(section) = decay.get("semantic") {
457 let toml::Value::Table(sem) = section else {
458 return Err(DecayConfigError::ExpectedTable {
459 path: "decay.semantic",
460 });
461 };
462 apply_section(
463 sem,
464 "decay.semantic",
465 &mut [
466 ("profile", &mut self.sem_profile),
467 ("observation", &mut self.sem_observation),
468 ("self_report", &mut self.sem_self_report),
469 ("participant_report", &mut self.sem_participant_report),
470 ("document", &mut self.sem_document),
471 ("registry", &mut self.sem_registry),
472 ("policy", &mut self.sem_policy),
473 ("external_authority", &mut self.sem_external_authority),
474 ("agent_instruction", &mut self.sem_agent_instruction),
475 ("librarian_assignment", &mut self.sem_librarian_assignment),
476 ("pending_verification", &mut self.sem_pending_verification),
477 ],
478 )?;
479 }
480 if let Some(section) = decay.get("episodic") {
481 let toml::Value::Table(epi) = section else {
482 return Err(DecayConfigError::ExpectedTable {
483 path: "decay.episodic",
484 });
485 };
486 apply_section(
487 epi,
488 "decay.episodic",
489 &mut [
490 ("observation", &mut self.epi_observation),
491 ("self_report", &mut self.epi_self_report),
492 ("participant_report", &mut self.epi_participant_report),
493 ],
494 )?;
495 }
496 if let Some(section) = decay.get("procedural") {
497 let toml::Value::Table(pro) = section else {
498 return Err(DecayConfigError::ExpectedTable {
499 path: "decay.procedural",
500 });
501 };
502 apply_section(pro, "decay.procedural", &mut [("any", &mut self.pro_any)])?;
503 }
504 Ok(())
505 }
506}
507
508fn apply_section(
512 section: &toml::Table,
513 section_path: &'static str,
514 slots: &mut [(&'static str, &mut HalfLife)],
515) -> Result<(), DecayConfigError> {
516 for (key, slot) in slots {
517 let Some(value) = section.get(*key) else {
518 continue;
519 };
520 let toml::Value::Integer(days) = value else {
521 return Err(DecayConfigError::ExpectedNonNegInteger { path: section_path });
526 };
527 if *days < 0 {
528 return Err(DecayConfigError::InvalidDays {
529 path: section_path,
530 value: *days,
531 });
532 }
533 #[allow(clippy::cast_sign_loss)]
534 let days_u64 = *days as u64;
535 **slot = HalfLife::from_days(days_u64);
536 }
537 Ok(())
538}
539
540#[must_use]
574pub fn effective_confidence(
575 stored: Confidence,
576 elapsed_ms: u64,
577 memory_kind: MemoryKindTag,
578 source_kind: SourceKind,
579 flags: DecayFlags,
580 config: &DecayConfig,
581) -> Confidence {
582 if flags.suspends_decay() {
583 return stored;
584 }
585 let Some(half_life) = config.half_life_for(memory_kind, source_kind) else {
590 return stored;
591 };
592 let factor = decay_factor_u16(elapsed_ms, half_life);
593 let product = u32::from(stored.as_u16()) * u32::from(factor);
594 let scaled = (product + u32::from(u16::MAX) / 2) / u32::from(u16::MAX);
596 #[allow(clippy::cast_possible_truncation)]
597 Confidence::from_u16(scaled as u16)
598}
599
600#[cfg(test)]
605mod tests {
606 use super::*;
607
608 fn c(f: f32) -> Confidence {
609 Confidence::try_from_f32(f).expect("in range")
610 }
611
612 #[test]
615 fn table_first_entry_is_unit_factor() {
616 assert_eq!(DECAY_TABLE[0], u16::MAX);
617 }
618
619 #[test]
620 fn table_is_strictly_monotonically_decreasing() {
621 for i in 1..256 {
622 assert!(
623 DECAY_TABLE[i] < DECAY_TABLE[i - 1],
624 "non-monotonic at index {i}: {} >= {}",
625 DECAY_TABLE[i],
626 DECAY_TABLE[i - 1]
627 );
628 }
629 }
630
631 #[test]
632 fn no_decay_half_life_returns_unit() {
633 assert_eq!(decay_factor_u16(1_000_000, HalfLife::no_decay()), u16::MAX);
634 assert_eq!(decay_factor_u16(u64::MAX, HalfLife::no_decay()), u16::MAX);
635 }
636
637 #[test]
638 fn zero_elapsed_returns_unit() {
639 assert_eq!(decay_factor_u16(0, HalfLife::from_days(180)), u16::MAX);
640 }
641
642 #[test]
643 fn one_half_life_returns_approximately_half() {
644 let factor = decay_factor_u16(180 * DAY_MS, HalfLife::from_days(180));
645 assert!(factor.abs_diff(u16::MAX / 2) <= 1);
646 }
647
648 #[test]
649 fn sixteen_half_lives_saturate_to_zero() {
650 let factor = decay_factor_u16(16 * 180 * DAY_MS, HalfLife::from_days(180));
653 assert_eq!(factor, 0);
654 }
655
656 #[test]
657 fn elapsed_near_u64_max_saturates_to_zero_not_panics() {
658 let factor = decay_factor_u16(u64::MAX, HalfLife::from_millis(1));
663 assert_eq!(factor, 0);
664 let factor = decay_factor_u16(u64::MAX - 1, HalfLife::from_days(180));
666 assert_eq!(factor, 0);
667 }
668
669 #[test]
670 fn half_life_of_one_millisecond_is_the_tightest_divisor() {
671 let one_ms = HalfLife::from_millis(1);
674 assert_eq!(decay_factor_u16(0, one_ms), u16::MAX);
675 assert_eq!(decay_factor_u16(1, one_ms), u16::MAX >> 1);
677 assert_eq!(decay_factor_u16(16, one_ms), 0);
679 }
680
681 #[test]
682 fn decay_is_monotonic_in_elapsed() {
683 let hl = HalfLife::from_days(180);
686 let mut prev = u16::MAX;
687 for days in (0_u64..=1800).step_by(7) {
688 let f = decay_factor_u16(days * DAY_MS, hl);
689 assert!(f <= prev, "non-monotonic at day {days}");
690 prev = f;
691 }
692 }
693
694 #[test]
697 fn pinned_short_circuits_to_stored() {
698 let cfg = DecayConfig::librarian_defaults();
699 let stored = c(0.8);
700 let eff = effective_confidence(
701 stored,
702 10 * 365 * DAY_MS,
703 MemoryKindTag::Semantic,
704 SourceKind::Observation,
705 DecayFlags {
706 pinned: true,
707 authoritative: false,
708 },
709 &cfg,
710 );
711 assert_eq!(eff, stored);
712 }
713
714 #[test]
715 fn authoritative_short_circuits_to_stored() {
716 let cfg = DecayConfig::librarian_defaults();
717 let stored = c(0.8);
718 let eff = effective_confidence(
719 stored,
720 10 * 365 * DAY_MS,
721 MemoryKindTag::Semantic,
722 SourceKind::Observation,
723 DecayFlags {
724 pinned: false,
725 authoritative: true,
726 },
727 &cfg,
728 );
729 assert_eq!(eff, stored);
730 }
731
732 #[test]
733 fn librarian_assignment_never_decays() {
734 let cfg = DecayConfig::librarian_defaults();
735 let stored = c(1.0);
736 let eff = effective_confidence(
737 stored,
738 100 * 365 * DAY_MS,
739 MemoryKindTag::Semantic,
740 SourceKind::LibrarianAssignment,
741 DecayFlags::default(),
742 &cfg,
743 );
744 assert_eq!(eff, stored);
745 }
746
747 #[test]
748 fn procedural_time_decay_is_disabled() {
749 let cfg = DecayConfig::librarian_defaults();
752 let stored = c(0.9);
753 let eff = effective_confidence(
754 stored,
755 10 * 365 * DAY_MS,
756 MemoryKindTag::Procedural,
757 SourceKind::AgentInstruction,
758 DecayFlags::default(),
759 &cfg,
760 );
761 assert_eq!(eff, stored);
762 }
763
764 #[test]
765 fn inferential_is_passthrough_at_this_layer() {
766 let cfg = DecayConfig::librarian_defaults();
771 let stored = c(0.7);
772 let eff = effective_confidence(
773 stored,
774 10 * 365 * DAY_MS,
775 MemoryKindTag::Inferential,
776 SourceKind::Observation,
777 DecayFlags::default(),
778 &cfg,
779 );
780 assert_eq!(eff, stored);
781 }
782
783 #[test]
784 fn one_half_life_halves_stored_confidence() {
785 let cfg = DecayConfig::librarian_defaults();
786 let stored = c(0.8);
787 let eff = effective_confidence(
788 stored,
789 180 * DAY_MS,
790 MemoryKindTag::Semantic,
791 SourceKind::Observation,
792 DecayFlags::default(),
793 &cfg,
794 );
795 let target = i32::from(stored.as_u16()) / 2;
797 let actual = i32::from(eff.as_u16());
798 assert!(
799 (actual - target).abs() <= 1,
800 "expected ≈{target}, got {actual}"
801 );
802 }
803
804 #[test]
805 fn defaults_match_spec_table() {
806 let cfg = DecayConfig::librarian_defaults();
807 assert_eq!(cfg.sem_profile, HalfLife::from_days(730));
810 assert_eq!(cfg.sem_pending_verification, HalfLife::from_days(30));
811 assert_eq!(cfg.sem_librarian_assignment, HalfLife::no_decay());
812 assert_eq!(cfg.epi_self_report, HalfLife::from_days(30));
813 assert_eq!(cfg.pro_any, HalfLife::no_decay());
814 }
815
816 #[test]
819 fn toml_empty_input_preserves_defaults() {
820 let cfg = DecayConfig::from_toml("").expect("parse");
821 assert_eq!(cfg, DecayConfig::librarian_defaults());
822 }
823
824 #[test]
825 fn toml_overrides_semantic_half_lives() {
826 let toml = r"
827 [decay.semantic]
828 profile = 30
829 observation = 365
830 ";
831 let cfg = DecayConfig::from_toml(toml).expect("parse");
832 assert_eq!(cfg.sem_profile, HalfLife::from_days(30));
833 assert_eq!(cfg.sem_observation, HalfLife::from_days(365));
834 assert_eq!(cfg.sem_document, HalfLife::from_days(365)); }
837
838 #[test]
839 fn toml_zero_encodes_no_decay() {
840 let toml = r"
842 [decay.semantic]
843 librarian_assignment = 0
844 profile = 0
845 ";
846 let cfg = DecayConfig::from_toml(toml).expect("parse");
847 assert_eq!(cfg.sem_librarian_assignment, HalfLife::no_decay());
848 assert_eq!(cfg.sem_profile, HalfLife::no_decay());
849 }
850
851 #[test]
852 fn toml_unknown_keys_are_ignored() {
853 let toml = r"
854 [decay.semantic]
855 profile = 30
856 future_source_kind = 42 # not in the v1 registry — must be ignored
857
858 [decay.not_a_real_section]
859 key = 1
860 ";
861 let cfg = DecayConfig::from_toml(toml).expect("parse");
862 assert_eq!(cfg.sem_profile, HalfLife::from_days(30));
863 }
864
865 #[test]
866 fn toml_rejects_negative_days() {
867 let toml = r"
868 [decay.semantic]
869 profile = -1
870 ";
871 let err = DecayConfig::from_toml(toml).expect_err("negative");
872 assert!(matches!(err, DecayConfigError::InvalidDays { .. }));
873 }
874
875 #[test]
876 fn toml_rejects_non_integer_values() {
877 let toml = r#"
878 [decay.semantic]
879 profile = "thirty"
880 "#;
881 let err = DecayConfig::from_toml(toml).expect_err("string");
882 assert!(matches!(
883 err,
884 DecayConfigError::ExpectedNonNegInteger { .. }
885 ));
886 }
887
888 #[test]
889 fn toml_rejects_wrong_section_type() {
890 let toml = r"
891 decay = 42
892 ";
893 let err = DecayConfig::from_toml(toml).expect_err("not a table");
894 assert!(matches!(
895 err,
896 DecayConfigError::ExpectedTable { path: "decay" }
897 ));
898 }
899
900 #[test]
901 fn toml_overrides_episodic_and_procedural() {
902 let toml = r"
903 [decay.episodic]
904 observation = 7
905 self_report = 3
906 participant_report = 14
907
908 [decay.procedural]
909 any = 365
910 ";
911 let cfg = DecayConfig::from_toml(toml).expect("parse");
912 assert_eq!(cfg.epi_observation, HalfLife::from_days(7));
913 assert_eq!(cfg.epi_self_report, HalfLife::from_days(3));
914 assert_eq!(cfg.epi_participant_report, HalfLife::from_days(14));
915 assert_eq!(cfg.pro_any, HalfLife::from_days(365));
916 }
917
918 #[test]
919 fn apply_toml_is_additive() {
920 let mut cfg = DecayConfig::librarian_defaults();
922 cfg.apply_toml("[decay.semantic]\nprofile = 30")
923 .expect("first");
924 assert_eq!(cfg.sem_profile, HalfLife::from_days(30));
925 cfg.apply_toml("[decay.semantic]\nobservation = 7")
926 .expect("second");
927 assert_eq!(cfg.sem_profile, HalfLife::from_days(30));
929 assert_eq!(cfg.sem_observation, HalfLife::from_days(7));
930 }
931
932 #[test]
933 fn toml_reload_changes_effective_confidence_without_restart() {
934 let mut cfg = DecayConfig::librarian_defaults();
937 let stored = c(1.0);
938 let elapsed = 30 * DAY_MS;
939
940 let before = effective_confidence(
941 stored,
942 elapsed,
943 MemoryKindTag::Semantic,
944 SourceKind::Observation,
945 DecayFlags::default(),
946 &cfg,
947 );
948
949 cfg.apply_toml("[decay.semantic]\nobservation = 1")
953 .expect("reload");
954 let after = effective_confidence(
955 stored,
956 elapsed,
957 MemoryKindTag::Semantic,
958 SourceKind::Observation,
959 DecayFlags::default(),
960 &cfg,
961 );
962 assert!(
963 after < before,
964 "reload did not accelerate decay: before={before:?} after={after:?}"
965 );
966 }
967
968 #[test]
969 fn user_override_takes_effect_at_runtime() {
970 let mut cfg = DecayConfig::librarian_defaults();
974 let stored = c(1.0);
975 let baseline = effective_confidence(
977 stored,
978 180 * DAY_MS,
979 MemoryKindTag::Semantic,
980 SourceKind::Observation,
981 DecayFlags::default(),
982 &cfg,
983 );
984 cfg.sem_observation = HalfLife::from_days(90);
987 let overridden = effective_confidence(
988 stored,
989 180 * DAY_MS,
990 MemoryKindTag::Semantic,
991 SourceKind::Observation,
992 DecayFlags::default(),
993 &cfg,
994 );
995 assert!(
996 overridden < baseline,
997 "override should accelerate decay; baseline={baseline:?} overridden={overridden:?}"
998 );
999 }
1000}