Skip to main content

hippmem_engine/
feedback_api.rs

1//! Engine::feedback — usage feedback API.
2//!
3//! Corresponds to 05#feedback, 09 §4.6. Records usage signals to activation_log
4//! (driving Hebbian reinforcement/decay, consumed by consolidate) and updates each
5//! memory's `usage_score` (0.3.0: confirmations/references raise it, rejections
6//! lower it — retrieval energy is weighted by usage_score).
7
8use crate::signals::usage_delta;
9use crate::{Engine, EngineError, EngineResult, FeedbackInput};
10use hippmem_core::model::unit::MemoryUnit;
11use hippmem_core::score::UnitScore;
12use hippmem_core::time::{Clock, SystemClock};
13use hippmem_store::activation_log::{ActivationLogger, ActivationRecord};
14use hippmem_store::kv::KvStore;
15
16impl Engine {
17    /// Records a usage feedback signal and updates usage_score of the referenced memories.
18    ///
19    /// The signal is persisted to activation_log for consumption by the
20    /// background Hebbian/decay worker; usage_score changes take effect on the
21    /// next retrieval (energy formula, 03 §4).
22    pub fn feedback(&self, input: FeedbackInput) -> EngineResult<()> {
23        let clock = SystemClock;
24        let now = clock.now();
25
26        let logger = ActivationLogger::new(self.store.db_arc());
27        let signal = signal_to_string(&input.signal);
28        let rec = ActivationRecord {
29            retrieval_id: input.retrieval_id,
30            // P1 回归:保留完整 u128 id(截断会导致下游 Hebbian/RecentActivation 失配)
31            used_memory_ids: input.used_memory_ids.iter().map(|id| id.0).collect(),
32            signal: signal.clone(),
33            recorded_at_ms: now.as_i64(),
34        };
35        logger
36            .record(&rec)
37            .map_err(|e| EngineError::Internal(format!("activation_log: {}", e)))?;
38
39        // 0.3.0: usage_score 更新(确认 ↑ / 拒绝 ↓,clamp [0,1])
40        let delta = usage_delta(&signal);
41        let apply_delta = |ids: &[u128], amount: f32| -> EngineResult<()> {
42            let kv = KvStore::new(self.store.db_arc());
43            for id in ids {
44                if let Some(raw) = kv.get(id).map_err(|e| EngineError::Store(e.to_string()))? {
45                    let (mut unit, _): (MemoryUnit, _) =
46                        bincode::serde::decode_from_slice(&raw, bincode::config::standard())
47                            .map_err(|e| EngineError::Internal(e.to_string()))?;
48                    let new_usage = (unit.activation.usage_score.value() + amount).clamp(0.0, 1.0);
49                    unit.activation.usage_score = UnitScore::new(new_usage);
50                    let encoded = bincode::serde::encode_to_vec(&unit, bincode::config::standard())
51                        .map_err(|e| EngineError::Internal(e.to_string()))?;
52                    kv.put(*id, &encoded)
53                        .map_err(|e| EngineError::Store(e.to_string()))?;
54                }
55            }
56            Ok(())
57        };
58        if delta != 0.0 && !input.used_memory_ids.is_empty() {
59            let ids: Vec<u128> = input.used_memory_ids.iter().map(|id| id.0).collect();
60            apply_delta(&ids, delta)?;
61        }
62        // D-B (0.4.0): result-set reject. An empty used_memory_ids with
63        // UserRejected means "the whole result set was wrong" (trap questions,
64        // noisy stores) — lower the usage score of every memory returned by
65        // that retrieval. The result set is recovered from the retrieval's
66        // own activation-log record (written by retrieve). A targeted reject
67        // (non-empty used_memory_ids) keeps the stronger -0.10 per memory.
68        if matches!(input.signal, crate::UsageSignal::UserRejected)
69            && input.used_memory_ids.is_empty()
70        {
71            if let Ok(records) = logger.read_all() {
72                if let Some(rec) = records
73                    .iter()
74                    .find(|r| r.retrieval_id == input.retrieval_id && r.signal == "retrieve")
75                {
76                    apply_delta(&rec.used_memory_ids, RESULT_SET_REJECT_DELTA)?;
77                }
78            }
79        }
80
81        Ok(())
82    }
83}
84
85/// Usage-score adjustment per memory for a result-set reject (D-B, 0.4.0):
86/// weaker than the targeted reject (-0.10) because no single memory was
87/// singled out as wrong.
88const RESULT_SET_REJECT_DELTA: f32 = -0.05;
89
90fn signal_to_string(s: &crate::UsageSignal) -> String {
91    match s {
92        crate::UsageSignal::Referenced => "Referenced".into(),
93        crate::UsageSignal::UserConfirmedCorrect => "UserConfirmedCorrect".into(),
94        crate::UsageSignal::TaskSucceeded => "TaskSucceeded".into(),
95        crate::UsageSignal::UserRejected => "UserRejected".into(),
96    }
97}