Skip to content

Commit 60cc0fd

Browse files
committed
fix: hybrid search column mismatch, clamp limits, bound queries
- Fix critical bug: search_hybrid FTS query was missing updated_at column, causing row_to_memory to misparse all fields and silently fail - Clamp all user-facing limit params to 1..100 (prevent i64→usize wrap) - Clamp depth to 1..3 in memoir_inspect, min_cluster_size to 2..50 - Add LIMIT 10000 to list_all(), LIMIT 500 to get_by_topic_prefix() - Use collect_rows() in get_by_topic_prefix
1 parent b1d9e63 commit 60cc0fd

2 files changed

Lines changed: 11 additions & 14 deletions

File tree

crates/icm-mcp/src/tools.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -734,7 +734,7 @@ fn tool_recall(
734734
Some(q) => q,
735735
None => return ToolResult::error("missing required field: query".into()),
736736
};
737-
let limit = get_i64(args, "limit", 5) as usize;
737+
let limit = get_i64(args, "limit", 5).clamp(1, 100) as usize;
738738
let topic = get_str(args, "topic");
739739
let keyword = get_str(args, "keyword");
740740

@@ -1003,7 +1003,7 @@ fn tool_extract_patterns(store: &SqliteStore, args: &Value) -> ToolResult {
10031003
Some(t) => t,
10041004
None => return ToolResult::error("missing required field: topic".into()),
10051005
};
1006-
let min_cluster_size = get_i64(args, "min_cluster_size", 3) as usize;
1006+
let min_cluster_size = get_i64(args, "min_cluster_size", 3).clamp(2, 50) as usize;
10071007
let memoir_name = get_str(args, "memoir");
10081008

10091009
let patterns = match store.detect_patterns(topic, min_cluster_size) {
@@ -1317,7 +1317,7 @@ fn tool_memoir_search(store: &SqliteStore, args: &Value) -> ToolResult {
13171317
Some(q) => q,
13181318
None => return ToolResult::error("missing required field: query".into()),
13191319
};
1320-
let limit = get_i64(args, "limit", 10) as usize;
1320+
let limit = get_i64(args, "limit", 10).clamp(1, 100) as usize;
13211321
let label_str = get_str(args, "label");
13221322

13231323
let memoir = match resolve_memoir(store, memoir_name) {
@@ -1378,7 +1378,7 @@ fn tool_memoir_search_all(store: &SqliteStore, args: &Value) -> ToolResult {
13781378
Some(q) => q,
13791379
None => return ToolResult::error("missing required field: query".into()),
13801380
};
1381-
let limit = get_i64(args, "limit", 10) as usize;
1381+
let limit = get_i64(args, "limit", 10).clamp(1, 100) as usize;
13821382

13831383
let results = match store.search_all_concepts_fts(query, limit) {
13841384
Ok(r) => r,
@@ -1476,7 +1476,7 @@ fn tool_memoir_inspect(store: &SqliteStore, args: &Value) -> ToolResult {
14761476
Some(n) => n,
14771477
None => return ToolResult::error("missing required field: name".into()),
14781478
};
1479-
let depth = get_i64(args, "depth", 1) as usize;
1479+
let depth = get_i64(args, "depth", 1).clamp(1, 3) as usize;
14801480

14811481
let memoir = match resolve_memoir(store, memoir_name) {
14821482
Ok(m) => m,
@@ -1814,7 +1814,7 @@ fn tool_feedback_search(store: &SqliteStore, args: &Value) -> ToolResult {
18141814
None => return ToolResult::error("missing required field: query".into()),
18151815
};
18161816
let topic = get_str(args, "topic");
1817-
let limit = get_i64(args, "limit", 5) as usize;
1817+
let limit = get_i64(args, "limit", 5).clamp(1, 100) as usize;
18181818

18191819
match store.search_feedback(query, topic, limit) {
18201820
Ok(results) => {

crates/icm-store/src/store.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,8 @@ impl MemoryStore for SqliteStore {
513513
let sanitized = sanitize_fts_query(query);
514514

515515
// 1. Get FTS results with rank scores
516-
let fts_sql = "SELECT m.id, m.created_at, m.last_accessed, m.access_count, m.weight, \
516+
let fts_sql =
517+
"SELECT m.id, m.created_at, m.updated_at, m.last_accessed, m.access_count, m.weight, \
517518
m.topic, m.summary, m.raw_excerpt, m.keywords, \
518519
m.importance, m.source_type, m.source_data, m.related_ids, m.embedding, \
519520
fts.rank \
@@ -678,7 +679,7 @@ impl MemoryStore for SqliteStore {
678679
let mut stmt = self
679680
.conn
680681
.prepare(&format!(
681-
"SELECT {SELECT_COLS} FROM memories ORDER BY weight DESC"
682+
"SELECT {SELECT_COLS} FROM memories ORDER BY weight DESC LIMIT 10000"
682683
))
683684
.map_err(db_err)?;
684685

@@ -1706,19 +1707,15 @@ impl SqliteStore {
17061707
let mut stmt = self
17071708
.conn
17081709
.prepare(&format!(
1709-
"SELECT {SELECT_COLS} FROM memories WHERE topic LIKE ?1 ORDER BY weight DESC"
1710+
"SELECT {SELECT_COLS} FROM memories WHERE topic LIKE ?1 ORDER BY weight DESC LIMIT 500"
17101711
))
17111712
.map_err(db_err)?;
17121713

17131714
let rows = stmt
17141715
.query_map(params![pattern], row_to_memory)
17151716
.map_err(db_err)?;
17161717

1717-
let mut results = Vec::new();
1718-
for row in rows {
1719-
results.push(row.map_err(db_err)?);
1720-
}
1721-
Ok(results)
1718+
collect_rows(rows)
17221719
} else {
17231720
self.get_by_topic(topic)
17241721
}

0 commit comments

Comments
 (0)