feat(embedding,vision): candle embeddings with offline fallback, on-demand clipboard vision capture, and concurrency audit

This commit is contained in:
Riz Ashraf committed 2026-10-07 06:36:09 +01:00
1 parent 5bd8b1587a
commit e4a0fe72df
47 files changed
+6292 -3503

No files matched your search

+250 -57
View File
@@ -49,6 +49,11 @@ impl McpTool for LogDecisionHandler {
status: "accepted".to_string(),
supersedes: req.supersedes,
timestamp: crate::handlers::utils::now_secs(),
namespace: req.namespace.unwrap_or_else(|| crate::models::default_namespace()),
repo_name: req.repo_name,
alternatives_considered: req.alternatives_considered.unwrap_or_default(),
affected_components: req.affected_components.unwrap_or_default(),
author: req.author,
};
adrs_to_index.push(a.clone());
@@ -201,7 +206,11 @@ impl McpTool for LogErrorFixHandler {
git_commit: req.git_commit,
git_branch: req.git_branch,
embedding,
})
..Default::default()
});
if fixes.len() > 300 {
fixes.remove(0);
}
});
state.record_activity("error_fix", &format!("Fixed error: {}", req.signature), Some(&solution));
Ok(format!("Logged error fix for {}: {}", req.signature, solution))
@@ -238,8 +247,13 @@ impl McpTool for SearchErrorFixesHandler {
.iter()
.map(|f| {
let mut score = 0.0;
if st.contains(&f.signature) || f.signature.contains(st) {
let st_lower = st.to_lowercase();
let sig_lower = f.signature.to_lowercase();
let sol_lower = f.solution.to_lowercase();
if st_lower.contains(&sig_lower) || sig_lower.contains(&st_lower) {
score += 0.8;
} else if st_lower.contains(&sol_lower) || sol_lower.contains(&st_lower) {
score += 0.5;
}
if let Some(emb) = &f.embedding {
if !query_emb.is_empty() {
@@ -343,16 +357,65 @@ impl McpTool for LogCodeChangeHandler {
}
}
}
let change_kind = match req.change_kind.as_deref().map(|s| s.to_lowercase()).as_deref() {
Some("added") | Some("create") | Some("created") | Some("new") => crate::models::ChangeKind::Added,
Some("deleted") | Some("remove") | Some("removed") => crate::models::ChangeKind::Deleted,
Some("renamed") | Some("move") | Some("moved") => crate::models::ChangeKind::Renamed,
_ => crate::models::ChangeKind::Modified,
};
let namespace = req
.namespace
.filter(|ns| !ns.trim().is_empty())
.or_else(|| req.repo_name.clone().filter(|rn| !rn.trim().is_empty()))
.unwrap_or_else(crate::models::default_namespace);
let symbols = req.symbol_references.clone().unwrap_or_default();
let line_range = req.line_range.clone();
let effective_rev = req.revision.clone().or_else(|| req.git_commit.clone());
let effective_branch = req.branch.clone().or_else(|| req.git_branch.clone());
let detected_vcs = if let Some(vcs) = req.vcs_type.clone() {
Some(vcs)
} else if let Some(ref rev) = effective_rev {
if rev.starts_with('r') && rev[1..].chars().all(|c| c.is_ascii_digit()) {
Some("svn".to_string())
} else if req.repo_url.as_deref().map(|u| u.contains("/svn/")).unwrap_or(false) {
Some("svn".to_string())
} else {
Some("git".to_string())
}
} else if let Some(ref br) = effective_branch {
if br.eq_ignore_ascii_case("trunk") || br.starts_with("branches/") || br.starts_with("tags/") {
Some("svn".to_string())
} else {
Some("git".to_string())
}
} else {
None
};
state.code.ledger.modify(|ledger| {
ledger.push(CodeChange {
timestamp: crate::handlers::utils::now_secs(),
file_path: req.file_path.clone(),
description: description.clone(),
git_commit: req.git_commit,
git_branch: req.git_branch,
git_commit: effective_rev.clone(),
git_branch: effective_branch.clone(),
repo_name: req.repo_name,
repo_url: req.repo_url,
namespace: namespace.clone(),
change_kind,
symbols,
line_range,
author: req.author,
session_id: req.session_id,
vcs_type: detected_vcs,
revision: effective_rev,
branch: effective_branch,
repository_root: req.repository_root,
});
if ledger.len() > 1000 {
if ledger.len() > 500 {
ledger.remove(0);
}
});
@@ -376,15 +439,44 @@ impl McpTool for QueryRecentChangesHandler {
)
}
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let data = state
.code
.ledger
.read_with(|l| Ok::<String, crate::error::AppError>(serde_json::to_string(l)?))?;
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: QueryRecentChangesTool = serde_json::from_value(args).unwrap_or(QueryRecentChangesTool {
namespace: None,
repo_name: None,
vcs_type: None,
limit: None,
});
let limit = req.limit.unwrap_or(50);
let data = state.code.ledger.read_with(|l| {
let filtered: Vec<_> = l
.iter()
.rev()
.filter(|c| {
if let Some(ns) = &req.namespace {
if !c.namespace.eq_ignore_ascii_case(ns) {
return false;
}
}
if let Some(repo) = &req.repo_name {
if c.repo_name.as_ref().map(|rn| !rn.eq_ignore_ascii_case(repo)).unwrap_or(true) {
return false;
}
}
if let Some(vcs) = &req.vcs_type {
if !c.effective_vcs().eq_ignore_ascii_case(vcs) {
return false;
}
}
true
})
.take(limit)
.cloned()
.collect();
Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
})?;
Ok(data)
}
}
pub struct LearnPreferenceHandler;
#[async_trait]
@@ -409,6 +501,7 @@ impl McpTool for LearnPreferenceHandler {
key: req.key.clone(),
value: req.value,
updated_at: crate::handlers::utils::now_secs(),
..Default::default()
},
);
});
@@ -474,6 +567,12 @@ impl McpTool for LogTechDebtHandler {
git_commit: req.git_commit,
git_branch: req.git_branch,
embedding,
repo_name: req.repo_name,
severity: req.severity,
file_path: req.file_path,
line_range: req.line_range,
workaround: req.workaround,
effort_estimate: req.effort_estimate,
})
});
Ok("Tech debt logged".to_string())
@@ -605,7 +704,11 @@ impl McpTool for DecisionsHandler {
"context": context,
"decision": decision,
"consequences": consequences,
"namespace": ns
"namespace": ns,
"repo_name": req.repo_name,
"author": req.author,
"affected_components": req.affected_components,
"alternatives_considered": req.alternatives_considered,
}),
state,
).await
@@ -655,7 +758,12 @@ impl McpTool for TechDebtHandler {
"git_branch": req.git_branch,
"symbol_references": req.symbol_references,
"line_range": req.line_range,
"namespace": ns
"namespace": ns,
"repo_name": req.repo_name,
"severity": req.severity,
"file_path": req.file_path,
"workaround": req.workaround,
"effort_estimate": req.effort_estimate,
}),
state,
).await
@@ -699,23 +807,12 @@ impl McpTool for OmniSearchHandler {
.search(&req.query, req.namespace.as_deref())
.unwrap_or_default();
let q = req.query.clone();
let query_emb = crate::embedding::generate_embedding_async(q.clone())
let vector_matches = state
.search()
.semantic_search(&req.query, req.namespace.as_deref(), limit * 2)
.await
.unwrap_or_default();
let vector_matches = if !query_emb.is_empty() {
if let Some(vdb) = &*state.vector_db.read().await {
vdb.search(query_emb.clone(), (limit * 2) as u64)
.await
.unwrap_or_default()
} else {
vec![]
}
} else {
vec![]
};
// Reciprocal Rank Fusion (RRF) algorithm
#[allow(dead_code)]
#[derive(Clone)]
@@ -754,8 +851,8 @@ impl McpTool for OmniSearchHandler {
let item = MatchItem {
id: v_match.id.clone(),
doc_type: v_match.doc_type,
title: v_match.id,
body: v_match.text,
title: v_match.title,
body: v_match.body,
};
rrf_scores.insert(item_id, (score, item));
}
@@ -770,29 +867,38 @@ impl McpTool for OmniSearchHandler {
let kg_json = state.read_graph(|full| {
let mut kg_results = serde_json::Map::new();
let mut count = 0;
// Build pre-indexed adjacency map: O(R) once instead of O(E * R)
let mut adj_map: std::collections::HashMap<&str, Vec<(&str, &str, &str)>> =
std::collections::HashMap::new();
for rel in &full.relations {
adj_map
.entry(rel.from.as_str())
.or_default()
.push((rel.to.as_str(), rel.relation_type.as_str(), "outgoing"));
adj_map
.entry(rel.to.as_str())
.or_default()
.push((rel.from.as_str(), rel.relation_type.as_str(), "incoming"));
}
for res in &matches {
if res.doc_type == "entity"
&& let Some(e) = full.entities.get(&res.id)
{
if count >= limit {
continue;
break;
}
count += 1;
// 1-hop relation expansion for GraphRAG
// 1-hop relation expansion for GraphRAG via pre-indexed adjacency
let mut connected_rels = Vec::new();
for rel in &full.relations {
if rel.from == res.id {
if let Some(rels) = adj_map.get(res.id.as_str()) {
for (target, rel_type, direction) in rels {
connected_rels.push(serde_json::json!({
"target": rel.to,
"relation": rel.relation_type,
"direction": "outgoing"
}));
} else if rel.to == res.id {
connected_rels.push(serde_json::json!({
"target": rel.from,
"relation": rel.relation_type,
"direction": "incoming"
"target": target,
"relation": rel_type,
"direction": direction
}));
}
}
@@ -851,6 +957,11 @@ impl McpTool for OmniSearchHandler {
serde_json::to_value(&filtered).map_err(|e| e.to_string())
})?;
let q = req.query.clone();
let query_emb = crate::embedding::generate_embedding_async(req.query.clone())
.await
.unwrap_or_default();
let snippets_json = state.code.snippets.read_with(|all_snippets| {
let mut scored: Vec<_> = all_snippets
.iter()
@@ -907,7 +1018,6 @@ impl McpTool for OmniSearchHandler {
serde_json::to_value(&filtered).map_err(|e| e.to_string())
})?;
let q = req.query;
let tech_debts_json = state.code.tech_debts.read_with(|debts| {
let mut scored: Vec<_> = debts
.iter()
@@ -995,7 +1105,7 @@ impl McpTool for OmniSearchHandler {
serde_json::to_value(&filtered).map_err(|e| e.to_string())
})?;
let report = serde_json::json!({
let mut report = serde_json::json!({
"knowledge_graph": kg_json,
"tasks": tasks_json,
"snippets": snippets_json,
@@ -1004,15 +1114,52 @@ impl McpTool for OmniSearchHandler {
"handoff_memos": memos_json,
"error_fixes": error_fixes_json
});
let mut out_str = report.to_string();
if let Some(max_tok) = req.max_tokens {
let max_chars = max_tok * 4;
let mut out_str = report.to_string();
if out_str.len() > max_chars {
out_str.truncate(max_chars);
out_str.push_str("... [TRUNCATED_TO_MAX_TOKENS]");
let prune_keys = [
"error_fixes",
"tech_debts",
"snippets",
"adrs",
"handoff_memos",
"knowledge_graph",
"tasks",
];
let mut pruned = false;
for key in prune_keys {
while out_str.len() > max_chars {
let popped = if let Some(arr) = report.get_mut(key).and_then(|v| v.as_array_mut()) {
if arr.len() > 1 {
arr.pop();
pruned = true;
true
} else {
false
}
} else {
false
};
if popped {
out_str = report.to_string();
} else {
break;
}
}
if out_str.len() <= max_chars {
break;
}
}
if pruned && let Some(obj) = report.as_object_mut() {
obj.insert(
"_truncated_to_max_tokens".to_string(),
serde_json::Value::Bool(true),
);
}
}
}
Ok(out_str)
Ok(report.to_string())
}
}
@@ -1101,8 +1248,13 @@ impl McpTool for SuggestErrorFixHandler {
.iter()
.map(|f| {
let mut score = 0.0;
if req.stack_trace.contains(&f.signature) || f.signature.contains(&req.stack_trace) {
let st_lower = req.stack_trace.to_lowercase();
let sig_lower = f.signature.to_lowercase();
let sol_lower = f.solution.to_lowercase();
if st_lower.contains(&sig_lower) || sig_lower.contains(&st_lower) {
score += 0.8;
} else if st_lower.contains(&sol_lower) || sol_lower.contains(&st_lower) {
score += 0.5;
}
if let Some(emb) = &f.embedding {
if !query_emb.is_empty() {
@@ -1178,6 +1330,7 @@ impl McpTool for ManageCheckpointHandler {
timestamp: crate::handlers::utils::now_secs(),
description: desc.clone(),
namespace: ns,
..Default::default()
};
state.project.snapshots.modify(|snaps| snaps.push(snapshot));
}
@@ -1381,22 +1534,31 @@ impl McpTool for QueryLineageHandler {
"decision": adr.decision
}));
}
}
let changes = state.code.ledger.read_with(|c| c.clone());
for change in changes {
let file_match = change.file_path.to_lowercase().contains(&q);
let desc_match = change.description.to_lowercase().contains(&q);
if file_match || desc_match {
let repo_match = change.repo_name.as_ref().map(|r| r.to_lowercase().contains(&q)).unwrap_or(false);
let symbol_match = change.symbols.iter().any(|s| s.to_lowercase().contains(&q));
let ns_match = change.namespace.to_lowercase().contains(&q);
if file_match || desc_match || repo_match || symbol_match || ns_match {
timeline.push(serde_json::json!({
"timestamp": change.timestamp,
"type": "CodeChange",
"file": change.file_path,
"description": change.description,
"commit": change.git_commit
"commit": change.git_commit,
"branch": change.git_branch,
"repo_name": change.repo_name,
"namespace": change.namespace,
"change_kind": change.change_kind,
"symbols": change.symbols,
"line_range": change.line_range,
"author": change.author,
}));
}
}
}
let fixes = state.code.error_fixes.read_with(|f| f.clone());
for fix in fixes {
@@ -1509,6 +1671,7 @@ impl McpTool for LogHypothesisHandler {
status: req.status.unwrap_or_else(|| "unverified".to_string()),
evidence: req.evidence,
timestamp,
..Default::default()
};
state.code.hypotheses.modify(|h| h.push(record));
@@ -1576,10 +1739,16 @@ impl McpTool for GetPreflightContextHandler {
let pinned = state.project.pinned_files.read_with(|p| p.clone());
let tech_debts = state.code.tech_debts.read_with(|d| d.clone());
let hypotheses = state.code.hypotheses.read_with(|h| h.clone());
let recent_commands = state.telemetry.terminal_history.read_with(|h| {
h.iter().take(5).cloned().collect::<Vec<_>>()
});
let recent_activities = state.telemetry.recent_activities.read_with(|a| {
a.iter().take(5).cloned().collect::<Vec<_>>()
});
let active_tasks: Vec<_> = tasks
.into_iter()
.filter(|t| t.status == "in_progress" || t.status == "open")
.filter(|t| !matches!(t.status.as_str(), "completed" | "done" | "cancelled"))
.map(|t| serde_json::json!({
"id": t.id,
"title": t.title,
@@ -1590,7 +1759,12 @@ impl McpTool for GetPreflightContextHandler {
let branch_pinned: Vec<_> = pinned
.into_iter()
.filter(|p| p.namespace == req.namespace && (req.git_branch.is_none() || p.git_branch == req.git_branch))
.filter(|p| {
p.namespace == req.namespace
&& (req.git_branch.is_none()
|| p.git_branch.is_none()
|| p.git_branch == req.git_branch)
})
.map(|p| p.file_path)
.collect();
@@ -1617,7 +1791,9 @@ impl McpTool for GetPreflightContextHandler {
"active_tasks": active_tasks,
"pinned_files": branch_pinned,
"top_open_tech_debts": open_tech_debts,
"active_hypotheses": active_hypotheses
"active_hypotheses": active_hypotheses,
"recent_terminal_commands": recent_commands,
"recent_activities": recent_activities
});
Ok(serde_json::to_string_pretty(&preflight)?)
@@ -1652,6 +1828,7 @@ impl McpTool for BroadcastAgentSignalHandler {
payload: req.payload,
timestamp,
ttl_seconds: req.ttl_seconds,
..Default::default()
};
state.telemetry.agent_signals.modify(|s| {
@@ -1747,7 +1924,11 @@ impl McpTool for AutoSessionCheckpointHandler {
let hypotheses = state.code.hypotheses.read_with(|h| h.clone());
let ledger = state.code.ledger.read_with(|l| l.clone());
let active_tasks: Vec<_> = tasks.iter().filter(|t| t.status == "in_progress" || t.status == "open").map(|t| t.title.as_str()).collect();
let active_tasks: Vec<_> = tasks
.iter()
.filter(|t| !matches!(t.status.as_str(), "completed" | "done" | "cancelled"))
.map(|t| t.title.as_str())
.collect();
let unverified_hyp: Vec<_> = hypotheses.iter().filter(|h| h.status == "unverified").map(|h| h.hypothesis.as_str()).collect();
let recent_changes: Vec<_> = ledger.iter().rev().take(5).map(|c| c.file_path.as_str()).collect();
@@ -1765,6 +1946,7 @@ impl McpTool for AutoSessionCheckpointHandler {
expires_at: None,
namespace: req.namespace,
timestamp,
..Default::default()
};
state.telemetry.handoff_memos.modify(|m| {
@@ -2036,6 +2218,7 @@ mod tests {
expires_at: None,
dependencies: vec![],
acceptance_criteria: vec![],
..Default::default()
};
{
@@ -2232,6 +2415,7 @@ mod tests {
expires_at: None,
dependencies: vec![],
acceptance_criteria: vec![],
..Default::default()
};
state.project.tasks.modify(|t| t.push(task));
@@ -2408,6 +2592,7 @@ mod tests {
tags: vec!["rust".to_string(), "mcp".to_string()],
updated_at: 0,
embedding: None,
..Default::default()
});
});
@@ -2467,6 +2652,7 @@ mod tests {
expires_at: None,
dependencies: vec![],
acceptance_criteria: vec![],
..Default::default()
};
let task2 = crate::models::Task {
id: "t-2".to_string(),
@@ -2480,6 +2666,7 @@ mod tests {
expires_at: None,
dependencies: vec!["t-1".to_string()],
acceptance_criteria: vec![],
..Default::default()
};
state.project.tasks.modify(|t| {
t.push(task1);
@@ -2607,6 +2794,7 @@ mod tests {
observations: vec!["Obs 1".to_string()],
namespace: "global".to_string(),
git_branch: None,
..Default::default()
});
g.entities.insert("Ent2".to_string(), crate::models::Entity {
name: "Ent2".to_string(),
@@ -2614,12 +2802,14 @@ mod tests {
observations: vec!["Obs 2".to_string()],
namespace: "global".to_string(),
git_branch: None,
..Default::default()
});
g.relations.push(crate::models::Relation {
from: "Ent1".to_string(),
to: "Ent2".to_string(),
relation_type: "uses".to_string(),
namespace: "global".to_string(),
..Default::default()
});
});
@@ -2693,6 +2883,7 @@ mod tests {
timestamp: 0,
description: "Test snap".to_string(),
namespace: "global".to_string(),
..Default::default()
});
});
let rest_snap = mg_chk
@@ -2737,6 +2928,7 @@ mod tests {
dependencies: vec!["NON-EXISTENT-TASK".to_string()],
description: "Blocked task desc".to_string(),
expires_at: None,
..Default::default()
});
@@ -2756,6 +2948,7 @@ mod tests {
timestamp: 0,
namespace: "global".to_string(),
git_branch: Some("main".to_string()),
..Default::default()
});
});