Add Layer 4 caching to reduce AI load

Implemented 5-minute short-term caching for relationship inference:

**store.rs**:
- Added relationship_cache SQLite table
- save_relationship_cache(), get_cached_relationship()
- save_all_relationships_cache(), get_cached_all_relationships()
- clear_relationship_cache() - called on memory create/update/delete
- Cache duration: 5 minutes (configurable constant)

**relationship.rs**:
- Modified infer_all_relationships() to use cache
- Added get_relationship() function with caching support
- Cache hit: return immediately
- Cache miss: compute, save to cache, return

**base.rs**:
- Updated tool_get_relationship() to use cached version
- Reduced load from O(n) scan to O(1) cache lookup

**Benefits**:
- Reduces AI load when frequently querying relationships
- Automatic cache invalidation on data changes
- Scales better with growing memory count
- No user-facing changes

**Documentation**:
- Updated ARCHITECTURE.md with caching strategy details

This addresses scalability concerns for Layer 4 as memory data grows.
This commit is contained in:
Claude
2025-11-06 09:33:42 +00:00
parent 8037477104
commit 2579312029
5 changed files with 203 additions and 39 deletions

View File

@@ -102,6 +102,17 @@ impl MemoryStore {
[],
)?;
// Create relationship_cache table (Layer 4 - relationship inference cache)
// entity_id = "" for all_relationships cache
conn.execute(
"CREATE TABLE IF NOT EXISTS relationship_cache (
entity_id TEXT PRIMARY KEY,
data TEXT NOT NULL,
cached_at TEXT NOT NULL
)",
[],
)?;
Ok(Self { conn })
}
@@ -137,6 +148,10 @@ impl MemoryStore {
memory.updated_at.to_rfc3339(),
],
)?;
// Clear relationship cache since memory data changed
self.clear_relationship_cache()?;
Ok(())
}
@@ -204,6 +219,9 @@ impl MemoryStore {
return Err(MemoryError::NotFound(memory.id.clone()));
}
// Clear relationship cache since memory data changed
self.clear_relationship_cache()?;
Ok(())
}
@@ -217,6 +235,9 @@ impl MemoryStore {
return Err(MemoryError::NotFound(id.to_string()));
}
// Clear relationship cache since memory data changed
self.clear_relationship_cache()?;
Ok(())
}
@@ -464,6 +485,123 @@ impl MemoryStore {
Ok(profile)
}
// ========== Layer 4: Relationship Cache Methods ==========
/// Cache duration in minutes
const RELATIONSHIP_CACHE_DURATION_MINUTES: i64 = 5;
/// Save relationship inference to cache
pub fn save_relationship_cache(
&self,
entity_id: &str,
relationship: &super::relationship::RelationshipInference,
) -> Result<()> {
let data = serde_json::to_string(relationship)?;
let cached_at = Utc::now().to_rfc3339();
self.conn.execute(
"INSERT OR REPLACE INTO relationship_cache (entity_id, data, cached_at) VALUES (?1, ?2, ?3)",
params![entity_id, data, cached_at],
)?;
Ok(())
}
/// Get cached relationship inference
pub fn get_cached_relationship(
&self,
entity_id: &str,
) -> Result<Option<super::relationship::RelationshipInference>> {
let mut stmt = self
.conn
.prepare("SELECT data, cached_at FROM relationship_cache WHERE entity_id = ?1")?;
let result = stmt.query_row([entity_id], |row| {
let data: String = row.get(0)?;
let cached_at: String = row.get(1)?;
Ok((data, cached_at))
});
match result {
Ok((data, cached_at_str)) => {
// Check if cache is still valid (within 5 minutes)
let cached_at = DateTime::parse_from_rfc3339(&cached_at_str)
.map_err(|e| MemoryError::Parse(e.to_string()))?
.with_timezone(&Utc);
let age_minutes = (Utc::now() - cached_at).num_minutes();
if age_minutes < Self::RELATIONSHIP_CACHE_DURATION_MINUTES {
let relationship: super::relationship::RelationshipInference =
serde_json::from_str(&data)?;
Ok(Some(relationship))
} else {
// Cache expired
Ok(None)
}
}
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
/// Save all relationships list to cache (use empty string as entity_id)
pub fn save_all_relationships_cache(
&self,
relationships: &[super::relationship::RelationshipInference],
) -> Result<()> {
let data = serde_json::to_string(relationships)?;
let cached_at = Utc::now().to_rfc3339();
self.conn.execute(
"INSERT OR REPLACE INTO relationship_cache (entity_id, data, cached_at) VALUES ('', ?1, ?2)",
params![data, cached_at],
)?;
Ok(())
}
/// Get cached all relationships list
pub fn get_cached_all_relationships(
&self,
) -> Result<Option<Vec<super::relationship::RelationshipInference>>> {
let mut stmt = self
.conn
.prepare("SELECT data, cached_at FROM relationship_cache WHERE entity_id = ''")?;
let result = stmt.query_row([], |row| {
let data: String = row.get(0)?;
let cached_at: String = row.get(1)?;
Ok((data, cached_at))
});
match result {
Ok((data, cached_at_str)) => {
let cached_at = DateTime::parse_from_rfc3339(&cached_at_str)
.map_err(|e| MemoryError::Parse(e.to_string()))?
.with_timezone(&Utc);
let age_minutes = (Utc::now() - cached_at).num_minutes();
if age_minutes < Self::RELATIONSHIP_CACHE_DURATION_MINUTES {
let relationships: Vec<super::relationship::RelationshipInference> =
serde_json::from_str(&data)?;
Ok(Some(relationships))
} else {
Ok(None)
}
}
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
/// Clear all relationship caches (call when memories are modified)
pub fn clear_relationship_cache(&self) -> Result<()> {
self.conn.execute("DELETE FROM relationship_cache", [])?;
Ok(())
}
}
#[cfg(test)]