From 884939b14622348b537152054b7d8a6439f0130f Mon Sep 17 00:00:00 2001 From: ProofOfConcept Date: Tue, 10 Mar 2026 23:59:39 -0400 Subject: [PATCH] counters: add redb-backed persistent counters (skeleton) First use case: search hit tracking for rename protection. Nodes that memory-search actively finds shouldn't be renamed. The counters module provides increment/read/decay operations backed by redb (pure Rust, ACID, no C deps). Next step: wire into the poc-memory daemon via RPC so the daemon owns the DB exclusively and memory-search sends hits via RPC. Also reverts the JSONL search-hits approach in favor of this. --- poc-memory/Cargo.toml | 1 + poc-memory/src/counters.rs | 116 +++++++++++++++++++++++++++++++++++++ poc-memory/src/lib.rs | 1 + 3 files changed, 118 insertions(+) create mode 100644 poc-memory/src/counters.rs diff --git a/poc-memory/Cargo.toml b/poc-memory/Cargo.toml index 117c528..0364ef3 100644 --- a/poc-memory/Cargo.toml +++ b/poc-memory/Cargo.toml @@ -20,6 +20,7 @@ rayon = "1" peg = "0.8" paste = "1" jobkit = { git = "https://evilpiepirate.org/git/jobkit.git/" } +redb = "2" log = "0.4" ratatui = "0.29" crossterm = { version = "0.28", features = ["event-stream"] } diff --git a/poc-memory/src/counters.rs b/poc-memory/src/counters.rs new file mode 100644 index 0000000..7166bf1 --- /dev/null +++ b/poc-memory/src/counters.rs @@ -0,0 +1,116 @@ +// counters.rs — persistent counters backed by redb +// +// Tracks search hits, visit counts, and other per-key metrics that +// need fast increment/read without loading the full capnp store. +// +// Tables: +// search_hits: key → u64 (how often memory-search found this node) +// last_hit_ts: key → i64 (unix timestamp of last search hit) + +use redb::{Database, ReadableTable, TableDefinition}; +use std::path::PathBuf; + +const SEARCH_HITS: TableDefinition<&str, u64> = TableDefinition::new("search_hits"); +const LAST_HIT_TS: TableDefinition<&str, i64> = TableDefinition::new("last_hit_ts"); + +fn db_path() -> PathBuf { + crate::config::get().data_dir.join("counters.redb") +} + +/// Open (or create) the counters database. +pub fn open() -> Result { + Database::create(db_path()).map_err(|e| format!("open counters db: {}", e)) +} + +/// Increment search hit count for a set of keys. +pub fn record_search_hits(keys: &[&str]) -> Result<(), String> { + let db = open()?; + let ts = chrono::Utc::now().timestamp(); + let txn = db.begin_write().map_err(|e| format!("begin write: {}", e))?; + { + let mut hits = txn.open_table(SEARCH_HITS).map_err(|e| format!("open table: {}", e))?; + let mut ts_table = txn.open_table(LAST_HIT_TS).map_err(|e| format!("open table: {}", e))?; + for key in keys { + let count = hits.get(*key).map_err(|e| format!("get: {}", e))? + .map(|v| v.value()) + .unwrap_or(0); + hits.insert(*key, count + 1).map_err(|e| format!("insert: {}", e))?; + ts_table.insert(*key, ts).map_err(|e| format!("insert ts: {}", e))?; + } + } + txn.commit().map_err(|e| format!("commit: {}", e))?; + Ok(()) +} + +/// Get search hit count for a key. +pub fn search_hit_count(key: &str) -> u64 { + let db = match open() { + Ok(db) => db, + Err(_) => return 0, + }; + let txn = match db.begin_read() { + Ok(t) => t, + Err(_) => return 0, + }; + let table = match txn.open_table(SEARCH_HITS) { + Ok(t) => t, + Err(_) => return 0, + }; + table.get(key).ok().flatten().map(|v| v.value()).unwrap_or(0) +} + +/// Get all search hit counts (for rename agent). +/// Returns keys sorted by count descending. +pub fn all_search_hits() -> Vec<(String, u64)> { + let db = match open() { + Ok(db) => db, + Err(_) => return Vec::new(), + }; + let txn = match db.begin_read() { + Ok(t) => t, + Err(_) => return Vec::new(), + }; + let table = match txn.open_table(SEARCH_HITS) { + Ok(t) => t, + Err(_) => return Vec::new(), + }; + let mut results: Vec<(String, u64)> = match table.iter() { + Ok(iter) => iter + .flatten() + .map(|(k, v)| (k.value().to_string(), v.value())) + .collect(), + Err(_) => return Vec::new(), + }; + results.sort_by(|a, b| b.1.cmp(&a.1)); + results +} + +/// Decay all counters by a factor (e.g. 0.9 = 10% decay). +/// Removes entries that drop to zero. +pub fn decay_all(factor: f64) -> Result { + let db = open()?; + let txn = db.begin_write().map_err(|e| format!("begin write: {}", e))?; + let mut removed = 0; + { + let mut table = txn.open_table(SEARCH_HITS).map_err(|e| format!("open table: {}", e))?; + + // Collect keys first to avoid borrow conflict + let entries: Vec<(String, u64)> = table.iter() + .map_err(|e| format!("iter: {}", e))? + .flatten() + .map(|(k, v)| (k.value().to_string(), v.value())) + .collect(); + + for (key, count) in entries { + let new_count = (count as f64 * factor) as u64; + if new_count == 0 { + table.remove(key.as_str()).ok(); + removed += 1; + } else { + table.insert(key.as_str(), new_count).ok(); + } + } + } + txn.commit().map_err(|e| format!("commit: {}", e))?; + Ok(removed) +} diff --git a/poc-memory/src/lib.rs b/poc-memory/src/lib.rs index 9d7c6bb..037a998 100644 --- a/poc-memory/src/lib.rs +++ b/poc-memory/src/lib.rs @@ -16,6 +16,7 @@ pub mod query; pub mod migrate; pub mod transcript; pub mod neuro; +pub mod counters; // Agent layer (LLM-powered operations) pub mod agents;