Skip to content

Commit a8ecfdf

Browse files
gouhongshenCopilot
andcommitted
fix(memory): address pick review follow-ups
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent df8b1ce commit a8ecfdf

1 file changed

Lines changed: 69 additions & 40 deletions

File tree

memoria/crates/memoria-mcp/src/git_tools.rs

Lines changed: 69 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const DEFAULT_PICK_TARGET: &str = "main";
5454
const DEFAULT_PICK_STRATEGY: &str = "fail";
5555
const DEFAULT_PICK_TOP_K: i64 = 5;
5656
const MAX_PICK_TOP_K: i64 = 100;
57+
const MAX_PICK_KEYS: usize = 1000;
5758

5859
fn safety_prefix(db_name: Option<&str>) -> String {
5960
match db_name {
@@ -1110,7 +1111,7 @@ pub async fn call(
11101111
err,
11111112
&pick_args.source,
11121113
&pick_args.target,
1113-
pick_keys.len(),
1114+
Some(pick_keys.len()),
11141115
);
11151116
}
11161117

@@ -1133,10 +1134,10 @@ pub async fn call(
11331134
} else {
11341135
String::new()
11351136
};
1136-
return Ok(mcp_text(&format!(
1137+
Ok(mcp_text(&format!(
11371138
"Picked {applied} change(s) from branch '{}' into '{}' ({skipped} skipped{ignored_suffix})",
11381139
pick_args.source, pick_args.target
1139-
)));
1140+
)))
11401141
}
11411142
PickSelector::SnapshotRange {
11421143
from_snapshot,
@@ -1168,12 +1169,12 @@ pub async fn call(
11681169
)
11691170
.await
11701171
{
1171-
return map_pick_conflict(err, &pick_args.source, &pick_args.target, 0);
1172+
return map_pick_conflict(err, &pick_args.source, &pick_args.target, None);
11721173
}
1173-
return Ok(mcp_text(&format!(
1174+
Ok(mcp_text(&format!(
11741175
"Picked snapshot range '{}'..'{}' from branch '{}' into '{}'.",
11751176
from_snapshot, to_snapshot, pick_args.source, pick_args.target
1176-
)));
1177+
)))
11771178
}
11781179
PickSelector::Retrieve { query, top_k } => {
11791180
if top_k <= 0 || top_k > MAX_PICK_TOP_K {
@@ -1187,20 +1188,18 @@ pub async fn call(
11871188
&source_table,
11881189
&target_table,
11891190
None,
1190-
Some((top_k * 10).max(20)),
1191+
None,
11911192
)
11921193
.await?;
1193-
let selected = select_retrieved_pick_keys(
1194+
let select_ctx = PickSelectionContext {
11941195
svc,
1195-
&sql,
1196+
sql: &sql,
11961197
user_id,
1197-
&source_table,
1198-
&target_table,
1199-
&query,
1200-
top_k,
1201-
candidates,
1202-
)
1203-
.await?;
1198+
source_table: &source_table,
1199+
target_table: &target_table,
1200+
};
1201+
let selected =
1202+
select_retrieved_pick_keys(&select_ctx, &query, top_k, candidates).await?;
12041203
if selected.is_empty() {
12051204
return Ok(mcp_text(&format!(
12061205
"No pickable changes matched query '{}' in branch '{}'.",
@@ -1220,7 +1219,7 @@ pub async fn call(
12201219
err,
12211220
&pick_args.source,
12221221
&pick_args.target,
1223-
selected.len(),
1222+
Some(selected.len()),
12241223
);
12251224
}
12261225
let remaining = count_pick_candidates(
@@ -1237,10 +1236,10 @@ pub async fn call(
12371236
} else {
12381237
0
12391238
};
1240-
return Ok(mcp_text(&format!(
1239+
Ok(mcp_text(&format!(
12411240
"Picked {applied} of top {top_k} retrieved change(s) from branch '{}' into '{}' ({skipped} skipped)",
12421241
pick_args.source, pick_args.target
1243-
)));
1242+
)))
12441243
}
12451244
}
12461245
}
@@ -1510,9 +1509,22 @@ fn normalize_keys(keys: Vec<String>) -> Result<Vec<String>, MemoriaError> {
15101509
"key_list selector requires at least one non-empty key".into(),
15111510
));
15121511
}
1512+
if out.len() > MAX_PICK_KEYS {
1513+
return Err(MemoriaError::Validation(format!(
1514+
"key_list selector supports at most {MAX_PICK_KEYS} unique keys"
1515+
)));
1516+
}
15131517
Ok(out)
15141518
}
15151519

