mirror of
https://github.com/samvallad33/vestige.git
synced 2026-05-08 15:22:37 +02:00
feat: Vestige v1.7.0 — 18 tools, automation triggers, SQLite perf
Tool consolidation: 23 → 18 tools - ingest merged into smart_ingest (single + batch mode) - session_checkpoint merged into smart_ingest batch (items param) - promote_memory/demote_memory merged into memory(action=promote/demote) - health_check/stats merged into system_status Automation triggers in system_status: - lastDreamTimestamp, savesSinceLastDream, lastBackupTimestamp, lastConsolidationTimestamp — enables Claude to conditionally trigger dream/backup/gc/find_duplicates at session start - Migration v6: dream_history table (dreams were in-memory only) - DreamHistoryRecord struct + save/query methods - Dream persistence in dream.rs (non-fatal on failure) SQLite performance: - PRAGMA mmap_size = 256MB (2-5x read speedup) - PRAGMA journal_size_limit = 64MB (prevents WAL bloat) - PRAGMA optimize = 0x10002 (fresh query planner stats on connect) - FTS5 segment merge during consolidation (20-40% keyword boost) - PRAGMA optimize during consolidation cycle 1,152 tests passing, 0 failures, release build clean.
This commit is contained in:
parent
33d8b6b405
commit
c29023dd80
20 changed files with 1478 additions and 168 deletions
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "vestige-core"
|
||||
version = "1.6.0"
|
||||
version = "1.7.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.85"
|
||||
authors = ["Vestige Team"]
|
||||
|
|
|
|||
|
|
@ -138,8 +138,8 @@ pub use fsrs::{
|
|||
|
||||
// Storage layer
|
||||
pub use storage::{
|
||||
ConsolidationHistoryRecord, InsightRecord, IntentionRecord, Result, SmartIngestResult,
|
||||
StateTransitionRecord, Storage, StorageError,
|
||||
ConsolidationHistoryRecord, DreamHistoryRecord, InsightRecord, IntentionRecord, Result,
|
||||
SmartIngestResult, StateTransitionRecord, Storage, StorageError,
|
||||
};
|
||||
|
||||
// Consolidation (sleep-inspired memory processing)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,11 @@ pub const MIGRATIONS: &[Migration] = &[
|
|||
description: "FSRS-6 upgrade: access history, ACT-R activation, personalized decay",
|
||||
up: MIGRATION_V5_UP,
|
||||
},
|
||||
Migration {
|
||||
version: 6,
|
||||
description: "Dream history persistence for automation triggers",
|
||||
up: MIGRATION_V6_UP,
|
||||
},
|
||||
];
|
||||
|
||||
/// A database migration
|
||||
|
|
@ -447,6 +452,26 @@ ALTER TABLE consolidation_history ADD COLUMN w20_optimized REAL;
|
|||
UPDATE schema_version SET version = 5, applied_at = datetime('now');
|
||||
"#;
|
||||
|
||||
/// V6: Dream history persistence for automation triggers
|
||||
/// Dreams were in-memory only (MemoryDreamer.dream_history Vec<DreamResult> lost on restart).
|
||||
/// This table persists dream metadata so system_status can report when last dream ran.
|
||||
const MIGRATION_V6_UP: &str = r#"
|
||||
CREATE TABLE IF NOT EXISTS dream_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
dreamed_at TEXT NOT NULL,
|
||||
duration_ms INTEGER NOT NULL DEFAULT 0,
|
||||
memories_replayed INTEGER NOT NULL DEFAULT 0,
|
||||
connections_found INTEGER NOT NULL DEFAULT 0,
|
||||
insights_generated INTEGER NOT NULL DEFAULT 0,
|
||||
memories_strengthened INTEGER NOT NULL DEFAULT 0,
|
||||
memories_compressed INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_dream_history_dreamed_at ON dream_history(dreamed_at);
|
||||
|
||||
UPDATE schema_version SET version = 6, applied_at = datetime('now');
|
||||
"#;
|
||||
|
||||
/// Get current schema version from database
|
||||
pub fn get_current_version(conn: &rusqlite::Connection) -> rusqlite::Result<u32> {
|
||||
conn.query_row(
|
||||
|
|
|
|||
|
|
@ -11,6 +11,6 @@ mod sqlite;
|
|||
|
||||
pub use migrations::MIGRATIONS;
|
||||
pub use sqlite::{
|
||||
ConsolidationHistoryRecord, InsightRecord, IntentionRecord, Result, SmartIngestResult,
|
||||
StateTransitionRecord, Storage, StorageError,
|
||||
ConsolidationHistoryRecord, DreamHistoryRecord, InsightRecord, IntentionRecord, Result,
|
||||
SmartIngestResult, StateTransitionRecord, Storage, StorageError,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -140,7 +140,10 @@ impl Storage {
|
|||
PRAGMA cache_size = -64000;
|
||||
PRAGMA temp_store = MEMORY;
|
||||
PRAGMA foreign_keys = ON;
|
||||
PRAGMA busy_timeout = 5000;",
|
||||
PRAGMA busy_timeout = 5000;
|
||||
PRAGMA mmap_size = 268435456;
|
||||
PRAGMA journal_size_limit = 67108864;
|
||||
PRAGMA optimize = 0x10002;",
|
||||
)?;
|
||||
|
||||
#[cfg(feature = "embeddings")]
|
||||
|
|
@ -1851,6 +1854,14 @@ impl Storage {
|
|||
// 15. Connection Graph Maintenance (decay + prune weak connections)
|
||||
let _connections_pruned = self.prune_weak_connections(0.05).unwrap_or(0) as i64;
|
||||
|
||||
// 16. FTS5 index optimization — merge segments for faster keyword search
|
||||
let _ = self.conn.execute_batch(
|
||||
"INSERT INTO knowledge_fts(knowledge_fts) VALUES('optimize');"
|
||||
);
|
||||
|
||||
// 17. Run PRAGMA optimize to refresh query planner statistics
|
||||
let _ = self.conn.execute_batch("PRAGMA optimize;");
|
||||
|
||||
let duration = start.elapsed().as_millis() as i64;
|
||||
|
||||
// Record consolidation history (bug fix: was never recorded before v1.4.0)
|
||||
|
|
@ -2310,6 +2321,18 @@ pub struct ConsolidationHistoryRecord {
|
|||
pub insights_generated: i32,
|
||||
}
|
||||
|
||||
/// Dream history record — persists dream metadata for automation triggers
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct DreamHistoryRecord {
|
||||
pub dreamed_at: DateTime<Utc>,
|
||||
pub duration_ms: i64,
|
||||
pub memories_replayed: i32,
|
||||
pub connections_found: i32,
|
||||
pub insights_generated: i32,
|
||||
pub memories_strengthened: i32,
|
||||
pub memories_compressed: i32,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
// ========================================================================
|
||||
// INTENTIONS PERSISTENCE
|
||||
|
|
@ -2849,6 +2872,84 @@ impl Storage {
|
|||
Ok(result)
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// DREAM HISTORY PERSISTENCE
|
||||
// ========================================================================
|
||||
|
||||
/// Save a dream history record
|
||||
pub fn save_dream_history(&mut self, record: &DreamHistoryRecord) -> Result<i64> {
|
||||
self.conn.execute(
|
||||
"INSERT INTO dream_history (
|
||||
dreamed_at, duration_ms, memories_replayed, connections_found,
|
||||
insights_generated, memories_strengthened, memories_compressed
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||
params![
|
||||
record.dreamed_at.to_rfc3339(),
|
||||
record.duration_ms,
|
||||
record.memories_replayed,
|
||||
record.connections_found,
|
||||
record.insights_generated,
|
||||
record.memories_strengthened,
|
||||
record.memories_compressed,
|
||||
],
|
||||
)?;
|
||||
Ok(self.conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
/// Get last dream timestamp
|
||||
pub fn get_last_dream(&self) -> Result<Option<DateTime<Utc>>> {
|
||||
let result: Option<String> = self.conn.query_row(
|
||||
"SELECT MAX(dreamed_at) FROM dream_history",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
).ok().flatten();
|
||||
|
||||
Ok(result.and_then(|s| {
|
||||
DateTime::parse_from_rfc3339(&s).ok().map(|dt| dt.with_timezone(&Utc))
|
||||
}))
|
||||
}
|
||||
|
||||
/// Count memories created since a given timestamp
|
||||
pub fn count_memories_since(&self, since: DateTime<Utc>) -> Result<i64> {
|
||||
let count: i64 = self.conn.query_row(
|
||||
"SELECT COUNT(*) FROM knowledge_nodes WHERE created_at >= ?1",
|
||||
params![since.to_rfc3339()],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Get last backup timestamp by scanning the backups directory.
|
||||
/// Parses `vestige-YYYYMMDD-HHMMSS.db` filenames.
|
||||
pub fn get_last_backup_timestamp() -> Option<DateTime<Utc>> {
|
||||
let proj_dirs = directories::ProjectDirs::from("com", "vestige", "core")?;
|
||||
let backup_dir = proj_dirs.data_dir().parent()?.join("backups");
|
||||
|
||||
if !backup_dir.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut latest: Option<DateTime<Utc>> = None;
|
||||
|
||||
if let Ok(entries) = std::fs::read_dir(&backup_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name();
|
||||
let name_str = name.to_string_lossy();
|
||||
// Parse vestige-YYYYMMDD-HHMMSS.db
|
||||
if let Some(ts_part) = name_str.strip_prefix("vestige-").and_then(|s| s.strip_suffix(".db")) {
|
||||
if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(ts_part, "%Y%m%d-%H%M%S") {
|
||||
let dt = naive.and_utc();
|
||||
if latest.as_ref().is_none_or(|l| dt > *l) {
|
||||
latest = Some(dt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
latest
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// STATE TRANSITIONS (Audit Trail)
|
||||
// ========================================================================
|
||||
|
|
@ -3009,4 +3110,63 @@ mod tests {
|
|||
assert!(deleted);
|
||||
assert!(storage.get_node(&node.id).unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dream_history_save_and_get_last() {
|
||||
let mut storage = create_test_storage();
|
||||
let now = Utc::now();
|
||||
|
||||
let record = DreamHistoryRecord {
|
||||
dreamed_at: now,
|
||||
duration_ms: 1500,
|
||||
memories_replayed: 50,
|
||||
connections_found: 12,
|
||||
insights_generated: 3,
|
||||
memories_strengthened: 8,
|
||||
memories_compressed: 2,
|
||||
};
|
||||
|
||||
let id = storage.save_dream_history(&record).unwrap();
|
||||
assert!(id > 0);
|
||||
|
||||
let last = storage.get_last_dream().unwrap();
|
||||
assert!(last.is_some());
|
||||
// Timestamps should be within 1 second (RFC3339 round-trip)
|
||||
let diff = (last.unwrap() - now).num_seconds().abs();
|
||||
assert!(diff <= 1, "Timestamp mismatch: diff={}s", diff);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dream_history_empty() {
|
||||
let storage = create_test_storage();
|
||||
let last = storage.get_last_dream().unwrap();
|
||||
assert!(last.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_count_memories_since() {
|
||||
let mut storage = create_test_storage();
|
||||
let before = Utc::now() - Duration::seconds(10);
|
||||
|
||||
for i in 0..5 {
|
||||
storage.ingest(IngestInput {
|
||||
content: format!("Count test memory {}", i),
|
||||
node_type: "fact".to_string(),
|
||||
..Default::default()
|
||||
}).unwrap();
|
||||
}
|
||||
|
||||
let count = storage.count_memories_since(before).unwrap();
|
||||
assert_eq!(count, 5);
|
||||
|
||||
let future = Utc::now() + Duration::hours(1);
|
||||
let count_future = storage.count_memories_since(future).unwrap();
|
||||
assert_eq!(count_future, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_last_backup_timestamp_no_panic() {
|
||||
// Static method should not panic even if no backups exist
|
||||
let _ = Storage::get_last_backup_timestamp();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue