feat(growth): add memory decay + time-weighted scoring + remove dead frontend
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
- Add effective_importance() with exponential time decay (30-day half-life) and access count boost for fair scoring of stale vs fresh memories - Add SqliteStorage::decay_memories() for periodic maintenance: reduces stored importance per interval, archives (deletes) below threshold - Update find() scoring to use time-decayed importance in sort - Add DecayResult type and effective_importance re-export in lib.rs - Remove dead frontend active-learning.ts (370 lines, zero imports)
This commit is contained in:
@@ -67,6 +67,7 @@ pub mod summarizer;
|
|||||||
|
|
||||||
// Re-export main types for convenience
|
// Re-export main types for convenience
|
||||||
pub use types::{
|
pub use types::{
|
||||||
|
DecayResult,
|
||||||
ExtractedMemory,
|
ExtractedMemory,
|
||||||
ExtractionConfig,
|
ExtractionConfig,
|
||||||
GrowthStats,
|
GrowthStats,
|
||||||
@@ -75,6 +76,7 @@ pub use types::{
|
|||||||
RetrievalConfig,
|
RetrievalConfig,
|
||||||
RetrievalResult,
|
RetrievalResult,
|
||||||
UriBuilder,
|
UriBuilder,
|
||||||
|
effective_importance,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use extractor::{LlmDriverForExtraction, MemoryExtractor};
|
pub use extractor::{LlmDriverForExtraction, MemoryExtractor};
|
||||||
|
|||||||
@@ -270,6 +270,74 @@ impl SqliteStorage {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decay stale memories: reduce importance for long-unaccessed entries
|
||||||
|
/// and archive those below the minimum threshold.
|
||||||
|
///
|
||||||
|
/// - For every `decay_interval_days` since last access, importance drops by 1.
|
||||||
|
/// - Memories with importance ≤ `archive_threshold` are deleted.
|
||||||
|
pub async fn decay_memories(
|
||||||
|
&self,
|
||||||
|
decay_interval_days: u32,
|
||||||
|
archive_threshold: u8,
|
||||||
|
) -> crate::types::DecayResult {
|
||||||
|
// Step 1: Reduce importance of stale memories
|
||||||
|
let decay_result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
UPDATE memories
|
||||||
|
SET importance = MAX(1, importance - CAST(
|
||||||
|
(julianday('now') - julianday(last_accessed)) / ? AS INTEGER
|
||||||
|
))
|
||||||
|
WHERE last_accessed < datetime('now', '-' || ? || ' days')
|
||||||
|
AND importance > 1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(decay_interval_days)
|
||||||
|
.bind(decay_interval_days)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let decayed = decay_result
|
||||||
|
.map(|r| r.rows_affected())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
// Step 2: Remove memories that fell below archive threshold
|
||||||
|
// and haven't been accessed in 90+ days
|
||||||
|
let archive_result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
DELETE FROM memories
|
||||||
|
WHERE importance <= ?
|
||||||
|
AND last_accessed < datetime('now', '-90 days')
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(archive_threshold as i32)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Also clean up FTS entries for archived memories
|
||||||
|
let _ = sqlx::query(
|
||||||
|
r#"
|
||||||
|
DELETE FROM memories_fts
|
||||||
|
WHERE uri NOT IN (SELECT uri FROM memories)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let archived = archive_result
|
||||||
|
.map(|r| r.rows_affected())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
if decayed > 0 || archived > 0 {
|
||||||
|
tracing::info!(
|
||||||
|
"[SqliteStorage] Memory decay: {} decayed, {} archived",
|
||||||
|
decayed,
|
||||||
|
archived
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
crate::types::DecayResult { decayed, archived }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl sqlx::FromRow<'_, SqliteRow> for MemoryRow {
|
impl sqlx::FromRow<'_, SqliteRow> for MemoryRow {
|
||||||
@@ -567,7 +635,7 @@ impl VikingStorage for SqliteStorage {
|
|||||||
scorer.is_embedding_available()
|
scorer.is_embedding_available()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut scored_entries: Vec<(f32, MemoryEntry)> = Vec::new();
|
let mut scored_entries: Vec<(f32, f32, MemoryEntry)> = Vec::new();
|
||||||
|
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let entry = self.row_to_entry(&row);
|
let entry = self.row_to_entry(&row);
|
||||||
@@ -613,15 +681,18 @@ impl VikingStorage for SqliteStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scored_entries.push((semantic_score, entry));
|
// Apply time decay to importance before final scoring
|
||||||
|
let time_decayed_importance = crate::types::effective_importance(&entry);
|
||||||
|
|
||||||
|
scored_entries.push((semantic_score, time_decayed_importance, entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by score (descending), then by importance and access count
|
// Sort by: semantic score → time-decayed importance → access count (all descending)
|
||||||
scored_entries.sort_by(|a, b| {
|
scored_entries.sort_by(|a, b| {
|
||||||
b.0.partial_cmp(&a.0)
|
b.0.partial_cmp(&a.0)
|
||||||
.unwrap_or(std::cmp::Ordering::Equal)
|
.unwrap_or(std::cmp::Ordering::Equal)
|
||||||
.then_with(|| b.1.importance.cmp(&a.1.importance))
|
.then_with(|| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal))
|
||||||
.then_with(|| b.1.access_count.cmp(&a.1.access_count))
|
.then_with(|| b.2.access_count.cmp(&a.2.access_count))
|
||||||
});
|
});
|
||||||
|
|
||||||
// Apply limit
|
// Apply limit
|
||||||
@@ -629,7 +700,7 @@ impl VikingStorage for SqliteStorage {
|
|||||||
scored_entries.truncate(limit);
|
scored_entries.truncate(limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(scored_entries.into_iter().map(|(_, entry)| entry).collect())
|
Ok(scored_entries.into_iter().map(|(_, _, entry)| entry).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn find_by_prefix(&self, prefix: &str) -> Result<Vec<MemoryEntry>> {
|
async fn find_by_prefix(&self, prefix: &str) -> Result<Vec<MemoryEntry>> {
|
||||||
|
|||||||
@@ -385,6 +385,29 @@ impl UriBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Result of a memory decay operation
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DecayResult {
|
||||||
|
/// Number of memories whose importance was reduced
|
||||||
|
pub decayed: u64,
|
||||||
|
/// Number of memories archived (importance fell below threshold)
|
||||||
|
pub archived: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute effective importance with time decay.
|
||||||
|
///
|
||||||
|
/// Uses exponential decay: each 30-day period of non-access reduces
|
||||||
|
/// effective importance by ~50%. Frequently accessed memories decay slower
|
||||||
|
/// thanks to the access_count boost.
|
||||||
|
pub fn effective_importance(entry: &MemoryEntry) -> f32 {
|
||||||
|
let days_since = (Utc::now() - entry.last_accessed).num_days().max(0) as f32;
|
||||||
|
// Half-life: 30 days → decay factor per day ≈ 0.977
|
||||||
|
let time_decay = 0.977_f32.powf(days_since);
|
||||||
|
// Access boost: every 10 accesses add 1 to base importance (capped at 10)
|
||||||
|
let boosted = (entry.importance as f32 + entry.access_count as f32 / 10.0).min(10.0);
|
||||||
|
boosted * time_decay
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user