1520+
struct PickSelectionContext<'a> {
1521+
svc: &'a Arc<MemoryService>,
1522+
sql: &'a Arc<memoria_storage::SqlMemoryStore>,
1523+
user_id: &'a str,
1524+
source_table: &'a str,
1525+
target_table: &'a str,
1526+
}
1527+
15161528
async fn resolve_branch_table_name(
15171529
sql: &Arc<memoria_storage::SqlMemoryStore>,
15181530
user_id: &str,
@@ -1661,11 +1673,7 @@ fn vec_to_mo(v: &[f32]) -> String {
16611673
}
16621674

16631675
async fn select_retrieved_pick_keys(
1664-
svc: &Arc<MemoryService>,
1665-
sql: &Arc<memoria_storage::SqlMemoryStore>,
1666-
user_id: &str,
1667-
source_table: &str,
1668-
target_table: &str,
1676+
ctx: &PickSelectionContext<'_>,
16691677
query: &str,
16701678
top_k: i64,
16711679
candidates: Vec<PickCandidate>,
@@ -1683,22 +1691,25 @@ async fn select_retrieved_pick_keys(
16831691
let fetch_k = (top_k * 3).max(20);
16841692
let mut scores: HashMap<String, f64> = HashMap::new();
16851693
let diff_clause = pickable_diff_clause("s", "t");
1686-
let safe_user_id = sanitize_sql_literal(user_id);
1694+
let safe_user_id = sanitize_sql_literal(ctx.user_id);
16871695

1688-
if let Some(embedding) = svc.embed(query).await? {
1696+
if let Some(embedding) = ctx.svc.embed(query).await? {
16891697
let vec_literal = vec_to_mo(&embedding);
16901698
let sql_text = format!(
16911699
"SELECT s.memory_id, CAST(l2_distance(s.embedding, '{vec_literal}') AS DOUBLE) AS l2_dist \
16921700
FROM {source_table} s \
16931701
LEFT JOIN {target_table} t ON t.memory_id = s.memory_id \
1694-
WHERE s.user_id = '{safe_user_id}' AND s.is_active = 1 \
1695-
AND s.embedding IS NOT NULL AND vector_dims(s.embedding) > 0 \
1696-
AND ({diff_clause}) \
1697-
ORDER BY l2_distance(s.embedding, '{vec_literal}') ASC \
1698-
LIMIT {fetch_k} by rank with option 'mode=post'"
1702+
WHERE s.user_id = '{safe_user_id}' AND s.is_active = 1 \
1703+
AND s.embedding IS NOT NULL AND vector_dims(s.embedding) > 0 \
1704+
AND ({diff_clause}) \
1705+
ORDER BY l2_distance(s.embedding, '{vec_literal}') ASC \
1706+
LIMIT {fetch_k} by rank with option 'mode=post'"
1707+
,
1708+
source_table = ctx.source_table,
1709+
target_table = ctx.target_table
16991710
);
17001711
let rows = sqlx::query(&sql_text)
1701-
.fetch_all(sql.pool())
1712+
.fetch_all(ctx.sql.pool())
17021713
.await
17031714
.map_err(db_err)?;
17041715
for row in rows {
@@ -1715,14 +1726,17 @@ async fn select_retrieved_pick_keys(
17151726
FROM {source_table} s \
17161727
LEFT JOIN {target_table} t ON t.memory_id = s.memory_id \
17171728
WHERE s.user_id = ? AND s.is_active = 1 \
1718-
AND MATCH(s.content) AGAINST('{safe_query}' IN BOOLEAN MODE) \
1719-
AND ({diff_clause}) \
1720-
ORDER BY ft_score DESC LIMIT ?"
1729+
AND MATCH(s.content) AGAINST('{safe_query}' IN BOOLEAN MODE) \
1730+
AND ({diff_clause}) \
1731+
ORDER BY ft_score DESC LIMIT ?"
1732+
,
1733+
source_table = ctx.source_table,
1734+
target_table = ctx.target_table
17211735
);
17221736
let rows = match sqlx::query(&sql_text)
1723-
.bind(user_id)
1737+
.bind(ctx.user_id)
17241738
.bind(fetch_k)
1725-
.fetch_all(sql.pool())
1739+
.fetch_all(ctx.sql.pool())
17261740
.await
17271741
{
17281742
Ok(rows) => rows,
@@ -1789,12 +1803,15 @@ fn map_pick_conflict(
17891803
err: MemoriaError,
17901804
source: &str,
17911805
target: &str,
1792-
selected: usize,
1806+
selected: Option<usize>,
17931807
) -> Result<Value, MemoriaError> {
17941808
let message = err.to_string();
17951809
if message.to_lowercase().contains("conflict") {
1810+
let selection_detail = selected
1811+
.map(|count| format!(" for {count} selected change(s)"))
1812+
.unwrap_or_default();
17961813
return Err(MemoriaError::Validation(format!(
1797-
"Conflict: pick from branch '{source}' into '{target}' aborted for {selected} selected change(s): {message}"
1814+
"Conflict: pick from branch '{source}' into '{target}' aborted{selection_detail}: {message}"
17981815
)));
17991816
}
18001817
Err(err)
@@ -1806,7 +1823,10 @@ fn mcp_text(text: &str) -> Value {
18061823

18071824
#[cfg(test)]
18081825
mod tests {
1809-
use super::{safety_prefix, snap_internal, validate_identifier, MAX_IDENTIFIER_LEN};
1826+
use super::{
1827+
normalize_keys, safety_prefix, snap_internal, validate_identifier, MAX_IDENTIFIER_LEN,
1828+
MAX_PICK_KEYS,
1829+
};
18101830

18111831
#[test]
18121832
fn scoped_snapshot_internal_names_stay_within_matrixone_limit() {
@@ -1825,6 +1845,15 @@ mod tests {
18251845
assert!(prefix.len() < MAX_IDENTIFIER_LEN, "{prefix}");
18261846
}
18271847

1848+
#[test]
1849+
fn pick_key_list_rejects_too_many_keys() {
1850+
let keys = (0..=MAX_PICK_KEYS)
1851+
.map(|idx| format!("memory_{idx}"))
1852+
.collect::<Vec<_>>();
1853+
let err = normalize_keys(keys).expect_err("should reject oversized key lists");
1854+
assert!(err.to_string().contains("at most"), "{err}");
1855+
}
1856+
18281857
#[test]
18291858
fn validate_identifier_accepts_safe_names() {
18301859
assert_eq!(validate_identifier("br_valid_123").unwrap(), "br_valid_123");

0 commit comments

Comments
 (0)