training: per-node scoring with graph weight updates

Memory scoring now uses the graph as source of truth:
- last_scored timestamp on each node (new capnp field @22)
- Nodes scored when older than scoring_interval_secs (default 1hr)
- Oldest-scored-first ordering
- Window: scoring_response_window assistant responses (default 100)
- First-quarter memories scored even without full window
- Per-response normalization (raw divergence / response count)
- Asymmetric weight update: alpha=0.5 up, alpha=0.1 down
  (responds fast to importance, decays slowly — memories stay
  surfaced even if only useful 1/4 of the time)

Graph writes disabled pending normalization calibration.

Also: configurable scoring_interval_secs and scoring_response_window.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
ProofOfConcept 2026-04-04 05:01:49 -04:00 committed by Kent Overstreet
parent b0603fd1ef
commit fcd77fb79e
8 changed files with 109 additions and 64 deletions

View file

@ -296,6 +296,22 @@ impl Store {
Ok((old, weight))
}
/// Update a node's weight with a new score and record the scoring
/// timestamp. Uses asymmetric smoothing: responds quickly to high
/// scores (alpha=0.5) but decays slowly on low scores (alpha=0.1).
/// This keeps memories surfaced even if they're only useful 1 in 4 times.
/// Returns (old_weight, new_weight).
pub fn score_weight(&mut self, key: &str, score: f64) -> Result<(f32, f32), String> {
let node = self.nodes.get_mut(key)
.ok_or_else(|| format!("node not found: {}", key))?;
let old = node.weight;
let alpha = if score > old as f64 { 0.5 } else { 0.1 };
let new = (alpha * score + (1.0 - alpha) * old as f64) as f32;
node.weight = new.clamp(0.01, 1.0);
node.last_scored = chrono::Utc::now().timestamp();
Ok((old, node.weight))
}
/// Set the strength of a link between two nodes. Deduplicates if
/// multiple links exist. Returns the old strength, or error if no link.
pub fn set_link_strength(&mut self, source: &str, target: &str, strength: f32) -> Result<f32, String> {

View file

@ -214,6 +214,10 @@ pub struct Node {
#[serde(default)]
pub created_at: i64,
// Memory importance scoring — unix epoch seconds, 0 = never scored.
#[serde(default)]
pub last_scored: i64,
// Derived fields (not in capnp, computed from graph)
#[serde(default)]
pub community_id: Option<u32>,
@ -342,7 +346,7 @@ capnp_message!(Node,
uuid: [uuid],
prim: [version, timestamp, weight, emotion, deleted,
retrievals, uses, wrongs, last_replayed,
spaced_repetition_interval, position, created_at],
spaced_repetition_interval, position, created_at, last_scored],
enm: [node_type: NodeType],
skip: [community_id, clustering_coefficient, degree],
);
@ -531,6 +535,7 @@ pub fn new_node(key: &str, content: &str) -> Node {
spaced_repetition_interval: 1,
position: 0,
created_at: now_epoch(),
last_scored: 0,
community_id: None,
clustering_coefficient: None,
degree: None,