Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 80 additions & 9 deletions memoria/crates/memoria-api/src/routes/snapshots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,15 +374,51 @@ async fn snapshot_summary_value(
snapshot: &memoria_mcp::git_tools::VisibleSnapshot,
) -> Result<Value, (StatusCode, String)> {
let snapshot_name = validate_snapshot_identifier(&snapshot.internal_name)?.to_string();
let table = sql.t("mem_memories");
let count_sql = format!(
"SELECT COUNT(*) as cnt FROM {table} {{SNAPSHOT = '{snapshot_name}'}} WHERE user_id = ? AND is_active > 0"
);
let memory_count: i64 = sqlx::query_scalar(&count_sql)
.bind(user_id)
.fetch_one(sql.pool())
.await
.map_err(api_err)?;
let memory_count = if let Some(memory_count) = snapshot.memory_count {
memory_count
} else {
// Read-through backfill for registrations created before mem_snapshots.extra.
// The response still returns a concrete count, and registered snapshots are
// persisted so later list calls avoid historical COUNT queries.
let started = std::time::Instant::now();
let memory_count =
memoria_mcp::git_tools::count_active_memories_at_snapshot(sql, user_id, &snapshot_name)
.await
.map_err(api_err_typed)?;
if snapshot.registered {
sql.update_snapshot_memory_count(
user_id,
&snapshot.display_name,
&snapshot_name,
memory_count,
)
.await
.map_err(api_err_typed)?;
}
let elapsed_ms = started.elapsed().as_millis();
if elapsed_ms >= 100 {
tracing::info!(
user_id = %user_id,
snapshot_display = %snapshot.display_name,
snapshot_internal = %snapshot_name,
memory_count,
persisted = snapshot.registered,
elapsed_ms,
"snapshot_list_phase: snapshot_memory_count"
);
} else {
tracing::debug!(
user_id = %user_id,
snapshot_display = %snapshot.display_name,
snapshot_internal = %snapshot_name,
memory_count,
persisted = snapshot.registered,
elapsed_ms,
"snapshot_list_phase: snapshot_memory_count"
);
}
memory_count
};
let timestamp = format_snapshot_timestamp(snapshot.timestamp);
Ok(json!({
"name": snapshot.display_name,
Expand All @@ -401,11 +437,17 @@ async fn snapshot_list_payload(
limit: i64,
offset: i64,
) -> Result<Value, (StatusCode, String)> {
let started = std::time::Instant::now();
let store_started = std::time::Instant::now();
let sql = user_snapshot_store(state, user_id).await?;
let store_elapsed_ms = store_started.elapsed().as_millis();
let visible_started = std::time::Instant::now();
let all = memoria_mcp::git_tools::visible_snapshots_for_user(&state.service, user_id)
.await
.map_err(api_err_typed)?;
let visible_elapsed_ms = visible_started.elapsed().as_millis();
let total = all.len();
let summaries_started = std::time::Instant::now();
let mut snapshots = Vec::new();
for snapshot in all
.iter()
Expand All @@ -414,8 +456,37 @@ async fn snapshot_list_payload(
{
snapshots.push(snapshot_summary_value(&sql, user_id, snapshot).await?);
}
let summaries_elapsed_ms = summaries_started.elapsed().as_millis();
let has_more = offset.max(0) as usize + snapshots.len() < total;
let result = format_snapshot_list_result(&snapshots, total);
let total_elapsed_ms = started.elapsed().as_millis();
if total_elapsed_ms >= 100 {
tracing::info!(
user_id = %user_id,
total_snapshots = total,
returned_snapshots = snapshots.len(),
limit = limit.max(0),
offset = offset.max(0),
store_elapsed_ms,
visible_elapsed_ms,
summaries_elapsed_ms,
total_elapsed_ms,
"snapshot_list_phase: list_payload"
);
} else {
tracing::debug!(
user_id = %user_id,
total_snapshots = total,
returned_snapshots = snapshots.len(),
limit = limit.max(0),
offset = offset.max(0),
store_elapsed_ms,
visible_elapsed_ms,
summaries_elapsed_ms,
total_elapsed_ms,
"snapshot_list_phase: list_payload"
);
}
Ok(json!({
"snapshots": snapshots,
"total": total,
Expand Down
58 changes: 40 additions & 18 deletions memoria/crates/memoria-git/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -544,37 +544,59 @@ impl GitForDataService {
}

pub async fn list_snapshots(&self) -> Result<Vec<Snapshot>, MemoriaError> {
let rows = sqlx::query("SHOW SNAPSHOTS")
let started = std::time::Instant::now();
let show_sql = format!(
"SHOW SNAPSHOTS WHERE DATABASE_NAME = '{}'",
quote_sql_literal(&self.db_name)
);
let rows = sqlx::query(&show_sql)
.fetch_all(&self.pool)
.await
.map_err(db_err)?;
rows.iter()
let show_elapsed_ms = started.elapsed().as_millis();
let snapshots: Vec<Snapshot> = rows
.iter()
.map(|r| {
// Try NaiveDateTime directly first, then fall back to string parsing
let timestamp = r.try_get::<NaiveDateTime, _>("TIMESTAMP").ok().or_else(|| {
r.try_get::<String, _>("TIMESTAMP").ok().and_then(|s| {
// `SHOW SNAPSHOTS WHERE ...` is rewritten through a SELECT wrapper in
// MatrixOne and returns lower-case output column names.
let timestamp = r.try_get::<NaiveDateTime, _>("timestamp").ok().or_else(|| {
r.try_get::<String, _>("timestamp").ok().and_then(|s| {
NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S%.f")
.ok()
.or_else(|| NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S").ok())
})
});
Ok(Snapshot {
snapshot_name: r.try_get("SNAPSHOT_NAME").map_err(db_err)?,
snapshot_name: r.try_get("snapshot_name").map_err(db_err)?,
timestamp,
snapshot_level: r.try_get("SNAPSHOT_LEVEL").map_err(db_err)?,
account_name: r.try_get("ACCOUNT_NAME").map_err(db_err)?,
database_name: r.try_get("DATABASE_NAME").ok(),
table_name: r.try_get("TABLE_NAME").ok(),
snapshot_level: r.try_get("snapshot_level").map_err(db_err)?,
account_name: r.try_get("account_name").map_err(db_err)?,
database_name: r.try_get("database_name").ok(),
table_name: r.try_get("table_name").ok(),
})
})
.filter(|result| {
result
.as_ref()
.ok()
.and_then(|snapshot| snapshot.database_name.as_ref())
.is_some_and(|db_name| db_name == &self.db_name)
})
.collect()
.collect::<Result<Vec<Snapshot>, MemoriaError>>()?;
let total_elapsed_ms = started.elapsed().as_millis();
if total_elapsed_ms >= 100 {
tracing::info!(
db_name = %self.db_name,
show_rows = rows.len(),
filtered_rows = snapshots.len(),
show_elapsed_ms,
total_elapsed_ms,
"snapshot_list_phase: show_snapshots"
);
} else {
tracing::debug!(
db_name = %self.db_name,
show_rows = rows.len(),
filtered_rows = snapshots.len(),
show_elapsed_ms,
total_elapsed_ms,
"snapshot_list_phase: show_snapshots"
);
}
Ok(snapshots)
}

pub async fn get_snapshot(&self, name: &str) -> Result<Option<Snapshot>, MemoriaError> {
Expand Down
57 changes: 55 additions & 2 deletions memoria/crates/memoria-mcp/src/git_tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ fn safety_display_name(internal: &str) -> Option<String> {
pub struct VisibleSnapshot {
pub display_name: String,
pub internal_name: String,
pub memory_count: Option<i64>,
pub timestamp: Option<NaiveDateTime>,
pub registered: bool,
}
Expand Down Expand Up @@ -355,14 +356,34 @@ fn git_for_store(
))
}

pub async fn count_active_memories_at_snapshot(
sql: &Arc<memoria_storage::SqlMemoryStore>,
user_id: &str,
snapshot_name: &str,
) -> Result<i64, MemoriaError> {
let snapshot_name = validate_identifier(snapshot_name)?;
let table = sql.t("mem_memories");
let count_sql = format!(
"SELECT COUNT(*) as cnt FROM {table} {{SNAPSHOT = '{snapshot_name}'}} WHERE user_id = ? AND is_active > 0"
);
sqlx::query_scalar(&count_sql)
.bind(user_id)
.fetch_one(sql.pool())
.await
.map_err(db_err)
}

pub async fn visible_snapshots_for_user(
svc: &Arc<MemoryService>,
user_id: &str,
) -> Result<Vec<VisibleSnapshot>, MemoriaError> {
let started = std::time::Instant::now();
let sql = snapshot_store(svc, user_id).await?;
let db_name = sql.database_name();
let git = git_for_store(&sql)?;
let list_started = std::time::Instant::now();
let all = git.list_snapshots().await.map_err(git_err)?;
let list_elapsed_ms = list_started.elapsed().as_millis();
let actual_by_name: HashMap<String, memoria_git::Snapshot> = all
.into_iter()
.filter(|s| {
Expand All @@ -374,12 +395,16 @@ pub async fn visible_snapshots_for_user(

let mut snapshots = Vec::new();
let mut seen_internal = HashSet::new();
for reg in sql.list_snapshot_registrations(user_id).await? {
let registrations_started = std::time::Instant::now();
let registrations = sql.list_snapshot_registrations(user_id).await?;
let registrations_elapsed_ms = registrations_started.elapsed().as_millis();
for reg in registrations {
if let Some(actual) = actual_by_name.get(&reg.snapshot_name) {
seen_internal.insert(reg.snapshot_name.clone());
snapshots.push(VisibleSnapshot {
display_name: reg.name,
internal_name: reg.snapshot_name,
memory_count: memoria_storage::snapshot_extra_memory_count(reg.extra.as_ref()),
timestamp: actual.timestamp.or(Some(reg.created_at)),
registered: true,
});
Expand All @@ -399,13 +424,38 @@ pub async fn visible_snapshots_for_user(
snap_display(&actual.snapshot_name)
},
internal_name: actual.snapshot_name.clone(),
memory_count: None,
timestamp: actual.timestamp,
registered: false,
});
}
}

snapshots.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
let total_elapsed_ms = started.elapsed().as_millis();
if total_elapsed_ms >= 100 {
tracing::info!(
user_id = %user_id,
db_name = ?db_name,
actual_snapshot_count = actual_by_name.len(),
visible_snapshot_count = snapshots.len(),
show_snapshots_elapsed_ms = list_elapsed_ms,
registrations_elapsed_ms,
total_elapsed_ms,
"snapshot_list_phase: visible_snapshots_for_user"
);
} else {
tracing::debug!(
user_id = %user_id,
db_name = ?db_name,
actual_snapshot_count = actual_by_name.len(),
visible_snapshot_count = snapshots.len(),
show_snapshots_elapsed_ms = list_elapsed_ms,
registrations_elapsed_ms,
total_elapsed_ms,
"snapshot_list_phase: visible_snapshots_for_user"
);
}
Ok(snapshots)
}

Expand Down Expand Up @@ -816,7 +866,10 @@ pub async fn call(
return Err(git_err(err));
}
};
sql.register_snapshot(user_id, &display, &snap.snapshot_name)
let memory_count =
count_active_memories_at_snapshot(&sql, user_id, &snap.snapshot_name).await?;
let extra = memoria_storage::snapshot_extra_with_memory_count(memory_count);
sql.register_snapshot(user_id, &display, &snap.snapshot_name, Some(&extra))
.await?;
Ok(mcp_text(&format!(
"Snapshot '{}' created at {:?}",
Expand Down
5 changes: 3 additions & 2 deletions memoria/crates/memoria-storage/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub use pool_config::{
};
pub use router::{DbRouter, UserDatabaseRecord};
pub use store::{
FeedbackStats, MemoryFeedback, OwnedEditLogEntry, PoolHealthLevel, PoolHealthSnapshot,
SqlMemoryStore, TierFeedback, UserRetrievalParams, ACTOR_USER_ID,
snapshot_extra_memory_count, snapshot_extra_with_memory_count, FeedbackStats, MemoryFeedback,
OwnedEditLogEntry, PoolHealthLevel, PoolHealthSnapshot, SqlMemoryStore, TierFeedback,
UserRetrievalParams, ACTOR_USER_ID,
};
Loading
Loading