Skip to content

Commit afc8f4a

Browse files
authored
fix(mcp): dedupe reasoning mirrors, rename harness→mode, improve errors & docs (#199)
1 parent 1b8983e commit afc8f4a

3 files changed

Lines changed: 139 additions & 17 deletions

File tree

apps/moraine/src/main.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2799,6 +2799,38 @@ async fn main() -> Result<ExitCode> {
27992799
#[cfg(test)]
28002800
mod tests {
28012801
use super::*;
2802+
use std::ffi::OsString;
2803+
use std::sync::{Mutex, MutexGuard};
2804+
2805+
static ENV_VAR_LOCK: Mutex<()> = Mutex::new(());
2806+
2807+
struct EnvVarGuard {
2808+
key: &'static str,
2809+
original: Option<OsString>,
2810+
}
2811+
2812+
impl EnvVarGuard {
2813+
fn capture(key: &'static str) -> Self {
2814+
Self {
2815+
key,
2816+
original: std::env::var_os(key),
2817+
}
2818+
}
2819+
}
2820+
2821+
impl Drop for EnvVarGuard {
2822+
fn drop(&mut self) {
2823+
if let Some(value) = &self.original {
2824+
std::env::set_var(self.key, value);
2825+
} else {
2826+
std::env::remove_var(self.key);
2827+
}
2828+
}
2829+
}
2830+
2831+
fn lock_env_vars() -> MutexGuard<'static, ()> {
2832+
ENV_VAR_LOCK.lock().expect("env-var lock poisoned")
2833+
}
28022834

28032835
fn temp_dir(name: &str) -> PathBuf {
28042836
let stamp = SystemTime::now()
@@ -3126,6 +3158,10 @@ mod tests {
31263158

31273159
#[test]
31283160
fn resolve_service_binary_prefers_env_then_config() {
3161+
let _env_lock = lock_env_vars();
3162+
let _service_bin_dir_guard = EnvVarGuard::capture("MORAINE_SERVICE_BIN_DIR");
3163+
let _source_tree_mode_guard = EnvVarGuard::capture("MORAINE_SOURCE_TREE_MODE");
3164+
31293165
let root = temp_dir("resolver");
31303166
let env_dir = root.join("env");
31313167
let cfg_dir = root.join("cfg");
@@ -3156,6 +3192,10 @@ mod tests {
31563192

31573193
#[test]
31583194
fn resolve_service_binary_reports_missing_without_path_fallback() {
3195+
let _env_lock = lock_env_vars();
3196+
let _service_bin_dir_guard = EnvVarGuard::capture("MORAINE_SERVICE_BIN_DIR");
3197+
let _source_tree_mode_guard = EnvVarGuard::capture("MORAINE_SOURCE_TREE_MODE");
3198+
31593199
let root = temp_dir("resolver-path");
31603200
let mut cfg = AppConfig::default();
31613201
cfg.runtime.service_bin_dir = root.join("missing").to_string_lossy().to_string();
@@ -3177,6 +3217,10 @@ mod tests {
31773217

31783218
#[test]
31793219
fn require_service_binary_includes_remediation() {
3220+
let _env_lock = lock_env_vars();
3221+
let _service_bin_dir_guard = EnvVarGuard::capture("MORAINE_SERVICE_BIN_DIR");
3222+
let _source_tree_mode_guard = EnvVarGuard::capture("MORAINE_SOURCE_TREE_MODE");
3223+
31803224
let root = temp_dir("resolver-remediation");
31813225
let mut cfg = AppConfig::default();
31823226
cfg.runtime.service_bin_dir = root.join("missing").to_string_lossy().to_string();

crates/moraine-conversations/src/clickhouse_repo.rs

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1600,13 +1600,25 @@ FORMAT JSONEachRow",
16001600
|| row.payload_type == "event_msg")
16011601
}
16021602

1603+
fn is_reasoning_search_row(row: &SearchRow) -> bool {
1604+
row.event_class == "reasoning"
1605+
}
1606+
1607+
fn is_event_msg_reasoning_search_row(row: &SearchRow) -> bool {
1608+
row.event_class == "event_msg" && row.payload_type == "agent_reasoning"
1609+
}
1610+
16031611
fn compact_preview_for_dedup(text: &str) -> String {
16041612
text.split_whitespace().collect::<Vec<_>>().join(" ")
16051613
}
16061614

16071615
fn search_rows_are_mirrors(a: &SearchRow, b: &SearchRow) -> bool {
1608-
let same_kind_pair = (Self::is_message_search_row(a) && Self::is_event_msg_search_row(b))
1616+
let is_message_pair = (Self::is_message_search_row(a) && Self::is_event_msg_search_row(b))
16091617
|| (Self::is_event_msg_search_row(a) && Self::is_message_search_row(b));
1618+
let is_reasoning_pair = (Self::is_reasoning_search_row(a)
1619+
&& Self::is_event_msg_reasoning_search_row(b))
1620+
|| (Self::is_event_msg_reasoning_search_row(a) && Self::is_reasoning_search_row(b));
1621+
let same_kind_pair = is_message_pair || is_reasoning_pair;
16101622
if !same_kind_pair {
16111623
return false;
16121624
}
@@ -1627,9 +1639,10 @@ FORMAT JSONEachRow",
16271639
}
16281640

16291641
fn search_row_kind_priority(row: &SearchRow) -> u8 {
1630-
if Self::is_message_search_row(row) {
1642+
if Self::is_message_search_row(row) || Self::is_reasoning_search_row(row) {
16311643
0
1632-
} else if Self::is_event_msg_search_row(row) {
1644+
} else if Self::is_event_msg_search_row(row) || Self::is_event_msg_reasoning_search_row(row)
1645+
{
16331646
1
16341647
} else {
16351648
2
@@ -3088,7 +3101,9 @@ FORMAT JSONEachRow",
30883101

30893102
let terms_with_qf = tokenize_query(query_text, self.cfg.bm25_max_query_terms);
30903103
if terms_with_qf.is_empty() {
3091-
return Err(RepoError::invalid_argument("query has no searchable terms"));
3104+
return Err(RepoError::invalid_argument(
3105+
"query has no searchable terms (tokens shorter than 2 characters are excluded)",
3106+
));
30923107
}
30933108
let terms: Vec<String> = terms_with_qf.iter().map(|(term, _)| term.clone()).collect();
30943109

@@ -3252,7 +3267,9 @@ FORMAT JSONEachRow",
32523267

32533268
let terms_with_qf = tokenize_query(query_text, self.cfg.bm25_max_query_terms);
32543269
if terms_with_qf.is_empty() {
3255-
return Err(RepoError::invalid_argument("query has no searchable terms"));
3270+
return Err(RepoError::invalid_argument(
3271+
"query has no searchable terms (tokens shorter than 2 characters are excluded)",
3272+
));
32563273
}
32573274
let terms: Vec<String> = terms_with_qf.iter().map(|(term, _)| term.clone()).collect();
32583275

@@ -3799,6 +3816,66 @@ mod tests {
37993816
assert_eq!(deduped.len(), 2);
38003817
}
38013818

3819+
#[test]
3820+
fn dedupe_search_rows_prefers_reasoning_over_event_msg_reasoning_mirror() {
3821+
let rows = vec![
3822+
sample_search_row(
3823+
"uid-event-msg-reasoning",
3824+
"sess-a",
3825+
"event_msg",
3826+
"agent_reasoning",
3827+
"assistant",
3828+
"Let me think about this",
3829+
12.50,
3830+
2,
3831+
),
3832+
sample_search_row(
3833+
"uid-reasoning",
3834+
"sess-a",
3835+
"reasoning",
3836+
"reasoning",
3837+
"assistant",
3838+
"Let me think about this",
3839+
12.50,
3840+
2,
3841+
),
3842+
];
3843+
3844+
let deduped = ClickHouseConversationRepository::dedupe_search_rows(rows, 5);
3845+
assert_eq!(deduped.len(), 1);
3846+
assert_eq!(deduped[0].event_uid, "uid-reasoning");
3847+
assert_eq!(deduped[0].event_class, "reasoning");
3848+
}
3849+
3850+
#[test]
3851+
fn dedupe_search_rows_reasoning_mirrors_do_not_collapse_with_messages() {
3852+
let rows = vec![
3853+
sample_search_row(
3854+
"uid-reasoning",
3855+
"sess-a",
3856+
"reasoning",
3857+
"reasoning",
3858+
"assistant",
3859+
"same text",
3860+
10.0,
3861+
2,
3862+
),
3863+
sample_search_row(
3864+
"uid-message",
3865+
"sess-a",
3866+
"message",
3867+
"message",
3868+
"assistant",
3869+
"same text",
3870+
10.0,
3871+
2,
3872+
),
3873+
];
3874+
3875+
let deduped = ClickHouseConversationRepository::dedupe_search_rows(rows, 5);
3876+
assert_eq!(deduped.len(), 2);
3877+
}
3878+
38023879
#[test]
38033880
fn low_information_system_event_classifier_targets_open_noise() {
38043881
assert!(

crates/moraine-mcp-core/src/lib.rs

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ struct SessionListProseSession {
303303
#[serde(default)]
304304
event_count: u64,
305305
#[serde(default)]
306-
harness_type: String,
306+
mode: String,
307307
}
308308

309309
#[derive(Clone)]
@@ -387,15 +387,15 @@ impl AppState {
387387
"tools": [
388388
{
389389
"name": "search",
390-
"description": "BM25 lexical search over Moraine indexed conversation events.",
390+
"description": "BM25 lexical search over Moraine indexed conversation events. Bag-of-words ranking: no phrase matching, no stemming. Word order does not matter.",
391391
"inputSchema": {
392392
"type": "object",
393393
"properties": {
394394
"query": { "type": "string" },
395395
"limit": { "type": "integer", "minimum": limit_min, "maximum": limit_max },
396396
"session_id": { "type": "string" },
397397
"min_score": { "type": "number" },
398-
"min_should_match": { "type": "integer", "minimum": 1 },
398+
"min_should_match": { "type": "integer", "minimum": 1, "description": "Minimum number of query terms that must match. Values exceeding the number of query terms are clamped." },
399399
"include_tool_events": { "type": "boolean" },
400400
"event_kind": {
401401
"oneOf": [
@@ -452,7 +452,7 @@ impl AppState {
452452
"query": { "type": "string" },
453453
"limit": { "type": "integer", "minimum": limit_min, "maximum": limit_max },
454454
"min_score": { "type": "number" },
455-
"min_should_match": { "type": "integer", "minimum": 1 },
455+
"min_should_match": { "type": "integer", "minimum": 1, "description": "Minimum number of query terms that must match. Values exceeding the number of query terms are clamped." },
456456
"from_unix_ms": { "type": "integer" },
457457
"to_unix_ms": { "type": "integer" },
458458
"mode": {
@@ -483,7 +483,8 @@ impl AppState {
483483
"to_unix_ms": { "type": "integer" },
484484
"mode": {
485485
"type": "string",
486-
"enum": ["web_search", "mcp_internal", "tool_calling", "chat"]
486+
"enum": ["web_search", "mcp_internal", "tool_calling", "chat"],
487+
"description": SEARCH_CONVERSATIONS_MODE_DOC
487488
},
488489
"verbosity": {
489490
"type": "string",
@@ -665,7 +666,7 @@ impl AppState {
665666
"assistant_messages": summary.assistant_messages,
666667
"tool_calls": summary.tool_calls,
667668
"tool_results": summary.tool_results,
668-
"harness_type": summary.mode.as_str(),
669+
"mode": summary.mode.as_str(),
669670
})
670671
})
671672
.collect::<Vec<_>>();
@@ -991,17 +992,17 @@ fn format_session_list_prose(payload: &Value) -> Result<String> {
991992
}
992993

993994
for (idx, session) in parsed.sessions.iter().enumerate() {
994-
let harness = if session.harness_type.is_empty() {
995+
let mode = if session.mode.is_empty() {
995996
"chat"
996997
} else {
997-
session.harness_type.as_str()
998+
session.mode.as_str()
998999
};
9991000

10001001
out.push_str(&format!(
1001-
"\n{}) session={} harness={} events={}\n",
1002+
"\n{}) session={} mode={} events={}\n",
10021003
idx + 1,
10031004
session.session_id,
1004-
harness,
1005+
mode,
10051006
session.event_count
10061007
));
10071008
out.push_str(&format!(
@@ -1380,15 +1381,15 @@ mod tests {
13801381
"end_time": "2026-01-02 12:05:00",
13811382
"end_unix_ms": 1767355500000_i64,
13821383
"event_count": 22_u64,
1383-
"harness_type": "web_search"
1384+
"mode": "web_search"
13841385
}
13851386
],
13861387
"next_cursor": "cursor-token"
13871388
});
13881389

13891390
let text = format_session_list_prose(&payload).expect("format");
13901391
assert!(text.contains("session=sess-1"));
1391-
assert!(text.contains("harness=web_search"));
1392+
assert!(text.contains("mode=web_search"));
13921393
assert!(text.contains("next_cursor: cursor-token"));
13931394
}
13941395
}

0 commit comments

Comments
 (0)