refactor: eradicate deprecated tools (sticky_notes, pinned_files, context_workspaces, pr_checklist, preferences) and dead code

This commit is contained in:
Riz Ashraf committed 2026-10-07 11:12:42 +01:00
1 parent d80915635f
commit 79209da711
31 files changed
+1230 -3920

No files matched your search

+6 -2
View File
@@ -26,11 +26,15 @@ pub async fn wait_for_event_handler(
return axum::Json(event);
}
}
Err(_) => {
Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
tracing::warn!("Event bus receiver lagged by {} messages; continuing wait.", skipped);
continue;
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
return axum::Json(GenericEvent {
topic: "error".to_string(),
session_id: None,
payload: serde_json::json!({"error": "Event bus lagged or closed"}),
payload: serde_json::json!({"error": "Event bus closed"}),
});
}
}
+1 -9
View File
@@ -237,18 +237,14 @@ async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::E
let adrs = state_clone.adrs.read_with(|items| items.len());
let ledger = state_clone.ledger.read_with(|items| items.len());
let sticky = state_clone.sticky.read_with(|items| items.len());
let error_fixes = state_clone.error_fixes.read_with(|items| items.len());
let pinned_files = state_clone.pinned_files.read_with(|items| items.len());
let session_summaries = state_clone.session_summaries.read_with(|items| items.len());
let handoff_memos = state_clone.handoff_memos.read_with(|items| items.len());
let env_fingerprints = state_clone.env_fingerprints.read_with(|items| items.len());
let env_requirements = state_clone.env_requirements.read_with(|items| items.len());
let milestones = state_clone.milestones.read_with(|items| items.len());
let environments = state_clone.environments.read_with(|items| items.len());
let pr_checklists = state_clone.pr_checklists.read_with(|items| items.len());
let gates = state_clone.gates.read_with(|items| items.len());
let context_workspaces = state_clone.context_workspaces.read_with(|items| items.len());
axum::Json(serde_json::json!({
"entities": entities,
@@ -258,18 +254,14 @@ async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::E
"tech_debts": tech_debts,
"adrs": adrs,
"ledger": ledger,
"sticky": sticky,
"error_fixes": error_fixes,
"pinned_files": pinned_files,
"session_summaries": session_summaries,
"handoff_memos": handoff_memos,
"env_fingerprints": env_fingerprints,
"env_requirements": env_requirements,
"milestones": milestones,
"environments": environments,
"pr_checklists": pr_checklists,
"gates": gates,
"context_workspaces": context_workspaces
"gates": gates
}))
}
}),
+2 -42
View File
@@ -205,16 +205,7 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
}
}),
)
.route(
"/api/sticky",
get({
let state_clone = app_state.handler.state.clone();
move || async move {
let sticky_json = state_clone.code.sticky.read_with(|s| serde_json::to_string(s).unwrap_or_else(|_| "[]".to_string()));
([(axum::http::header::CONTENT_TYPE, "application/json")], sticky_json)
}
}),
)
.route(
"/api/search",
get({
@@ -264,16 +255,6 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
}
}),
)
.route(
"/api/context_workspaces",
get({
let state_clone = app_state.handler.state.clone();
move || async move {
let json = state_clone.project.context_workspaces.read_with(|items| serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string()));
([(axum::http::header::CONTENT_TYPE, "application/json")], json)
}
}),
)
.route(
"/api/handoff_memos",
get({
@@ -304,16 +285,6 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
}
}),
)
.route(
"/api/pr_checklists",
get({
let state_clone = app_state.handler.state.clone();
move || async move {
let json = state_clone.project.pr_checklists.read_with(|items| serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string()));
([(axum::http::header::CONTENT_TYPE, "application/json")], json)
}
}),
)
.route(
"/api/error_fixes",
get({
@@ -336,18 +307,14 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
let adrs = state_clone.code.adrs.read_with(|items| items.len());
let ledger = state_clone.code.ledger.read_with(|items| items.len());
let sticky = state_clone.code.sticky.read_with(|items| items.len());
let error_fixes = state_clone.code.error_fixes.read_with(|items| items.len());
let pinned_files = state_clone.project.pinned_files.read_with(|items| items.len());
let session_summaries = state_clone.telemetry.session_summaries.read_with(|items| items.len());
let handoff_memos = state_clone.telemetry.handoff_memos.read_with(|items| items.len());
let env_fingerprints = state_clone.env.env_fingerprints.read_with(|items| items.len());
let env_requirements = state_clone.env.env_requirements.read_with(|items| items.len());
let milestones = state_clone.project.milestones.read_with(|items| items.len());
let environments = state_clone.env.environments.read_with(|items| items.len());
let pr_checklists = state_clone.project.pr_checklists.read_with(|items| items.len());
let gates = state_clone.env.gates.read_with(|items| items.len());
let context_workspaces = state_clone.project.context_workspaces.read_with(|items| items.len());
axum::Json(serde_json::json!({
"entities": entities,
@@ -357,18 +324,14 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
"tech_debts": tech_debts,
"adrs": adrs,
"ledger": ledger,
"sticky": sticky,
"error_fixes": error_fixes,
"pinned_files": pinned_files,
"session_summaries": session_summaries,
"handoff_memos": handoff_memos,
"env_fingerprints": env_fingerprints,
"env_requirements": env_requirements,
"milestones": milestones,
"environments": environments,
"pr_checklists": pr_checklists,
"gates": gates,
"context_workspaces": context_workspaces
"gates": gates
}))
}
}),
@@ -640,15 +603,12 @@ mod tests {
"/",
"/api/graph",
"/api/tasks",
"/api/sticky",
"/api/search?q=test",
"/api/tech_debts",
"/api/adrs",
"/api/context_workspaces",
"/api/handoff_memos",
"/api/milestones",
"/api/snippets",
"/api/pr_checklists",
"/api/error_fixes",
"/api/stats",
];
-5
View File
@@ -127,23 +127,18 @@ pub fn init_redb(base: &Path) -> Arc<Database> {
let stores = vec![
("knowledge_graph_master", "knowledge_graph_master.json"),
("audit_ledger", "audit_ledger.json"),
("sticky_notes", "sticky_notes.json"),
("tasks", "tasks.json"),
("snippets", "snippets.json"),
("adrs", "adrs.json"),
("preferences", "preferences.json"),
("error_fixes", "error_fixes.json"),
("pinned_files", "pinned_files.json"),
("session_summaries", "session_summaries.json"),
("handoff_memos", "handoff_memos.json"),
("env_fingerprints", "env_fingerprints.json"),
("env_requirements", "env_requirements.json"),
("milestones", "milestones.json"),
("environments", "environments.json"),
("pr_checklists", "pr_checklists.json"),
("tech_debts", "tech_debts.json"),
("gates", "gates.json"),
("context_workspaces", "context_workspaces.json"),
("state_snapshots", "state_snapshots.json"),
("hypotheses", "hypotheses.json"),
("agent_signals", "agent_signals.json"),
+8 -4
View File
@@ -280,12 +280,16 @@ pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
norm_b_sq += y * y;
}
let norm_a = norm_a_sq.sqrt();
let norm_b = norm_b_sq.sqrt();
if norm_a == 0.0 || norm_b == 0.0 {
// Fast path: If vectors are already normalized (Candle & fallback embeddings), skip square roots
if (norm_a_sq - 1.0).abs() < 1e-4 && (norm_b_sq - 1.0).abs() < 1e-4 {
return dot_product.clamp(-1.0, 1.0);
}
let norm_product = norm_a_sq * norm_b_sq;
if norm_product <= 0.0 {
0.0
} else {
dot_product / (norm_a * norm_b)
(dot_product / norm_product.sqrt()).clamp(-1.0, 1.0)
}
}
+67 -46
View File
@@ -81,27 +81,64 @@ impl McpTool for ReadFileSkeletonHandler {
return;
}
let kind = node.kind();
let is_structural = matches!(
let is_container = matches!(
kind,
"use_declaration"
| "import_statement"
| "import_from_statement"
| "struct_item"
| "enum_item"
| "trait_item"
| "impl_item"
| "function_item"
| "function_declaration"
| "function_definition"
| "method_definition"
| "interface_declaration"
| "type_alias_declaration"
"impl_item"
| "class_declaration"
| "class_definition"
| "trait_item"
| "interface_declaration"
);
if is_structural {
let is_structural = is_container
|| matches!(
kind,
"use_declaration"
| "import_statement"
| "import_from_statement"
| "struct_item"
| "enum_item"
| "function_item"
| "function_declaration"
| "function_definition"
| "method_definition"
| "type_alias_declaration"
);
if is_container {
let indent = " ".repeat(depth);
let node_text = node.utf8_text(code.as_bytes()).unwrap_or("");
let mut header = String::new();
for line in node_text.lines() {
let trimmed = line.trim();
if trimmed.ends_with('{') || trimmed.ends_with(':') {
header.push_str(line);
break;
} else {
header.push_str(line);
header.push('\n');
}
}
if header.is_empty() {
if let Some(first_line) = node_text.lines().next() {
header = first_line.to_string();
}
}
out.push_str(&indent);
out.push_str(header.trim());
out.push('\n');
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
extract_skeleton(child, code, out, depth + 1);
}
if header.trim().ends_with('{') {
out.push_str(&indent);
out.push_str("}\n");
}
} else if is_structural {
let indent = " ".repeat(depth);
let node_text = node.utf8_text(code.as_bytes()).unwrap_or("");
@@ -127,7 +164,7 @@ impl McpTool for ReadFileSkeletonHandler {
} else if node.is_named() {
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
extract_skeleton(child, code, out, depth + 1);
extract_skeleton(child, code, out, depth);
}
}
}
@@ -231,6 +268,7 @@ impl McpTool for ReplaceAstNodeHandler {
actual_kind,
"trait_item" | "interface_declaration" | "interface_item"
),
"impl" | "impl_item" => actual_kind == "impl_item",
"type" | "type_alias" => matches!(
actual_kind,
"type_alias_declaration" | "type_item" | "type_definition"
@@ -253,6 +291,7 @@ impl McpTool for ReplaceAstNodeHandler {
let kind = child.kind();
if kind == "identifier"
|| kind == "name"
|| kind == "type_identifier"
|| kind == "property_identifier"
|| kind == "field_identifier"
{
@@ -386,6 +425,11 @@ fn scan_workspace_for_symbol(
if scanned_files > 500 {
break;
}
if let Ok(meta) = std::fs::metadata(path) {
if meta.len() > 1024 * 1024 {
continue;
}
}
if let Ok(content) = std::fs::read_to_string(path) {
for (line_num, line) in content.lines().enumerate() {
let is_match = if filter_fn_call {
@@ -450,17 +494,7 @@ impl McpTool for FindSymbolReferencesHandler {
Ok::<Vec<serde_json::Value>, crate::error::AppError>(refs)
})?;
let custom_dir = if let Some(ref d) = req.workspace_dir {
Some(std::path::PathBuf::from(d))
} else {
state.project.pinned_files.read_with(|pinned| {
pinned.first().and_then(|pin| {
std::path::Path::new(&pin.file_path)
.parent()
.map(|p| p.to_path_buf())
})
})
};
let custom_dir = req.workspace_dir.as_ref().map(std::path::PathBuf::from);
if matches.len() < limit {
let remaining = limit - matches.len();
@@ -516,17 +550,7 @@ impl McpTool for GetCallersHandler {
Ok::<Vec<serde_json::Value>, crate::error::AppError>(matching)
})?;
let custom_dir = if let Some(ref d) = req.workspace_dir {
Some(std::path::PathBuf::from(d))
} else {
state.project.pinned_files.read_with(|pinned| {
pinned.first().and_then(|pin| {
std::path::Path::new(&pin.file_path)
.parent()
.map(|p| p.to_path_buf())
})
})
};
let custom_dir = req.workspace_dir.as_ref().map(std::path::PathBuf::from);
if callers.len() < limit {
let remaining = limit - callers.len();
@@ -571,13 +595,10 @@ impl McpTool for AnalyzeImpactHandler {
}
});
let custom_dir = state.project.pinned_files.read_with(|pinned| {
pinned.first().and_then(|pin| {
std::path::Path::new(&pin.file_path)
.parent()
.map(|p| p.to_path_buf())
})
});
let custom_dir = req
.file_path
.as_ref()
.and_then(|p| std::path::Path::new(p).parent().map(|p| p.to_path_buf()));
let sym_clone = sym.clone();
let disk_refs = tokio::task::spawn_blocking(move || {
+74 -183
View File
@@ -5,166 +5,6 @@ use async_trait::async_trait;
use serde_json::Value;
use std::sync::Arc;
pub struct UpdateEnvFingerprintHandler;
#[async_trait]
impl McpTool for UpdateEnvFingerprintHandler {
fn name(&self) -> &'static str {
"update_env_fingerprint"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<UpdateEnvFingerprintTool>(
"update_env_fingerprint",
"Update system OS, shell, and installed tool version fingerprints for a workspace namespace.",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: UpdateEnvFingerprintTool =
serde_json::from_value(args).map_err(|e| e.to_string())?;
state.env.env_fingerprints.modify(|fps| {
fps.insert(
req.namespace.clone(),
crate::models::EnvFingerprint {
namespace: req.namespace.clone(),
os: std::env::consts::OS.to_string(),
shell: std::env::var("SHELL").unwrap_or_else(|_| "unknown".to_string()),
tool_versions: req.tool_versions,
updated_at: crate::handlers::utils::now_secs(),
..Default::default()
},
);
});
Ok("Env fingerprint updated".to_string())
}
}
pub struct ReadEnvFingerprintHandler;
#[async_trait]
impl McpTool for ReadEnvFingerprintHandler {
fn name(&self) -> &'static str {
"read_env_fingerprint"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<ReadEnvFingerprintTool>(
"read_env_fingerprint",
"Read system OS, shell, and tool version fingerprints for a workspace namespace.",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ReadEnvFingerprintTool =
serde_json::from_value(args).map_err(|e| e.to_string())?;
let data = state
.env
.env_fingerprints
.read_with(|fps| fps.get(&req.namespace).cloned());
if let Some(fp) = data {
let data = Ok::<String, crate::error::AppError>(serde_json::to_string(&fp)?)?;
Ok(data)
} else {
Ok("{}".to_string())
}
}
}
pub struct LogEnvRequirementHandler;
#[async_trait]
impl McpTool for LogEnvRequirementHandler {
fn name(&self) -> &'static str {
"log_env_requirement"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<LogEnvRequirementTool>(
"log_env_requirement",
"Log required environment variables, configuration keys, or secrets for a namespace.",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: LogEnvRequirementTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
state.env.env_requirements.modify(|reqs| {
reqs.retain(|r| !(r.namespace == req.namespace && r.key == req.key));
reqs.push(crate::models::EnvRequirement {
namespace: req.namespace,
key: req.key,
description: req.description,
is_secret: req.is_secret,
..Default::default()
});
});
Ok("Env requirement logged".to_string())
}
}
pub struct RegisterEnvironmentHandler;
#[async_trait]
impl McpTool for RegisterEnvironmentHandler {
fn name(&self) -> &'static str {
"register_environment"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<RegisterEnvironmentTool>(
"register_environment",
"Register infrastructure environment details (URL, VPN requirements, notes) for a namespace.",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: RegisterEnvironmentTool =
serde_json::from_value(args).map_err(|e| e.to_string())?;
state.env.environments.modify(|envs| {
envs.retain(|e| !(e.namespace == req.namespace && e.name == req.name));
envs.push(crate::models::EnvironmentDetail {
namespace: req.namespace,
name: req.name,
url: req.url,
description: req.description,
requires_vpn: req.requires_vpn,
updated_at: crate::handlers::utils::now_secs(),
..Default::default()
});
});
Ok("Environment registered".to_string())
}
}
pub struct GetEnvironmentDetailsHandler;
#[async_trait]
impl McpTool for GetEnvironmentDetailsHandler {
fn name(&self) -> &'static str {
"get_environment_details"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<GetEnvironmentDetailsTool>(
"get_environment_details",
"Retrieve registered infrastructure environment details for a namespace.",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: GetEnvironmentDetailsTool =
serde_json::from_value(args).map_err(|e| e.to_string())?;
let data = state.env.environments.read_with(|envs| {
let filtered: Vec<_> = envs
.iter()
.filter(|e| e.namespace == req.namespace)
.collect();
Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
})?;
Ok(data)
}
}
pub struct EnvironmentHandler;
#[async_trait]
@@ -186,10 +26,32 @@ impl McpTool for EnvironmentHandler {
match req.action {
EnvAction::UpdateFingerprint => {
let tool_versions = req.tool_versions.unwrap_or_default();
UpdateEnvFingerprintHandler.execute(serde_json::json!({"namespace": ns, "tool_versions": tool_versions}), state).await
state.env.env_fingerprints.modify(|fps| {
fps.insert(
ns.clone(),
crate::models::EnvFingerprint {
namespace: ns.clone(),
os: std::env::consts::OS.to_string(),
shell: std::env::var("SHELL").unwrap_or_else(|_| "unknown".to_string()),
tool_versions,
updated_at: crate::handlers::utils::now_secs(),
..Default::default()
},
);
});
Ok("Env fingerprint updated".to_string())
}
EnvAction::ReadFingerprint => {
ReadEnvFingerprintHandler.execute(serde_json::json!({"namespace": ns}), state).await
let data = state
.env
.env_fingerprints
.read_with(|fps| fps.get(&ns).cloned());
if let Some(fp) = data {
let data = Ok::<String, crate::error::AppError>(serde_json::to_string(&fp)?)?;
Ok(data)
} else {
Ok("{}".to_string())
}
}
EnvAction::LogRequirement => {
let key = req.key.ok_or_else(|| {
@@ -197,7 +59,17 @@ impl McpTool for EnvironmentHandler {
})?;
let desc = req.description.unwrap_or_default();
let secret = req.is_secret.unwrap_or(false);
LogEnvRequirementHandler.execute(serde_json::json!({"key": key, "description": desc, "is_secret": secret, "namespace": ns}), state).await
state.env.env_requirements.modify(|reqs| {
reqs.retain(|r| !(r.namespace == ns && r.key == key));
reqs.push(crate::models::EnvRequirement {
namespace: ns,
key,
description: desc,
is_secret: secret,
..Default::default()
});
});
Ok("Env requirement logged".to_string())
}
EnvAction::Register => {
let name = req.name.ok_or_else(|| {
@@ -206,15 +78,35 @@ impl McpTool for EnvironmentHandler {
let url = req.url.unwrap_or_default();
let desc = req.description.unwrap_or_default();
let vpn = req.requires_vpn.unwrap_or(false);
RegisterEnvironmentHandler.execute(serde_json::json!({"name": name, "url": url, "description": desc, "requires_vpn": vpn, "namespace": ns}), state).await
state.env.environments.modify(|envs| {
envs.retain(|e| !(e.namespace == ns && e.name == name));
envs.push(crate::models::EnvironmentDetail {
namespace: ns,
name,
url,
description: desc,
requires_vpn: vpn,
updated_at: crate::handlers::utils::now_secs(),
..Default::default()
});
});
Ok("Environment registered".to_string())
}
EnvAction::GetDetails => {
GetEnvironmentDetailsHandler.execute(serde_json::json!({"namespace": ns}), state).await
let data = state.env.environments.read_with(|envs| {
let filtered: Vec<_> = envs
.iter()
.filter(|e| e.namespace == ns)
.collect();
Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
})?;
Ok(data)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -225,25 +117,25 @@ mod tests {
async fn test_env_fingerprint() {
let dir = tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let handler = EnvironmentHandler;
let update_handler = UpdateEnvFingerprintHandler;
let args = json!({
"action": "update_fingerprint",
"namespace": "global",
"tool_versions": {
"rustc": "1.70.0"
}
});
let res = update_handler
let res = handler
.execute(args, state.clone())
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
assert_eq!(res, "Env fingerprint updated");
let read_handler = ReadEnvFingerprintHandler;
let res2 = read_handler
.execute(json!({"namespace": "global"}), state.clone())
let res2 = handler
.execute(json!({"action": "read_fingerprint", "namespace": "global"}), state.clone())
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
@@ -269,9 +161,9 @@ mod tests {
});
});
let handler = GetEnvironmentDetailsHandler;
let handler = EnvironmentHandler;
let res = handler
.execute(json!({"namespace": "global"}), state.clone())
.execute(json!({"action": "get_details", "namespace": "global"}), state.clone())
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
@@ -283,41 +175,39 @@ mod tests {
async fn test_env_requirements_and_registration() {
let dir = tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let handler = EnvironmentHandler;
let req_handler = LogEnvRequirementHandler;
let args_req = json!({
"action": "log_requirement",
"key": "vpn",
"description": "Requires VPN",
"is_secret": false,
"requirement": "Needs specific VPN",
"context": "For database access",
"namespace": "global"
});
let res1 = req_handler
let res1 = handler
.execute(args_req, state.clone())
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
assert_eq!(res1, "Env requirement logged");
let reg_handler = RegisterEnvironmentHandler;
let args_reg = json!({
"action": "register",
"name": "prod",
"url": "https://prod.local",
"description": "Production",
"requires_vpn": true,
"namespace": "global"
});
let res2 = reg_handler
let res2 = handler
.execute(args_reg, state.clone())
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
assert_eq!(res2, "Environment registered");
let get_handler = GetEnvironmentDetailsHandler;
let res3 = get_handler
.execute(json!({"namespace": "global"}), state.clone())
let res3 = handler
.execute(json!({"action": "get_details", "namespace": "global"}), state.clone())
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
@@ -329,16 +219,17 @@ mod tests {
async fn test_empty_namespace_in_update_env_fingerprint() {
let dir = tempfile::tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let handler = EnvironmentHandler;
let update_handler = UpdateEnvFingerprintHandler;
let args = serde_json::json!({
"action": "update_fingerprint",
"namespace": "",
"tool_versions": {
"rustc": "1.70.0"
}
});
let res = update_handler
let res = handler
.execute(args, state.clone())
.await;
+122 -95
View File
@@ -459,14 +459,15 @@ impl McpTool for ReadGraphHandler {
let offset = req.offset.unwrap_or(0);
let data = state.read_graph(|full| -> crate::error::Result<String> {
let mut result_json = if limit.is_some() || req.offset.is_some() {
let filtered_entities: Vec<(&String, &crate::models::Entity)> = full
.entities
.iter()
.filter(|(_, v)| req.namespace.as_ref().map_or(true, |ns| &v.namespace == ns))
.collect();
let filtered_entities: Vec<(&String, &crate::models::Entity)> = full
.entities
.iter()
.filter(|(_, v)| req.namespace.as_ref().map_or(true, |ns| &v.namespace == ns))
.collect();
let total_count = filtered_entities.len();
let total_count = filtered_entities.len();
let (entities_map, relations_vec, meta_json) = if limit.is_some() || req.offset.is_some() {
let effective_limit = limit.unwrap_or(50);
let paged_entities: std::collections::HashMap<&String, &crate::models::Entity> = filtered_entities
.into_iter()
@@ -485,45 +486,94 @@ impl McpTool for ReadGraphHandler {
})
.collect();
serde_json::to_string(&serde_json::json!({
"entities": paged_entities,
"relations": relevant_relations,
let meta = serde_json::json!({
"total_count": total_count,
"offset": offset,
"limit": effective_limit,
"has_more": has_more,
}))?
} else if let Some(ns) = req.namespace {
let mut filtered = BorrowedGraph::default();
for (k, v) in &full.entities {
if v.namespace == ns {
filtered.entities.insert(k, v);
}
}
for r in &full.relations {
if r.namespace == ns {
filtered.relations.push(r);
}
}
serde_json::to_string(&filtered)?
});
(paged_entities, relevant_relations, Some(meta))
} else {
serde_json::to_string(full)?
let ent_map: std::collections::HashMap<&String, &crate::models::Entity> =
filtered_entities.into_iter().collect();
let rel_vec: Vec<&crate::models::Relation> = full
.relations
.iter()
.filter(|r| req.namespace.as_ref().map_or(true, |ns| &r.namespace == ns))
.collect();
(ent_map, rel_vec, None)
};
let serialized = if let Some(meta) = &meta_json {
serde_json::json!({
"entities": entities_map,
"relations": relations_vec,
"total_count": meta["total_count"],
"offset": meta["offset"],
"limit": meta["limit"],
"has_more": meta["has_more"],
})
} else {
serde_json::json!({
"entities": entities_map,
"relations": relations_vec,
})
};
let mut result_json = serde_json::to_string(&serialized)?;
if let Some(max_tok) = max_tokens {
let max_chars = max_tok * 4;
if result_json.len() > max_chars {
let valid_boundary = result_json.floor_char_boundary(max_chars);
result_json.truncate(valid_boundary);
result_json.push_str("\n... [TRUNCATED_TO_MAX_TOKENS. Use search_nodes, pagination (limit, offset), or get_subgraph for targeted discovery]");
let total_ent = entities_map.len();
let total_rel = relations_vec.len();
let frac = (max_chars as f64) / (result_json.len() as f64);
let mut keep_count = (total_ent as f64 * frac * 0.85).max(1.0) as usize;
let ent_vec: Vec<_> = entities_map.into_iter().collect();
while keep_count > 0 {
let sliced_ent: std::collections::HashMap<_, _> = ent_vec.iter().take(keep_count).cloned().collect();
let sliced_rel: Vec<_> = relations_vec.iter().filter(|r| sliced_ent.contains_key(&r.from) && sliced_ent.contains_key(&r.to)).copied().collect();
let mut truncated_val = serde_json::json!({
"entities": sliced_ent,
"relations": sliced_rel,
"_meta": {
"truncated": true,
"total_entities": total_ent,
"returned_entities": sliced_ent.len(),
"total_relations": total_rel,
"returned_relations": sliced_rel.len(),
"note": "Output structurally truncated to fit within max_tokens. Use pagination (limit, offset), search_nodes, or get_subgraph for targeted discovery."
}
});
if let Some(meta) = &meta_json {
if let Some(obj) = truncated_val.as_object_mut() {
obj.insert("total_count".to_string(), meta["total_count"].clone());
obj.insert("offset".to_string(), meta["offset"].clone());
obj.insert("limit".to_string(), meta["limit"].clone());
obj.insert("has_more".to_string(), serde_json::Value::Bool(true));
}
}
if let Ok(trunc_str) = serde_json::to_string(&truncated_val) {
if trunc_str.len() <= max_chars || keep_count == 1 {
result_json = trunc_str;
break;
}
}
keep_count = keep_count.saturating_sub(keep_count / 3 + 1);
}
}
}
Ok(result_json)
})?;
Ok(data)
}
}
pub struct SearchNodesHandler;
#[async_trait]
@@ -910,36 +960,49 @@ impl McpTool for GetSubgraphHandler {
let depth = req.depth.unwrap_or(2);
let format = req.format.unwrap_or(SubgraphFormat::Json);
if format == SubgraphFormat::MarkdownTree {
let (sub_entities, sub_relations) = state.read_graph(|g| {
let mut visited = std::collections::HashSet::new();
let mut queue = std::collections::VecDeque::new();
queue.push_back((root.clone(), 0));
visited.insert(root.clone());
let (matched_entities, matched_relations) = state.read_graph(|full| {
let mut adj: std::collections::HashMap<&str, Vec<&str>> =
std::collections::HashMap::with_capacity(full.relations.len() * 2);
for r in &full.relations {
adj.entry(r.from.as_str()).or_default().push(r.to.as_str());
adj.entry(r.to.as_str()).or_default().push(r.from.as_str());
}
while let Some((curr, d)) = queue.pop_front() {
if d >= depth {
continue;
}
for r in &g.relations {
if r.from == curr && !visited.contains(&r.to) {
visited.insert(r.to.clone());
queue.push_back((r.to.clone(), d + 1));
} else if r.to == curr && !visited.contains(&r.from) {
visited.insert(r.from.clone());
queue.push_back((r.from.clone(), d + 1));
let mut visited = std::collections::HashSet::new();
let mut queue = std::collections::VecDeque::new();
queue.push_back((root.as_str(), 0));
visited.insert(root.clone());
while let Some((curr, d)) = queue.pop_front() {
if d < depth {
if let Some(neighbors) = adj.get(curr) {
for &next in neighbors {
if visited.insert(next.to_string()) {
queue.push_back((next, d + 1));
}
}
}
}
}
let ent: Vec<_> = g.entities.values().filter(|e| visited.contains(&e.name)).cloned().collect();
let rel: Vec<_> = g.relations.iter().filter(|r| visited.contains(&r.from) && visited.contains(&r.to)).cloned().collect();
(ent, rel)
});
let ent: std::collections::HashMap<String, crate::models::Entity> = visited
.iter()
.filter_map(|name| full.entities.get(name).map(|e| (name.clone(), e.clone())))
.collect();
let rel: Vec<crate::models::Relation> = full
.relations
.iter()
.filter(|r| visited.contains(&r.from) && visited.contains(&r.to))
.cloned()
.collect();
(ent, rel)
});
if format == SubgraphFormat::MarkdownTree {
let mut markdown = format!("# Subgraph Topology for `{}`\n\n", root);
markdown.push_str("## Entities\n");
for e in &sub_entities {
for e in matched_entities.values() {
markdown.push_str(&format!("- **{}** (`{}`)\n", e.name, e.entity_type));
for obs in &e.observations {
markdown.push_str(&format!(" - {}\n", obs));
@@ -947,56 +1010,20 @@ impl McpTool for GetSubgraphHandler {
}
markdown.push_str("\n## Relations\n");
for r in &sub_relations {
for r in &matched_relations {
markdown.push_str(&format!("- `{}` --[{}]--> `{}`\n", r.from, r.relation_type, r.to));
}
return Ok(markdown);
}
let data = state.read_graph(|full| {
let mut visited_nodes: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut matched_entities = std::collections::HashMap::new();
let mut matched_relations = Vec::new();
let result = serde_json::json!({
"root_node": root,
"depth": depth,
"entities": matched_entities,
"relations": matched_relations,
});
let mut queue = std::collections::VecDeque::new();
queue.push_back((root.clone(), 0));
visited_nodes.insert(root.clone());
while let Some((curr, current_depth)) = queue.pop_front() {
if let Some(entity) = full.entities.get(&curr) {
matched_entities.insert(curr.clone(), entity.clone());
}
if current_depth < depth {
for rel in &full.relations {
if rel.from == curr {
matched_relations.push(rel.clone());
if !visited_nodes.contains(&rel.to) {
visited_nodes.insert(rel.to.clone());
queue.push_back((rel.to.clone(), current_depth + 1));
}
} else if rel.to == curr {
matched_relations.push(rel.clone());
if !visited_nodes.contains(&rel.from) {
visited_nodes.insert(rel.from.clone());
queue.push_back((rel.from.clone(), current_depth + 1));
}
}
}
}
}
let result = serde_json::json!({
"root_node": root,
"depth": depth,
"entities": matched_entities,
"relations": matched_relations,
});
Ok::<String, crate::error::AppError>(serde_json::to_string_pretty(&result)?)
})?;
Ok(data)
Ok(serde_json::to_string_pretty(&result)?)
}
}
File diff suppressed because it is too large. Load diff
+1 -156
View File
@@ -7,100 +7,6 @@ use serde_json::Value;
use std::collections::HashSet;
use std::sync::Arc;
pub struct StickyNotesHandler;
#[async_trait]
impl McpTool for StickyNotesHandler {
fn name(&self) -> &'static str {
"sticky_notes"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<StickyNotesTool>(
"sticky_notes",
"Ephemeral sticky notes with TTL (add, read, delete, clear)",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: StickyNotesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
match req.action {
StickyNoteAction::Add => {
let content = req.content.ok_or_else(|| {
crate::error::AppError::Internal("Missing required parameter 'content' for action 'add'. Next step: Provide non-empty 'content' string in request and retry.".to_string())
})?;
let now = crate::handlers::utils::now_secs();
let expires_at = if let Some(ttl) = req.ttl_seconds {
Some(now + ttl)
} else if req.session_only.unwrap_or(false) {
Some(now + 14400)
} else {
None
};
state.code.sticky.modify(|notes| {
notes.push(StickyNote {
timestamp: now,
content,
expires_at,
..Default::default()
});
});
Ok("Sticky note added.".to_string())
}
StickyNoteAction::Read => {
let now = crate::handlers::utils::now_secs();
let has_expired = state.code.sticky.read_with(|notes| {
notes.iter().any(|n| matches!(n.expires_at, Some(exp) if exp <= now))
});
if has_expired {
state.code.sticky.modify(|notes| {
notes.retain(|n| {
if let Some(exp) = n.expires_at {
exp > now
} else {
true
}
});
});
}
let active_notes = state.code.sticky.read_with(|notes| notes.clone());
Ok(serde_json::to_string(&active_notes)?)
}
StickyNoteAction::Delete => {
let idx = req.index.ok_or_else(|| {
crate::error::AppError::Internal("Missing required parameter 'index' for action 'delete'. Next step: Provide 1-based sticky note 'index' integer in request and retry.".to_string())
})?;
let mut success = false;
state.code.sticky.modify(|notes| {
if idx > 0 && idx <= notes.len() {
notes.remove(idx - 1);
success = true;
} else if idx == 0 && !notes.is_empty() {
notes.remove(0);
success = true;
}
});
if success {
Ok("Sticky note deleted.".to_string())
} else {
Err(crate::error::AppError::Internal(
"Invalid sticky note index. Next step: Call sticky_notes with action='read' to view active notes and valid indices, then retry.".to_string(),
))
}
}
StickyNoteAction::Clear => {
state.code.sticky.modify(|notes| {
notes.clear();
});
Ok("All sticky notes cleared.".to_string())
}
}
}
}
pub struct HandoffMemosHandler;
#[async_trait]
@@ -255,7 +161,7 @@ impl McpTool for PromoteToEntityHandler {
fn schema(&self) -> Value {
crate::mcp::tool_def::<PromoteToEntityTool>(
"promote_to_entity",
"Promote a transient sticky note or active task observation into a permanent Knowledge Graph entity.",
"Promote a task observation or finding into a permanent Knowledge Graph entity.",
)
}
@@ -285,10 +191,6 @@ impl McpTool for PromoteToEntityHandler {
}
});
state.code.sticky.modify(|notes| {
notes.retain(|n| !n.content.contains(&obs));
});
Ok(format!(
"Promoted transient note into permanent entity '{}'.",
req.entity_name
@@ -302,47 +204,6 @@ mod tests {
use serde_json::json;
use tempfile::tempdir;
#[tokio::test]
async fn test_notes_lifecycle() {
let dir = tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let handler = StickyNotesHandler;
let args = json!({
"action": "add",
"content": "Buy milk",
});
let res = handler
.execute(args, state.clone())
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
assert!(res.contains("Sticky note added"));
let res2 = handler
.execute(json!({"action": "read"}), state.clone())
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
assert!(res2.contains("Buy milk"));
let args2 = json!({"action": "delete", "index": 1});
let res3 = handler
.execute(args2, state.clone())
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
assert_eq!(res3, "Sticky note deleted.");
let res4 = handler
.execute(json!({"action": "read"}), state.clone())
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
assert!(!res4.contains("Buy milk"));
}
#[tokio::test]
async fn test_handoff_and_summaries() {
let dir = tempdir().unwrap();
@@ -392,22 +253,6 @@ mod tests {
.unwrap();
assert!(!res4.is_empty());
}
#[tokio::test]
async fn test_invalid_sticky_note_action() {
let dir = tempfile::tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let handler = StickyNotesHandler;
let invalid_args = serde_json::json!({
"action": "invalid_action"
});
let res = handler
.execute(invalid_args, state.clone())
.await;
assert!(res.is_err());
}
}
+20 -3
View File
@@ -81,7 +81,7 @@ impl McpTool for TasksHandler {
crate::error::AppError::Internal("Missing required parameter 'id' for action 'update'. Next step: Provide task 'id' string in request and retry.".to_string())
})?;
let status = req.status.ok_or_else(|| {
crate::error::AppError::Internal("Missing required parameter 'status' for action 'update'. Next step: Provide valid 'status' ('pending', 'completed', or 'cancelled') in request and retry.".to_string())
crate::error::AppError::Internal("Missing required parameter 'status' for action 'update'. Next step: Provide valid 'status' ('pending', 'active', 'in_progress', 'blocked', 'completed', or 'cancelled') in request and retry.".to_string())
})?;
let target_status = status.to_lowercase();
let mut found = false;
@@ -113,6 +113,7 @@ impl McpTool for TasksHandler {
if let Some(dep_task) = tasks.iter().find(|dt| dt.id == *dep_id)
&& dep_task.status != "completed"
&& dep_task.status != "done"
&& dep_task.status != "cancelled"
{
uncompleted_deps.push(dep_task.title.as_str());
}
@@ -132,7 +133,10 @@ impl McpTool for TasksHandler {
.iter()
.filter(|t| t.parent_id.as_deref() == Some(target_id_ref))
{
if child.status != "completed" && child.status != "done" {
if child.status != "completed"
&& child.status != "done"
&& child.status != "cancelled"
{
uncompleted_children.push(child.title.as_str());
}
}
@@ -303,6 +307,7 @@ impl McpTool for TasksHandler {
crate::error::AppError::Internal("Missing required parameter 'criteria' for action 'set_criteria'. Next step: Provide array of acceptance criteria strings in request and retry.".to_string())
})?;
let mut success = false;
let mut updated_task: Option<Task> = None;
state.project.tasks.modify(|tasks| {
if let Some(task) = tasks.iter_mut().find(|t| t.id == id || t.title == id) {
task.acceptance_criteria = criteria_list
@@ -315,10 +320,15 @@ impl McpTool for TasksHandler {
})
.collect();
task.updated_at = crate::handlers::utils::now_secs();
updated_task = Some(task.clone());
success = true;
}
});
if success {
if let Some(ref ut) = updated_task {
let idx = state.get_search_index().await;
drop(idx.index_task(ut));
}
state.broadcast_task_event(TaskEvent {
task_id: id.clone(),
status: "criteria_set".to_string(),
@@ -343,6 +353,7 @@ impl McpTool for TasksHandler {
let proof_str = req.proof.unwrap_or_else(|| "Verified".to_string());
let mut success = false;
let mut already_met = false;
let mut updated_task: Option<Task> = None;
state.project.tasks.modify(|tasks| {
if let Some(task) = tasks.iter_mut().find(|t| t.id == id || t.title == id) {
if let Some(ac) = task.acceptance_criteria.iter_mut().next() {
@@ -351,7 +362,8 @@ impl McpTool for TasksHandler {
} else {
ac.is_met = true;
success = true;
task.updated_at = crate::handlers::utils::now_secs();
task.updated_at = crate::handlers::utils::now_secs();
updated_task = Some(task.clone());
}
} else {
task.acceptance_criteria
@@ -362,11 +374,16 @@ impl McpTool for TasksHandler {
..Default::default()
});
task.updated_at = crate::handlers::utils::now_secs();
updated_task = Some(task.clone());
success = true;
}
}
});
if success {
if let Some(ref ut) = updated_task {
let idx = state.get_search_index().await;
drop(idx.index_task(ut));
}
state.broadcast_task_event(TaskEvent {
task_id: id.clone(),
status: "verified".to_string(),
+201 -213
View File
@@ -1,6 +1,6 @@
use crate::router::McpTool;
use crate::state::MemoryState;
use crate::tools::{ReadClipboardTool, WriteClipboardTool};
use crate::tools::{ClipboardAction, ClipboardTool};
use arboard::{Clipboard, ImageData};
use async_trait::async_trait;
use image::{ImageBuffer, imageops::FilterType};
@@ -8,104 +8,6 @@ use serde_json::{Value, json};
use std::borrow::Cow;
use std::sync::Arc;
pub struct WriteClipboardHandler;
#[async_trait]
impl McpTool for WriteClipboardHandler {
fn name(&self) -> &'static str {
"write_clipboard"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<WriteClipboardTool>(
"write_clipboard",
"Write text, HTML, files, and/or images directly to the OS clipboard.",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let tool_args: WriteClipboardTool = serde_json::from_value(args)
.map_err(|e| crate::error::AppError::Internal(format!("Invalid args: {}", e)))?;
let res = tokio::task::spawn_blocking(move || {
let mut msgs = Vec::new();
if let Some(text) = &tool_args.text {
let mut written = false;
if let Ok(mut clipboard) = Clipboard::new() {
for _ in 0..3 {
if clipboard.set_text(text).is_ok() {
written = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(20));
}
}
if written {
msgs.push("Wrote text");
}
}
// Handle arboard for image
if let Some(image_path) = &tool_args.image_path {
match image::open(image_path) {
Ok(img) => {
let img = if img.width() > 2048 || img.height() > 2048 {
img.resize(2048, 2048, FilterType::Triangle)
} else {
img
};
let rgba = img.into_rgba8();
let (w, h) = rgba.dimensions();
let raw_bytes = rgba.into_raw();
let mut written = false;
if let Ok(mut clipboard) = arboard::Clipboard::new() {
for _ in 0..3 {
let img_data = ImageData {
width: w as usize,
height: h as usize,
bytes: Cow::Borrowed(&raw_bytes),
};
if clipboard.set_image(img_data).is_ok() {
written = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(20));
}
}
if written {
msgs.push("Wrote image");
} else {
return Err(crate::error::AppError::Internal(
"Failed to write image to clipboard".to_string(),
));
}
}
Err(e) => {
return Err(crate::error::AppError::Internal(format!(
"Failed to open image {}: {}",
image_path, e
)));
}
}
}
if msgs.is_empty() {
Ok("No valid clipboard data provided or failed to write.".to_string())
} else {
Ok(format!(
"Successfully populated clipboard: {}",
msgs.join(", ")
))
}
})
.await
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
state.record_activity("clipboard", &res, None);
Ok(res)
}
}
pub fn get_native_clipboard_text() -> Option<String> {
for _ in 0..3 {
@@ -121,7 +23,53 @@ pub fn get_native_clipboard_text() -> Option<String> {
None
}
#[cfg(target_os = "windows")]
fn get_windows_png_clipboard_image() -> Option<image::DynamicImage> {
use windows_sys::Win32::System::DataExchange::{
CloseClipboard, GetClipboardData, IsClipboardFormatAvailable, OpenClipboard,
RegisterClipboardFormatW,
};
use windows_sys::Win32::System::Memory::{GlobalLock, GlobalSize, GlobalUnlock};
let format_name: Vec<u16> = "PNG\0".encode_utf16().collect();
unsafe {
let format_id = RegisterClipboardFormatW(format_name.as_ptr());
if format_id == 0 {
return None;
}
for _ in 0..3 {
if OpenClipboard(std::ptr::null_mut()) != 0 {
let has_format = IsClipboardFormatAvailable(format_id) != 0;
let mut img_opt = None;
if has_format {
let handle = GetClipboardData(format_id);
if !handle.is_null() {
let size = GlobalSize(handle as _);
if size > 0 {
let ptr = GlobalLock(handle as _);
if !ptr.is_null() {
let slice = std::slice::from_raw_parts(ptr as *const u8, size);
img_opt = image::load_from_memory(slice).ok();
GlobalUnlock(handle as _);
}
}
}
}
CloseClipboard();
if img_opt.is_some() {
return img_opt;
}
break;
}
std::thread::sleep(std::time::Duration::from_millis(20));
}
}
None
}
pub fn get_native_clipboard_image() -> Option<image::DynamicImage> {
#[cfg(target_os = "windows")]
if let Some(img) = get_windows_png_clipboard_image() {
return Some(img);
}
for _ in 0..3 {
if let Ok(mut clipboard) = arboard::Clipboard::new() {
if let Ok(image_data) = clipboard.get_image() {
@@ -139,105 +87,6 @@ pub fn get_native_clipboard_image() -> Option<image::DynamicImage> {
None
}
pub struct ReadClipboardHandler;
#[async_trait]
impl McpTool for ReadClipboardHandler {
fn name(&self) -> &'static str {
"read_clipboard"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<ReadClipboardTool>(
"read_clipboard",
"Read the current OS clipboard. Extracts text, HTML, file paths, and/or an image if available.",
)
}
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let result =
tokio::task::spawn_blocking(move || -> crate::error::Result<serde_json::Value> {
let mut out = serde_json::Map::new();
if let Some(text) = get_native_clipboard_text() {
out.insert("text".into(), json!(text));
}
if let Some(dynamic_img) = get_native_clipboard_image() {
let mut img = dynamic_img;
let max_dim = 1024;
if img.width() > max_dim || img.height() > max_dim {
img = img.resize(max_dim, max_dim, FilterType::Lanczos3);
}
let rgb_img = img.into_rgb8();
let cache_dir = dirs::home_dir()
.unwrap_or_default()
.join(".gemini/mcp_memory/clipboard");
let _ = std::fs::create_dir_all(&cache_dir);
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let file_path = cache_dir.join(format!("clipboard_{}.jpg", timestamp));
if rgb_img
.save_with_format(&file_path, image::ImageFormat::Jpeg)
.is_ok()
{
let path_str = file_path.to_string_lossy().to_string();
out.insert("image_path".into(), json!(path_str));
#[cfg(target_os = "windows")]
if let Some(rest) = path_str.strip_prefix("C:\\") {
let wsl_path = format!("/mnt/c/{}", rest.replace('\\', "/"));
out.insert("image_path_wsl".into(), json!(wsl_path));
} else if let Some(rest) = path_str.strip_prefix("c:\\") {
let wsl_path = format!("/mnt/c/{}", rest.replace('\\', "/"));
out.insert("image_path_wsl".into(), json!(wsl_path));
}
if let Ok(bytes) = std::fs::read(&file_path) {
use base64::Engine;
let b64 = base64::engine::general_purpose::STANDARD.encode(&bytes);
out.insert("image_base64".into(), json!(b64));
}
}
}
Ok(Value::Object(out))
})
.await
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
let mut final_obj = result;
if let Some(b64) = final_obj.get("image_base64").and_then(|v| v.as_str()) {
let b64_str = b64.to_string();
if let Some(obj) = final_obj.as_object_mut() {
obj.remove("image_base64");
}
if state.ollama.is_available().await
&& let Ok(analysis) = state
.ollama
.generate_vision(
"Describe the content, text, or UI layout of this image concisely:",
&b64_str,
)
.await
&& let Some(obj) = final_obj.as_object_mut()
{
obj.insert("image_analysis".to_string(), json!(analysis.trim()));
}
}
state.record_activity("clipboard", "Read contents from OS clipboard", None);
Ok::<String, crate::error::AppError>(serde_json::to_string_pretty(&final_obj)?)
}
}
use crate::tools::{ClipboardAction, ClipboardTool};
pub struct ClipboardHandler;
#[async_trait]
@@ -257,23 +106,161 @@ impl McpTool for ClipboardHandler {
let req: ClipboardTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
match req.action {
ClipboardAction::Read => {
ReadClipboardHandler.execute(serde_json::json!({}), state).await
let result =
tokio::task::spawn_blocking(move || -> crate::error::Result<serde_json::Value> {
let mut out = serde_json::Map::new();
if let Some(text) = get_native_clipboard_text() {
out.insert("text".into(), json!(text));
}
if let Some(dynamic_img) = get_native_clipboard_image() {
let mut img = dynamic_img;
let max_dim = 1024;
if img.width() > max_dim || img.height() > max_dim {
img = img.resize(max_dim, max_dim, FilterType::Lanczos3);
}
let rgb_img = img.into_rgb8();
let mut jpeg_bytes = std::io::Cursor::new(Vec::new());
if rgb_img.write_to(&mut jpeg_bytes, image::ImageFormat::Jpeg).is_ok() {
let bytes = jpeg_bytes.into_inner();
use base64::Engine;
let b64 = base64::engine::general_purpose::STANDARD.encode(&bytes);
out.insert("image_base64".into(), json!(b64));
let cache_dir = dirs::home_dir()
.unwrap_or_default()
.join(".gemini/mcp_memory/clipboard");
let _ = std::fs::create_dir_all(&cache_dir);
let file_path = cache_dir.join("clipboard_latest.jpg");
if std::fs::write(&file_path, &bytes).is_ok() {
let path_str = file_path.to_string_lossy().to_string();
out.insert("image_path".into(), json!(path_str));
#[cfg(target_os = "windows")]
if let Some(rest) = path_str.strip_prefix("C:\\") {
let wsl_path = format!("/mnt/c/{}", rest.replace('\\', "/"));
out.insert("image_path_wsl".into(), json!(wsl_path));
} else if let Some(rest) = path_str.strip_prefix("c:\\") {
let wsl_path = format!("/mnt/c/{}", rest.replace('\\', "/"));
out.insert("image_path_wsl".into(), json!(wsl_path));
}
}
}
}
Ok(Value::Object(out))
})
.await
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
let mut final_obj = result;
if let Some(b64) = final_obj.get("image_base64").and_then(|v| v.as_str()) {
let b64_str = b64.to_string();
if let Some(obj) = final_obj.as_object_mut() {
obj.remove("image_base64");
}
if state.ollama.is_available().await
&& let Ok(analysis) = state
.ollama
.generate_vision(
"Describe the content, text, or UI layout of this image concisely:",
&b64_str,
)
.await
&& let Some(obj) = final_obj.as_object_mut()
{
obj.insert("image_analysis".to_string(), json!(analysis.trim()));
}
}
state.record_activity("clipboard", "Read contents from OS clipboard", None);
Ok::<String, crate::error::AppError>(serde_json::to_string_pretty(&final_obj)?)
}
ClipboardAction::Write => {
WriteClipboardHandler.execute(
serde_json::json!({
"text": req.text,
"html": req.html,
"files": req.files,
"image_path": req.image_path
}),
state,
).await
let text_opt = req.text;
let image_path_opt = req.image_path;
let res = tokio::task::spawn_blocking(move || {
let mut msgs = Vec::new();
if let Some(text) = &text_opt {
let mut written = false;
if let Ok(mut clipboard) = Clipboard::new() {
for _ in 0..3 {
if clipboard.set_text(text).is_ok() {
written = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(20));
}
}
if written {
msgs.push("Wrote text");
}
}
if let Some(image_path) = &image_path_opt {
match image::open(image_path) {
Ok(img) => {
let img = if img.width() > 2048 || img.height() > 2048 {
img.resize(2048, 2048, FilterType::Triangle)
} else {
img
};
let rgba = img.into_rgba8();
let (w, h) = rgba.dimensions();
let raw_bytes = rgba.into_raw();
let mut written = false;
if let Ok(mut clipboard) = arboard::Clipboard::new() {
for _ in 0..3 {
let img_data = ImageData {
width: w as usize,
height: h as usize,
bytes: Cow::Borrowed(&raw_bytes),
};
if clipboard.set_image(img_data).is_ok() {
written = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(20));
}
}
if written {
msgs.push("Wrote image");
} else {
return Err(crate::error::AppError::Internal(
"Failed to write image to clipboard".to_string(),
));
}
}
Err(e) => {
return Err(crate::error::AppError::Internal(format!(
"Failed to open image {}: {}",
image_path, e
)));
}
}
}
if msgs.is_empty() {
Ok("No valid clipboard data provided or failed to write.".to_string())
} else {
Ok(format!(
"Successfully populated clipboard: {}",
msgs.join(", ")
))
}
})
.await
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
state.record_activity("clipboard", &res, None);
Ok(res)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -286,9 +273,10 @@ mod tests {
async fn test_write_clipboard() {
let dir = tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let handler = WriteClipboardHandler;
let handler = ClipboardHandler;
let args = json!({
"action": "write",
"text": "test_text"
});
@@ -310,10 +298,10 @@ mod tests {
async fn test_read_clipboard() {
let dir = tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let handler = ReadClipboardHandler;
let handler = ClipboardHandler;
let result = handler
.execute(json!({}), state)
.execute(json!({"action": "read"}), state)
.await
.map_err(|e| format!("Failed to read clipboard: {}", e))
.unwrap();
@@ -327,10 +315,10 @@ mod tests {
async fn test_read_clipboard_empty() {
let dir = tempfile::tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let handler = ReadClipboardHandler;
let handler = ClipboardHandler;
let result = handler
.execute(serde_json::json!({}), state)
.execute(serde_json::json!({"action": "read"}), state)
.await
.map_err(|e| format!("Failed to read clipboard: {}", e))
.unwrap();
File diff suppressed because it is too large. Load diff
+2 -27
View File
@@ -36,34 +36,11 @@ The server consolidates granular single-purpose tools into domain-named smart to
- `action: "update"`: Update milestone status (requires `id`, `status: "active" | "completed" | "cancelled"`).
- `action: "list"`: List milestones (optional `namespace`).
* **`sticky_notes`**: Ephemeral scratchpad notes with TTL.
- `action: "add"`: Add note (requires `content`, optional `ttl_seconds`, `session_only`).
- `action: "read"`: Read all active notes.
- `action: "delete"`: Delete note by index (requires 1-based `index`).
- `action: "clear"`: Clear all sticky notes.
* **`handoff_memos`**: Session handoff notes for future agents.
- `action: "leave"`: Leave a memo (requires `content`, optional `vcs_revision`, `repo_name`, `git_branch`, `blockers: Vec<String>`, `action_items: Vec<String>`, `expires_at`).
- `action: "read"`: Read active handoff memos.
- `action: "clear"`: Clear memos.
* **`pinned_files`**: Focus file working set.
- `action: "pin"`: Pin file to focus set (requires `path`, optional `pin_reason`, `symbol_or_line`, `repo_name`, `git_branch`).
- `action: "unpin"`: Unpin file from focus set (requires `path`).
- `action: "list"`: List pinned files (optional `namespace`).
* **`context_workspaces`**: Workspace context state snapshots.
- `action: "save"`: Save context workspace (requires `name`, optional `pinned_files`, `active_task_ids`, `description`, `git_branch`, `vcs_revision`, `repo_name`).
- `action: "load"`: Restore saved context workspace (requires `name`).
- `action: "list"`: List saved context workspaces.
- `action: "delete"`: Delete saved context workspace (requires `name`).
- `action: "diff"`: Compare two saved context workspaces (requires `name`, `other_name`).
* **`pr_checklist`**: Pre-commit and PR checklist.
- `action: "add"`: Add checklist item (requires `description`, optional `category: "Security" | "Testing" | "Formatting" | ...`, `automated_check`, `is_checked`, `repo_name`).
- `action: "get"`: Get PR checklist items.
- `action: "clear"`: Clear PR checklist.
* **`snippets`**: Reusable code snippet vault.
- `action: "store"`: Store snippet (requires `query` as name, optional `language`, `code`, `description`, `tags`, `origin_file`, `line_range`, `repo_name`).
- `action: "search"`: Search snippet vault (optional `query`, `tags`, `hybrid: true`).
@@ -90,7 +67,6 @@ The server consolidates granular single-purpose tools into domain-named smart to
* **`clipboard`**: OS Clipboard management.
- `action: "read"`: Read OS clipboard.
- `action: "write"`: Write text/html/files/image to clipboard.
- `action: "toggle_watch"`: Toggle auto-clipboard watcher.
---
@@ -189,9 +165,8 @@ To maintain maximum security, speed, and cross-platform reliability:
* **Zero-Allocation Stream Formatting**: Graph condensation loops (`condense_graph_worker`) format node/relation subgraphs into stream buffers via `std::fmt::Write`, eliminating intermediate String allocation overhead.
* **Single-Handle OS Clipboard Retries & Image Downsampling**: `WriteClipboardHandler` initializes OS clipboard handles once per operation and downsamples images exceeding $2048 \times 2048$ resolution before writing raw RGBA bytes.
* **Zero Transaction Drop Persistence Guarantee**: `Store::modify` automatically spawns an async task to execute `push_async` with channel backpressure if `push` encounters queue saturation, ensuring zero data loss under spike write loads.
* **Non-Blocking Read Lock Sticky Notes**: `StickyNotesHandler` (`action = "read"`) queries notes using shared read locks, executing write pruning only when expired items exist.
* **Token-Budgeted Query Projections**: Decision queries (`query_decisions`) support `limit` caps and compact `include_body: false` projections for token budget optimization.
* **Serde Parameter & Enum Ergonomics**: Action enums (`StickyNoteAction`, `SnippetSearchMode`, `Relation`) support case-insensitive variants and common synonyms (`create`/`add`, `remove`/`delete`, `list`/`read`, `source`/`from`, `target`/`to`, `relationType`/`relation_type`) ensuring seamless LLM tool execution.
* **Serde Parameter & Enum Ergonomics**: Action enums (`SnippetSearchMode`, `Relation`) support case-insensitive variants and common synonyms (`create`/`add`, `remove`/`delete`, `list`/`read`, `source`/`from`, `target`/`to`, `relationType`/`relation_type`) ensuring seamless LLM tool execution.
* **Atomic Search Index Swaps**: `MemoryState::rebuild_index` constructs and populates a new `MemoryIndex` instance in isolation before performing an atomic pointer swap (`*self.search_index.write().await = new_idx`), eliminating transient empty search result windows.
* **Non-Blocking Tantivy Search Queries**: `MemoryIndex::search` queries current index searcher snapshots without executing synchronous disk commit locks, preventing query stalls during heavy background indexing.
* **Zero-Allocation HashSet<&str> Snippet Deduplication**: `indexer.rs` utilizes borrowed `HashSet<&str>` name lookups during snippet batch modifications, eliminating heap string re-allocations inside the store write lock.
@@ -214,7 +189,7 @@ To maintain maximum security, speed, and cross-platform reliability:
* **Embedding Input Safeguard**: `generate_embedding_async` returns explicit errors for empty/0-length text inputs instead of returning empty vectors, preventing downstream vector dimension mismatches during cosine similarity calculations.
* **Path Traversal Security Guards**: `validate_safe_path` enforces path canonicalization and rejects relative parent traversal components (`..`) across file and process log handlers (`GetRecentLogsTool`, `WatchProcessLogsTool`).
* **Watcher Map Memory Eviction**: Proactive daemon file watcher in `watcher.rs` caps `last_processed` map size at 1,000 entries and purges entries older than 10 minutes to prevent monotonic memory leakage.
* **Comprehensive Serde Casing Aliases**: All 11 consolidated tool action enums (`TaskAction`, `MilestoneAction`, `PinnedFileAction`, `ContextWorkspaceAction`, `PrChecklistAction`, `SnippetAction`, `DecisionAction`, `TechDebtAction`, `EnvAction`, `ClipboardAction`, `HandoffMemoAction`) include serde alias attributes supporting `snake_case`, `camelCase`, `PascalCase`, and uppercase variants for maximum LLM casing resilience.
* **Comprehensive Serde Casing Aliases**: All 8 consolidated tool action enums (TaskAction, MilestoneAction, SnippetAction, DecisionAction, TechDebtAction, EnvAction, ClipboardAction, HandoffMemoAction) include serde alias attributes supporting `snake_case`, `camelCase`, `PascalCase`, and uppercase variants for maximum LLM casing resilience.
* **Two-Phase Graph Condensation**: `condense_graph_worker` uses a 2-phase commit (non-destructive `read_with` -> graph insert -> prune by timestamp/content) to prevent data loss if summarization or graph insertion fails.
* **Store Write Lock Minimization**: `Store::modify` and `Store::modify_async` unblock concurrent readers during JSON serialization by releasing the write lock immediately after mutating memory state.
* **Redb Database Lock Retry Backoff**: `init_db` retries transient Redb lock contention with exponential backoff (3 attempts, 150ms delay) before falling back.
+1 -45
View File
@@ -105,9 +105,7 @@ pub async fn ttl_sweeper_worker(state: Arc<MemoryState>) {
state.project.tasks.modify(|tasks| {
tasks.retain(|t| t.expires_at.is_none_or(|exp| exp > now));
});
state.code.sticky.modify(|notes| {
notes.retain(|n| n.expires_at.is_none_or(|exp| exp > now));
});
state.telemetry.handoff_memos.modify(|memos| {
memos.retain(|m| m.expires_at.is_none_or(|exp| exp > now));
});
@@ -139,48 +137,6 @@ pub async fn condense_graph_worker(state: Arc<MemoryState>) {
.unwrap_or_default()
.as_secs();
let sticky_condensation = state.code.sticky.read_with(|notes| {
if notes.len() > threshold {
let mut sorted = notes.clone();
sorted.sort_by_key(|n| n.timestamp);
let to_remove = sorted.len() - (threshold / 2);
let removed: Vec<_> = sorted.into_iter().take(to_remove).collect();
let mut content = String::new();
let mut ids = Vec::new();
for r in &removed {
content.push_str(&format!("{}\n", r.content));
ids.push(r.id.clone());
}
Some((content, ids))
} else {
None
}
});
if let Some((content, ids)) = sticky_condensation {
if !content.is_empty() {
let name = format!("StickyNote History {}", now);
state.modify_graph(|graph| {
graph.entities.insert(
name.clone(),
crate::models::Entity {
name: name.clone(),
entity_type: "Historical Summary".to_string(),
observations: vec![content],
namespace: crate::models::default_namespace(),
git_branch: None,
..Default::default()
},
);
});
let id_set: std::collections::HashSet<String> = ids.into_iter().collect();
state.code.sticky.modify(|notes| {
notes.retain(|n| !id_set.contains(&n.id));
});
tracing::info!("Condensed sticky notes into Historical Summary.");
}
}
let snippet_condensation = state.code.snippets.read_with(|snippets| {
if snippets.len() > threshold {
let mut sorted = snippets.clone();
+5 -71
View File
@@ -81,21 +81,6 @@ impl CodeChange {
self.branch.as_deref().or(self.git_branch.as_deref())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct StickyNote {
#[serde(default = "generate_uuid")]
pub id: String,
pub timestamp: u64,
pub content: String,
#[serde(default)]
pub expires_at: Option<u64>,
#[serde(default = "default_namespace")]
pub namespace: String,
#[serde(default)]
pub category: Option<String>,
#[serde(default)]
pub author: Option<String>,
}
pub fn generate_uuid() -> String {
uuid::Uuid::new_v4().to_string()
@@ -162,18 +147,20 @@ pub fn normalize_relation_type(input: &str) -> String {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
pub enum TaskStatus {
Pending,
#[default]
Active,
InProgress,
Blocked,
Completed,
Cancelled,
}
impl TaskStatus {
pub fn is_active(&self) -> bool {
matches!(
self,
TaskStatus::Active | TaskStatus::InProgress | TaskStatus::Blocked
TaskStatus::Pending | TaskStatus::Active | TaskStatus::InProgress | TaskStatus::Blocked
)
}
@@ -183,10 +170,12 @@ impl TaskStatus {
pub fn as_str(&self) -> &'static str {
match self {
TaskStatus::Pending => "pending",
TaskStatus::Active => "active",
TaskStatus::InProgress => "in_progress",
TaskStatus::Blocked => "blocked",
TaskStatus::Completed => "completed",
TaskStatus::Cancelled => "cancelled",
}
}
}
@@ -400,18 +389,6 @@ fn default_adr_status() -> String {
"accepted".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Preference {
pub key: String,
pub value: String,
pub updated_at: u64,
#[serde(default = "default_namespace")]
pub namespace: String,
#[serde(default)]
pub category: Option<String>,
#[serde(default)]
pub rationale: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ErrorFix {
@@ -434,19 +411,6 @@ pub struct ErrorFix {
pub toolchain: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PinnedFile {
pub namespace: String,
pub file_path: String,
pub timestamp: u64,
pub git_branch: Option<String>,
#[serde(default)]
pub repo_name: Option<String>,
#[serde(default)]
pub symbol_or_line: Option<String>,
#[serde(default)]
pub pin_reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SessionSummary {
@@ -554,20 +518,6 @@ pub struct EnvironmentDetail {
pub ssh_host: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PrChecklistItem {
pub namespace: String,
pub id: String,
pub description: String,
#[serde(default)]
pub is_checked: bool,
#[serde(default)]
pub category: Option<String>,
#[serde(default)]
pub automated_check: Option<String>,
#[serde(default)]
pub repo_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TechDebt {
@@ -595,22 +545,6 @@ pub struct TechDebt {
pub effort_estimate: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ContextWorkspace {
pub name: String,
pub namespace: String,
pub pinned_files: Vec<String>,
pub active_task_ids: Vec<String>,
pub saved_at: u64,
#[serde(default)]
pub repo_name: Option<String>,
#[serde(default)]
pub git_branch: Option<String>,
#[serde(default)]
pub vcs_revision: Option<String>,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GateRecord {
+26 -207
View File
@@ -261,21 +261,9 @@ impl McpResource for SessionDeltaResource {
.unwrap_or_else(|e| e.into_inner());
let active_tasks: Vec<_> = tasks.iter().filter(|t| t.is_active()).collect();
let notes = state
.code
.sticky
.cache
.read()
.unwrap_or_else(|e| e.into_inner());
let active_notes: Vec<_> = notes
.iter()
.filter(|n| n.expires_at.map_or(true, |exp| exp > now))
.collect();
let delta = serde_json::json!({
"recent_changes": recent_changes,
"active_tasks": active_tasks,
"active_sticky_notes": active_notes,
"timestamp": now,
});
@@ -291,7 +279,7 @@ impl McpPrompt for ContextWarmupPrompt {
}
fn description(&self) -> Option<&'static str> {
Some(
"Consolidated context warmup prompt providing active tasks, pinned files, open tech debt, and sticky notes",
"Consolidated context warmup prompt providing active tasks, active decisions, and unresolved tech debt",
)
}
async fn get(
@@ -305,7 +293,7 @@ impl McpPrompt for ContextWarmupPrompt {
"role": "user",
"content": {
"type": "text",
"text": "Perform a complete context warmup. Please read memory://session/delta, memory://tasks/active, memory://tech_debt/active, and memory://pinned_files to synthesize current project focus and active workspace constraints."
"text": "Perform a complete context warmup. Please read memory://session/delta, memory://tasks/active, memory://decisions/active, and memory://tech_debt/unresolved to synthesize current project focus and active workspace constraints."
}
}
]
@@ -397,28 +385,6 @@ impl MemoryHandler {
Ok(serde_json::to_string_pretty(&*items)?)
}
}
struct PinnedFilesResource;
#[async_trait]
impl McpResource for PinnedFilesResource {
fn uri(&self) -> &'static str {
"memory://pinned_files"
}
fn name(&self) -> &'static str {
"Pinned Files"
}
fn description(&self) -> Option<&'static str> {
Some("Currently pinned files in the workspace")
}
async fn read(&self, state: Arc<MemoryState>) -> crate::error::Result<String> {
let items = state
.project
.pinned_files
.cache
.read()
.unwrap_or_else(|e| e.into_inner());
Ok(serde_json::to_string_pretty(&*items)?)
}
}
struct MilestonesResource;
#[async_trait]
@@ -473,7 +439,6 @@ impl MemoryHandler {
register_resource!(TerminalHistoryResource);
register_resource!(RecentActivitiesResource);
register_resource!(PinnedFilesResource);
register_resource!(MilestonesResource);
struct ArchiveRoutinePrompt;
#[async_trait]
@@ -529,8 +494,6 @@ impl MemoryHandler {
register!(tasks::TasksHandler);
register!(tasks::MilestonesHandler);
register!(notes::StickyNotesHandler);
register!(notes::HandoffMemosHandler);
register!(notes::AddSessionSummaryHandler);
register!(notes::GenerateStandupReportHandler);
@@ -542,16 +505,11 @@ impl MemoryHandler {
register!(meta::SearchErrorFixesHandler);
register!(meta::LogCodeChangeHandler);
register!(meta::QueryRecentChangesHandler);
register!(meta::LearnPreferenceHandler);
register!(meta::ReadPreferencesHandler);
register!(meta::OmniSearchHandler);
register!(meta::GetProjectHealthHandler);
register!(env::EnvironmentHandler);
register!(workspaces::PinnedFilesHandler);
register!(workspaces::ContextWorkspacesHandler);
register!(workspaces::PrChecklistHandler);
register!(workspaces::SnippetsHandler);
register!(vision::ClipboardHandler);
@@ -574,7 +532,6 @@ impl MemoryHandler {
register!(meta::GetNextActionableTasksHandler);
register!(meta::LogHypothesisHandler);
register!(meta::QueryHypothesesHandler);
register!(workspaces::DiffContextWorkspacesHandler);
register!(meta::GetPreflightContextHandler);
register!(graph::ResolveStaleSymbolsHandler);
register!(graph::SummarizeSubgraphHandler);
@@ -774,14 +731,13 @@ impl MemoryHandler {
"log_error_fix" => "ERROR_FIX",
"tech_debt" => "TECH_DEBT",
"tasks" | "milestones" => "TASK",
"sticky_notes" | "handoff_memos" => "STICKY_NOTE",
"handoff_memos" => "STICKY_NOTE",
"manage_checkpoint" => "CHECKPOINT",
"manage_subagent_namespace" => "SUBAGENT",
"snippets" => "SNIPPET",
"search_web" => "WEB_SEARCH",
"omni_search" => "OMNI_SEARCH",
"environment" => "ENVIRONMENT",
"pinned_files" | "context_workspaces" | "pr_checklist" => "WORKSPACE",
_ => "TOOL",
};
@@ -880,157 +836,25 @@ pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) ->
.to_string(),
)
}
"sticky_notes" => {
let act = args.get("action").and_then(|v| v.as_str()).unwrap_or("add");
let preview = args
.get("content")
.and_then(|v| v.as_str())
.map(|c| c.chars().take(40).collect::<String>())
.unwrap_or_default();
(
"Sticky Notes",
format!("{}: {}", act, preview)
.trim_end_matches(": ")
.to_string(),
)
}
"clipboard" => {
let act = args
.get("action")
.and_then(|v| v.as_str())
.unwrap_or("read");
("Clipboard", act.to_string())
}
"snippets" => {
let act = args
.get("action")
.and_then(|v| v.as_str())
.unwrap_or("search");
let q = args.get("query").and_then(|v| v.as_str()).unwrap_or("");
(
"Snippets",
format!("{}: {}", act, q).trim_end_matches(": ").to_string(),
)
}
"log_code_change" => {
let file = args
.get("file_path")
.or_else(|| args.get("file"))
.or_else(|| args.get("target_file"))
.and_then(|v| v.as_str());
let summary = args
.get("summary")
.or_else(|| args.get("description"))
.and_then(|v| v.as_str());
let d = match (file, summary) {
(Some(f), Some(s)) => format!("{}: {}", f, s),
(Some(f), None) => f.to_string(),
(None, Some(s)) => s.to_string(),
(None, None) => String::new(),
};
("Log Code Change", d)
}
"log_error_fix" => {
let d = args
.get("error")
.or_else(|| args.get("summary"))
.or_else(|| args.get("solution"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
("Log Error Fix", d)
}
"create_entities" => {
let d = if let Some(entities) = args.get("entities").and_then(|v| v.as_array()) {
let names: Vec<&str> = entities
.iter()
.filter_map(|e| {
e.get("name")
.or_else(|| e.get("entity_name"))
.and_then(|n| n.as_str())
})
.take(3)
.collect();
if !names.is_empty() {
let mut s = names.join(", ");
if entities.len() > 3 {
s.push_str(&format!(" (+{} more)", entities.len() - 3));
}
s
} else {
format!("{} entity/entities", entities.len())
}
} else if let Some(name) = args
.get("name")
.or_else(|| args.get("entity_name"))
.and_then(|v| v.as_str())
{
name.to_string()
} else {
String::new()
};
("Create Entities", d)
}
"create_relations" => {
let d = if let Some(relations) = args.get("relations").and_then(|v| v.as_array()) {
if let Some(first) = relations.first() {
let from = first.get("from").and_then(|v| v.as_str()).unwrap_or("?");
let to = first.get("to").and_then(|v| v.as_str()).unwrap_or("?");
let pred = first
.get("relation_type")
.or_else(|| first.get("predicate"))
.and_then(|v| v.as_str())
.unwrap_or("->");
if relations.len() > 1 {
format!("{} {} {} (+{} more)", from, pred, to, relations.len() - 1)
} else {
format!("{} {} {}", from, pred, to)
}
} else {
format!("{} relation(s)", relations.len())
}
} else {
String::new()
};
("Create Relations", d)
}
"omni_search" | "search_graph" | "search_web" => {
let d = args
.get("query")
.and_then(|v| v.as_str())
.map(|q| format!("\"{}\"", q))
let names = args
.get("entities")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|e| e.get("name").and_then(|n| n.as_str()))
.collect::<Vec<_>>()
.join(", ")
})
.unwrap_or_default();
("Search", d)
}
_ => {
let d = args
.get("title")
.or_else(|| args.get("action"))
.or_else(|| args.get("summary"))
.or_else(|| args.get("description"))
.or_else(|| args.get("name"))
.or_else(|| args.get("query"))
.or_else(|| args.get("file_path"))
.or_else(|| args.get("file"))
.or_else(|| args.get("path"))
.and_then(|v| v.as_str())
.unwrap_or("");
(name, d.to_string())
("Create Entities", names)
}
_ => return name.to_string(),
};
let mut truncated = detail.trim().to_string();
if truncated.len() > 80 {
let valid_len = truncated.floor_char_boundary(77);
truncated.truncate(valid_len);
truncated.push_str("...");
}
if !truncated.is_empty() {
format!("{}: {}", action, truncated)
if detail.is_empty() {
action.to_string()
} else {
format!("{}", action)
format!("{}: {}", action, detail)
}
}
@@ -1047,20 +871,20 @@ async fn execute_tool_span(
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use tempfile::tempdir;
use serde_json::json;
#[test]
fn test_format_tool_activity_description() {
let msg1 = format_tool_activity_description(
"log_code_change",
&json!({ "file": "src/lib.rs", "summary": "Refactored library" }),
let msg = format_tool_activity_description(
"tasks",
&serde_json::json!({ "action": "add", "title": "Refactor router" }),
);
assert_eq!(msg1, "Log Code Change: src/lib.rs: Refactored library");
assert_eq!(msg, "Tasks: add: Refactor router");
let msg2 = format_tool_activity_description(
"create_entities",
&json!({ "entities": [{ "name": "EntityA" }, { "name": "EntityB" }] }),
&serde_json::json!({ "entities": [{ "name": "EntityA" }, { "name": "EntityB" }] }),
);
assert_eq!(msg2, "Create Entities: EntityA, EntityB");
@@ -1115,7 +939,7 @@ mod tests {
assert!(
resources_arr
.iter()
.any(|r| r["uri"] == "memory://pinned_files")
.any(|r| r["uri"] == "memory://decisions/active")
);
assert!(
resources_arr
@@ -1292,8 +1116,8 @@ mod tests {
.as_array()
.expect("tools array expected");
assert!(
tools.len() >= 40,
"Expected at least 40 registered MCP tools, found {}",
tools.len() >= 30,
"Expected at least 30 registered MCP tools, found {}",
tools.len()
);
@@ -1353,16 +1177,11 @@ mod tests {
("tasks", json!({"action": "list"})),
("decisions", json!({"action": "query"})),
("tech_debt", json!({"action": "list"})),
("sticky_notes", json!({"action": "read"})),
("pinned_files", json!({"action": "list"})),
("context_workspaces", json!({"action": "list"})),
("pr_checklist", json!({"action": "get"})),
("snippets", json!({"action": "search", "query": "test"})),
("clipboard", json!({"action": "read"})),
("environment", json!({"action": "read_fingerprint"})),
("omni_search", json!({"query": "test"})),
("get_project_health", json!({})),
("read_preferences", json!({})),
("find_orphans", json!({})),
("sweep_graph_health", json!({})),
("get_next_actionable_tasks", json!({})),
+26 -19
View File
@@ -253,15 +253,35 @@ impl MemoryIndex {
})?;
let target_limit = if is_wildcard_fallback { 10 } else { 50 };
let fetch_limit = if namespace.is_some() {
// Fetch deeper candidate pool so other namespaces do not starve the target namespace
1000
let final_query: Box<dyn tantivy::query::Query> = if let Some(ns) = namespace {
let ns_term_query: Box<dyn tantivy::query::Query> = Box::new(tantivy::query::TermQuery::new(
tantivy::Term::from_field_text(self.namespace_field, ns),
tantivy::schema::IndexRecordOption::Basic,
));
let ns_filter: Box<dyn tantivy::query::Query> = if ns != "global" {
let global_term_query = Box::new(tantivy::query::TermQuery::new(
tantivy::Term::from_field_text(self.namespace_field, "global"),
tantivy::schema::IndexRecordOption::Basic,
));
Box::new(tantivy::query::BooleanQuery::new(vec![
(tantivy::query::Occur::Should, ns_term_query),
(tantivy::query::Occur::Should, global_term_query),
]))
} else {
ns_term_query
};
Box::new(tantivy::query::BooleanQuery::new(vec![
(tantivy::query::Occur::Must, q),
(tantivy::query::Occur::Must, ns_filter),
]))
} else {
target_limit
q
};
let top_docs = searcher.search(
&q,
&tantivy::collector::TopDocs::with_limit(fetch_limit).order_by_score(),
&final_query,
&tantivy::collector::TopDocs::with_limit(target_limit).order_by_score(),
)?;
let mut results = Vec::with_capacity(target_limit);
@@ -287,20 +307,7 @@ impl MemoryIndex {
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let doc_ns = retrieved_doc
.get_first(self.namespace_field)
.and_then(|v| v.as_str())
.unwrap_or("");
if let Some(ns) = namespace
&& doc_ns != ns
&& doc_ns != "global"
{
continue;
}
results.push((id, doc_type, title, body, score));
if results.len() >= target_limit {
break;
}
}
Ok(results)
}
-25
View File
@@ -15,9 +15,6 @@ pub struct GenericEvent {
pub struct ProjectStores {
pub tasks: Store<Vec<Task>>,
pub milestones: Store<Vec<Milestone>>,
pub pr_checklists: Store<Vec<PrChecklistItem>>,
pub context_workspaces: Store<Vec<ContextWorkspace>>,
pub pinned_files: Store<Vec<PinnedFile>>,
pub snapshots: Store<Vec<StateSnapshot>>,
}
@@ -27,7 +24,6 @@ pub struct CodeStores {
pub adrs: Store<Vec<Adr>>,
pub error_fixes: Store<Vec<ErrorFix>>,
pub tech_debts: Store<Vec<TechDebt>>,
pub sticky: Store<Vec<StickyNote>>,
pub hypotheses: Store<Vec<Hypothesis>>,
}
@@ -36,7 +32,6 @@ pub struct EnvironmentStores {
pub env_requirements: Store<Vec<EnvRequirement>>,
pub environments: Store<Vec<EnvironmentDetail>>,
pub gates: Store<Vec<GateRecord>>,
pub prefs: Store<HashMap<String, Preference>>,
}
pub struct TelemetryStores {
@@ -112,9 +107,6 @@ impl MemoryState {
project: ProjectStores {
tasks: Store::new("tasks", db.clone()),
milestones: Store::new("milestones", db.clone()),
pr_checklists: Store::new("pr_checklists", db.clone()),
context_workspaces: Store::new("context_workspaces", db.clone()),
pinned_files: Store::new("pinned_files", db.clone()),
snapshots: Store::new("state_snapshots", db.clone()),
},
code: CodeStores {
@@ -123,7 +115,6 @@ impl MemoryState {
adrs: Store::new("adrs", db.clone()),
error_fixes: Store::new("error_fixes", db.clone()),
tech_debts: Store::new("tech_debts", db.clone()),
sticky: Store::new("sticky_notes", db.clone()),
hypotheses: Store::new("hypotheses", db.clone()),
},
env: EnvironmentStores {
@@ -131,7 +122,6 @@ impl MemoryState {
env_requirements: Store::new("env_requirements", db.clone()),
environments: Store::new("environments", db.clone()),
gates: Store::new("gates", db.clone()),
prefs: Store::new("preferences", db.clone()),
},
telemetry: TelemetryStores {
session_summaries: Store::new("session_summaries", db.clone()),
@@ -527,21 +517,6 @@ impl SearchService {
}
}
});
self.state.code.sticky.read_with(|sticky| {
for note in sticky.iter() {
if uncached_texts.len() >= 50 {
break;
}
let content_preview = note.content.chars().take(200).collect::<String>();
uncached_texts.push(note.content.clone());
uncached_meta.push((
"StickyNote".to_string(),
"sticky".to_string(),
content_preview,
));
}
});
self.state.read_graph(|graph| {
for entity in graph.entities.values() {
if let Some(ns) = filter_namespace {
+28 -6
View File
@@ -446,7 +446,10 @@ impl<T: DeserializeOwned + Default + Serialize + Send + Sync + 'static> Store<T>
// Expensive serialization and granular extraction run completely unblocked outside the lock
let full_bytes_res = serde_json::to_vec(&new_snapshot);
let granular_entries = serde_json::to_value(&new_snapshot)
let granular_entries = full_bytes_res
.as_ref()
.ok()
.and_then(|bytes| serde_json::from_slice::<serde_json::Value>(bytes).ok())
.map(|val| Self::extract_granular_entries(&self.key, &val))
.unwrap_or_default();
@@ -454,7 +457,10 @@ impl<T: DeserializeOwned + Default + Serialize + Send + Sync + 'static> Store<T>
granular_entries.iter().map(|(k, _)| k.clone()).collect();
let mut removed_keys = Vec::new();
{
let mut known = self.known_granular_keys.write().unwrap_or_else(|e| e.into_inner());
let mut known = self
.known_granular_keys
.write()
.unwrap_or_else(|e| e.into_inner());
for old_k in known.iter() {
if !new_keys.contains(old_k) {
removed_keys.push(old_k.clone());
@@ -473,7 +479,12 @@ impl<T: DeserializeOwned + Default + Serialize + Send + Sync + 'static> Store<T>
if self
.queue
.push_batch(self.key.clone(), batch_inserts.clone(), removed_keys.clone(), self.flushed.clone())
.push_batch(
self.key.clone(),
batch_inserts.clone(),
removed_keys.clone(),
self.flushed.clone(),
)
.is_none()
{
tracing::warn!(
@@ -521,7 +532,10 @@ impl<T: DeserializeOwned + Default + Serialize + Send + Sync + 'static> Store<T>
};
let full_bytes_res = serde_json::to_vec(&new_snapshot);
let granular_entries = serde_json::to_value(&new_snapshot)
let granular_entries = full_bytes_res
.as_ref()
.ok()
.and_then(|bytes| serde_json::from_slice::<serde_json::Value>(bytes).ok())
.map(|val| Self::extract_granular_entries(&self.key, &val))
.unwrap_or_default();
@@ -529,7 +543,10 @@ impl<T: DeserializeOwned + Default + Serialize + Send + Sync + 'static> Store<T>
granular_entries.iter().map(|(k, _)| k.clone()).collect();
let mut removed_keys = Vec::new();
{
let mut known = self.known_granular_keys.write().unwrap_or_else(|e| e.into_inner());
let mut known = self
.known_granular_keys
.write()
.unwrap_or_else(|e| e.into_inner());
for old_k in known.iter() {
if !new_keys.contains(old_k) {
removed_keys.push(old_k.clone());
@@ -548,7 +565,12 @@ impl<T: DeserializeOwned + Default + Serialize + Send + Sync + 'static> Store<T>
if let Some(rx) = self
.queue
.push_batch_async(self.key.clone(), batch_inserts, removed_keys, self.flushed.clone())
.push_batch_async(
self.key.clone(),
batch_inserts,
removed_keys,
self.flushed.clone(),
)
.await
{
let _ = rx.await;
+15 -726
View File
@@ -1,23 +1,6 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// Extracts the current contents of the OS clipboard (text, HTML, files, and image).
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ReadClipboardTool {}
/// Writes contents to the OS clipboard. You can provide any combination of text, HTML, file paths, or an image path.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct WriteClipboardTool {
/// Optional plain text to write to the clipboard.
pub text: Option<String>,
/// Optional HTML to write to the clipboard (useful for rich text pasting).
pub html: Option<String>,
/// Optional absolute file paths to write to the clipboard as a Windows File Drop.
pub files: Option<Vec<String>>,
/// Optional absolute path to a local image to write to the clipboard.
pub image_path: Option<String>,
}
/// Create new entities in the knowledge graph. Note: entity_type MUST ALWAYS be PascalCase (e.g. DatabaseTable, McpTool, File).
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct CreateEntitiesTool {
@@ -166,39 +149,6 @@ pub struct VisualizeGraphTool {
pub namespace: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum StickyNoteAction {
#[serde(alias = "ADD", alias = "Add", alias = "create", alias = "insert")]
Add,
#[serde(alias = "READ", alias = "Read", alias = "list", alias = "get")]
Read,
#[serde(alias = "DELETE", alias = "Delete", alias = "remove", alias = "rm")]
Delete,
#[serde(alias = "CLEAR", alias = "Clear", alias = "purge")]
Clear,
}
/// Ephemeral sticky notes with TTL for scratchpad items, ports, and transient notes.
/// Actions:
/// - 'add': Add note. Required: content. Optional: ttl_seconds, session_only.
/// - 'read': Read all active notes.
/// - 'delete': Delete note. Required: index (1-based integer).
/// - 'clear': Clear all active notes.
/// Next steps on error: Ensure required fields for the specific action are supplied.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct StickyNotesTool {
/// Action to perform: 'add', 'read', 'delete', or 'clear'.
pub action: StickyNoteAction,
/// Content of the sticky note (required for 'add').
pub content: Option<String>,
/// 1-indexed position of the sticky note to delete (required for 'delete').
pub index: Option<usize>,
/// Optional time-to-live in seconds (for 'add').
pub ttl_seconds: Option<u64>,
/// Optional flag to auto-expire the note at the end of the current session (for 'add').
pub session_only: Option<bool>,
}
/// Condense or summarize an entity's observations to reduce size.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
@@ -209,124 +159,6 @@ pub struct CondenseEntityTool {
pub summarized_observations: Vec<String>,
}
/// Add a new task to the task tracker.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct AddTaskTool {
/// The title of the task.
pub title: String,
/// A detailed description of the task.
pub description: String,
/// The associated git branch, if any.
pub git_branch: Option<String>,
/// Optional parent task ID to create a nested sub-task.
pub parent_id: Option<String>,
/// Optional list of task IDs this task depends on.
pub dependencies: Option<Vec<String>>,
/// Optional namespace (defaults to 'default').
pub namespace: Option<String>,
/// Optional repository name.
pub repo_name: Option<String>,
/// Optional task priority: 'low', 'medium', 'high', 'urgent'.
pub priority: Option<String>,
/// Optional assigned subagent role or identifier.
pub assigned_agent: Option<String>,
/// Optional verification command to validate criteria completion.
pub verification_command: Option<String>,
}
/// Delete a task and all its children.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct DeleteTaskTool {
/// The ID of the task to delete.
pub id: String,
}
/// Update the status of an existing task.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct UpdateTaskStatusTool {
/// The ID of the task to update.
pub id: String,
/// The new status of the task (e.g., 'pending', 'completed', 'cancelled').
#[schemars(description = "Must be 'pending', 'completed', or 'cancelled'")]
pub status: String,
}
/// List all currently active tasks.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ListActiveTasksTool {
/// Optional git branch to filter tasks by.
pub git_branch: Option<String>,
/// Optional summary level: 'compact' (IDs and titles), 'detailed', or 'full'. Defaults to 'detailed'.
pub summary_level: Option<String>,
/// Optional token budget cap. Truncates results to stay within max_tokens.
pub max_tokens: Option<usize>,
}
/// Store a reusable code snippet.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct StoreSnippetTool {
/// The name of the snippet.
pub name: String,
/// The programming language of the snippet.
pub language: String,
/// The code snippet content.
pub code: String,
/// A description of what the snippet does.
pub description: String,
/// Optional classification tags (e.g. ['rust', 'async', 'mcp']).
pub tags: Option<Vec<String>>,
/// Optional project namespace.
pub namespace: Option<String>,
/// Optional repository name.
pub repo_name: Option<String>,
/// Optional origin file path where the snippet was extracted from.
pub origin_file: Option<String>,
/// Optional line range in origin file (e.g. 'L10-L45').
pub line_range: Option<String>,
}
/// Add classification tags to an existing code snippet.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct TagSnippetTool {
/// The name of the snippet to tag.
pub name: String,
/// Array of tags (e.g. ['rust', 'async', 'mcp']).
pub tags: Vec<String>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum SnippetSearchMode {
#[serde(alias = "HYBRID", alias = "Hybrid")]
Hybrid,
#[serde(alias = "KEYWORD", alias = "Keyword")]
Keyword,
#[serde(alias = "SEMANTIC", alias = "Semantic")]
Semantic,
}
/// Search stored code snippets using Hybrid BM25 + Vector ranking or keyword search.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct SearchSnippetsTool {
/// The search query.
pub query: String,
/// Search mode: 'hybrid' (BM25 + Semantic Tags), 'keyword', or 'semantic'. Defaults to 'hybrid'.
pub mode: Option<SnippetSearchMode>,
/// Optional classification tags to filter by.
pub tags: Option<Vec<String>>,
/// Limit the number of results to avoid context limit overflow. Defaults to 10.
pub limit: Option<usize>,
/// Include the full code content of the matched items. If false, returns only IDs and descriptions. Defaults to false.
pub include_body: Option<bool>,
}
/// Delete a stored code snippet.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct DeleteSnippetTool {
/// The name of the snippet to delete.
pub name: String,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum SubgraphFormat {
@@ -351,15 +183,6 @@ pub struct GetSubgraphTool {
pub max_tokens: Option<usize>,
}
/// Search historical error fixes using keyword search or semantic vector similarity and return matching solutions.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct SuggestErrorFixTool {
/// The exact stack trace or error signature.
pub stack_trace: String,
/// Maximum number of suggested fixes to return. Defaults to 3.
pub limit: Option<usize>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum CheckpointAction {
@@ -382,66 +205,6 @@ pub struct ManageCheckpointTool {
pub namespace: Option<String>,
}
/// Save a named memory snapshot checkpoint before risky operations or subagent invocations.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct CheckpointStateTool {
/// The unique name for this state snapshot checkpoint.
pub name: String,
/// Optional description of why this checkpoint was created.
pub description: Option<String>,
}
/// Restore a previously saved memory snapshot checkpoint.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct RestoreStateTool {
/// The name of the checkpoint to restore.
pub name: String,
}
/// Log an architectural decision record (ADR).
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct LogDecisionTool {
/// The title of the decision.
pub title: String,
/// The context or problem requiring a decision.
pub context: String,
/// The decision made.
pub decision: String,
/// The consequence of the decision.
#[serde(alias = "consequences")]
pub consequence: String,
/// The ID of an older ADR this decision supersedes, if any.
pub supersedes: Option<String>,
/// Optional project namespace.
pub namespace: Option<String>,
/// Optional repository name.
pub repo_name: Option<String>,
/// Optional author or architect behind the decision.
pub author: Option<String>,
/// Optional affected system components or crates.
pub affected_components: Option<Vec<String>>,
/// Optional alternative designs or libraries considered.
pub alternatives_considered: Option<Vec<String>>,
}
/// Query architectural decision records.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct QueryDecisionsTool {
/// Optional search query.
pub query: Option<String>,
/// Limit the number of results to avoid context limit overflow. Defaults to 5.
pub limit: Option<usize>,
/// Include the full body/content of the matched ADRs. If false, returns only titles and IDs (recommended for LLMs to prevent context bloat). Defaults to false.
pub include_body: Option<bool>,
}
/// Delete an architectural decision record (ADR).
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct DeleteDecisionTool {
/// The ID of the decision to delete.
pub id: String,
}
/// Merge two entities in the knowledge graph into one.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct MergeEntitiesTool {
@@ -455,19 +218,6 @@ pub struct MergeEntitiesTool {
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct FindOrphansTool {}
/// Record a user preference or behavior to adapt future interactions.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct LearnPreferenceTool {
/// The key for the preference.
pub key: String,
/// The value of the preference.
pub value: String,
}
/// Read all learned user preferences.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ReadPreferencesTool {}
/// Log a complex error and its fix for future reference.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct LogErrorFixTool {
@@ -497,44 +247,6 @@ pub struct SearchErrorFixesTool {
/// Include the full solution details. If false, returns only error signatures. Defaults to false.
pub include_body: Option<bool>,
}
/// Pin a file to keep it explicitly in the context workspace.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct PinFileTool {
/// The namespace to pin the file in.
#[serde(default = "crate::models::default_namespace")]
pub namespace: String,
/// The absolute path of the file to pin.
pub file_path: String,
/// The associated git branch, if any.
pub git_branch: Option<String>,
/// Optional reason for pinning this file.
pub pin_reason: Option<String>,
/// Optional symbol focus or line range.
pub symbol_or_line: Option<String>,
/// Optional repository name.
pub repo_name: Option<String>,
}
/// Unpin a file from the context workspace.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct UnpinFileTool {
/// The namespace the file is pinned in.
#[serde(default = "crate::models::default_namespace")]
pub namespace: String,
/// The absolute path of the file to unpin.
pub file_path: String,
}
/// List all currently pinned files.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ListPinnedFilesTool {
/// Optional namespace to filter by.
pub namespace: Option<String>,
/// Optional git branch to filter by.
pub git_branch: Option<String>,
}
/// Add a summary of the current session.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct AddSessionSummaryTool {
@@ -544,105 +256,6 @@ pub struct AddSessionSummaryTool {
#[serde(default = "crate::models::default_namespace")]
pub namespace: String,
}
/// Get a timeline of major project events.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
#[allow(dead_code)]
pub struct GetProjectTimelineTool {
/// Optional namespace to restrict the timeline to.
pub namespace: Option<String>,
}
/// Leave a memo for the next session or agent.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct LeaveHandoffMemoTool {
/// The content of the memo.
pub content: String,
/// The namespace to leave the memo in.
#[serde(default = "crate::models::default_namespace")]
pub namespace: String,
}
/// Read pending handoff memos.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ReadHandoffMemosTool {
/// Optional namespace to restrict the read to.
pub namespace: Option<String>,
}
/// Clear handoff memos after reading.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ClearHandoffMemosTool {
/// Array of memo IDs to clear.
pub ids: Vec<String>,
}
/// Update the environment fingerprint (e.g., OS, tool versions).
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct UpdateEnvFingerprintTool {
/// The namespace to update the fingerprint for.
#[serde(default = "crate::models::default_namespace")]
pub namespace: String,
/// A map of tool names to their versions.
pub tool_versions: std::collections::HashMap<String, String>,
}
/// Read the environment fingerprint.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ReadEnvFingerprintTool {
/// The namespace to read the fingerprint for.
#[serde(default = "crate::models::default_namespace")]
pub namespace: String,
}
/// Log a required environment variable or configuration.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct LogEnvRequirementTool {
/// The namespace.
#[serde(default = "crate::models::default_namespace")]
pub namespace: String,
/// The environment variable key (e.g., DATABASE_URL).
pub key: String,
/// A description of what the variable is used for.
pub description: String,
/// Whether the variable contains a secret.
pub is_secret: bool,
}
/// Add a new milestone.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct AddMilestoneTool {
/// The title of the milestone.
pub title: String,
/// The namespace for the milestone.
#[serde(default = "crate::models::default_namespace")]
pub namespace: String,
/// Optional target completion date.
pub target_date: Option<String>,
/// Optional description of milestone scope.
pub description: Option<String>,
/// Optional deliverables or task checklist.
pub deliverables: Option<Vec<String>>,
/// Optional repository name.
pub repo_name: Option<String>,
}
/// Update the status of a milestone.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct UpdateMilestoneTool {
/// The ID of the milestone to update.
pub id: String,
/// The new status (e.g., 'active', 'completed').
pub status: String,
}
/// List all milestones.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ListMilestonesTool {
/// Optional namespace to restrict the list to.
pub namespace: Option<String>,
}
/// Generate a standup report for a specific time window.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct GenerateStandupReportTool {
@@ -652,165 +265,6 @@ pub struct GenerateStandupReportTool {
/// The number of hours to look back for activity.
pub hours_lookback: u64,
}
/// Register a new infrastructure environment.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct RegisterEnvironmentTool {
/// The namespace.
#[serde(default = "crate::models::default_namespace")]
pub namespace: String,
/// The name of the environment (e.g., 'staging', 'prod').
pub name: String,
/// The URL or connection string for the environment.
pub url: String,
/// A description of the environment.
pub description: String,
/// Whether a VPN is required to access the environment.
pub requires_vpn: bool,
}
/// Get details about a registered environment.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct GetEnvironmentDetailsTool {
/// The namespace to retrieve details for.
#[serde(default = "crate::models::default_namespace")]
pub namespace: String,
}
/// Add an item to the PR checklist.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct AddPrChecklistItemTool {
/// The namespace.
#[serde(default = "crate::models::default_namespace")]
pub namespace: String,
/// The description of the checklist item.
pub description: String,
/// Optional checklist category (e.g. 'Security', 'Testing', 'Formatting').
pub category: Option<String>,
/// Optional automated verification command or script.
pub automated_check: Option<String>,
/// Optional checked state.
pub is_checked: Option<bool>,
/// Optional repository name.
pub repo_name: Option<String>,
}
/// Get the PR checklist.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct GetPrChecklistTool {
/// The namespace.
#[serde(default = "crate::models::default_namespace")]
pub namespace: String,
}
/// Clear the PR checklist.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ClearPrChecklistTool {
/// The namespace.
#[serde(default = "crate::models::default_namespace")]
pub namespace: String,
}
/// Log a technical debt record.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct LogTechDebtTool {
/// The namespace.
#[serde(default = "crate::models::default_namespace")]
pub namespace: String,
/// A description of the technical debt.
pub description: String,
/// The ideal solution to resolve the debt.
pub ideal_solution: String,
/// The associated git commit hash, if any.
pub git_commit: Option<String>,
/// The associated git branch, if any.
pub git_branch: Option<String>,
/// Optional symbol references (e.g. ['MemoryIndex', 'switchTab']) associated with the tech debt.
pub symbol_references: Option<Vec<String>>,
/// Optional line range (e.g. 'L123-L145') associated with the tech debt.
pub line_range: Option<String>,
/// Optional debt severity: 'low', 'medium', 'high', 'critical'.
pub severity: Option<String>,
/// Optional repository name.
pub repo_name: Option<String>,
/// Optional file path where debt exists.
pub file_path: Option<String>,
/// Optional workaround currently in place.
pub workaround: Option<String>,
/// Optional effort estimate to fix (e.g. '1 hour', '2 days').
pub effort_estimate: Option<String>,
}
/// Resolve a technical debt record.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ResolveTechDebtTool {
/// The ID of the technical debt record to resolve.
pub id: String,
}
/// List technical debt records.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ListTechDebtTool {
/// The namespace.
#[serde(default = "crate::models::default_namespace")]
pub namespace: String,
/// Whether to include resolved technical debt in the results.
pub include_resolved: bool,
/// Optional summary level: 'compact' (IDs and descriptions), 'detailed', or 'full'. Defaults to 'detailed'.
pub summary_level: Option<String>,
/// Optional token budget cap. Truncates results to stay within max_tokens.
pub max_tokens: Option<usize>,
}
/// Save the current context workspace.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct SaveContextWorkspaceTool {
/// The namespace for the workspace.
#[serde(default = "crate::models::default_namespace")]
pub namespace: String,
/// The name to save the workspace as.
pub name: String,
/// Array of pinned file paths.
pub pinned_files: Vec<String>,
/// Array of active task IDs.
pub active_task_ids: Vec<String>,
/// Optional description of the context workspace purpose.
pub description: Option<String>,
/// Optional git branch associated with this workspace.
pub git_branch: Option<String>,
/// Optional repository name.
pub repo_name: Option<String>,
/// Optional VCS revision or commit hash.
pub vcs_revision: Option<String>,
}
/// Load a saved context workspace.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct LoadContextWorkspaceTool {
/// The namespace of the workspace.
#[serde(default = "crate::models::default_namespace")]
pub namespace: String,
/// The name of the workspace to load.
pub name: String,
}
/// List all saved context workspaces.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ListContextWorkspacesTool {
/// The namespace to list workspaces for.
pub namespace: Option<String>,
}
/// Delete a saved context workspace.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct DeleteContextWorkspaceTool {
/// The namespace of the workspace.
#[serde(default = "crate::models::default_namespace")]
pub namespace: String,
/// The name of the workspace to delete.
pub name: String,
}
/// Search across all memory stores (Graph, Tasks, Snippets, ADRs, Error Fixes, Tech Debt) using Hybrid Reciprocal Rank Fusion (BM25 + Dense Vectors).
/// Highly recommended for discovery. Supports Lucene query syntax.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
@@ -905,16 +359,6 @@ pub struct QueryHypothesesTool {
/// Optional search query text.
pub query: Option<String>,
}
/// Compare two saved context workspaces and return a structured diff of pinned files, active tasks, and metadata.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct DiffContextWorkspacesTool {
/// The name of the first context workspace.
pub workspace_a: String,
/// The name of the second context workspace.
pub workspace_b: String,
}
#[cfg(test)]
mod tests {
use super::*;
@@ -990,20 +434,6 @@ pub struct SemanticCodeSearchTool {
pub struct ReadDirectoryArchitectureTool {
pub directory: String,
}
/// Create a point-in-time state snapshot of memory stores for safety.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct CreateSnapshotTool {
pub description: String,
pub namespace: Option<String>,
}
/// Restore a point-in-time state snapshot of memory stores.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct RestoreSnapshotTool {
pub snapshot_id: String,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum SubagentNamespaceAction {
@@ -1022,19 +452,6 @@ pub struct ManageSubagentNamespaceTool {
/// For 'condense': whether to auto-purge the subagent namespace after promotion. Defaults to true.
pub purge_after_promotion: Option<bool>,
}
/// Create an isolated memory namespace for a subagent session.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct CreateSubagentNamespaceTool {
pub subagent_id: String,
}
/// Purge an isolated subagent memory namespace after session completion.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct PurgeSubagentNamespaceTool {
pub subagent_id: String,
}
/// Find all source locations and AST chunks where a specific symbol (function, struct, method, type) is referenced or called.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct FindSymbolReferencesTool {
@@ -1068,7 +485,7 @@ pub struct QueryGitDiffsTool {
pub limit: Option<usize>,
}
/// Promote a transient sticky note or active task observation into a permanent Knowledge Graph entity.
/// Promote a task observation or finding into a permanent Knowledge Graph entity.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct PromoteToEntityTool {
/// Note text or content to promote.
@@ -1090,17 +507,7 @@ pub struct AnalyzeImpactTool {
/// Optional file path.
pub file_path: Option<String>,
}
/// Auto-condense a subagent's memory namespace and promote its high-value entities/relations to the global Knowledge Graph.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct CondenseSubagentNamespaceTool {
/// The subagent namespace ID to consolidate.
pub subagent_id: String,
/// Whether to auto-purge the subagent namespace after promotion. Defaults to true.
pub purge_after_promotion: Option<bool>,
}
/// Get a consolidated 1-page executive summary of current active branch, active task, pinned files, open tech debt, and diagnostic hypotheses in 1 turn.
/// Get a consolidated 1-page executive summary of current active branch, active task, open tech debt, and diagnostic hypotheses in 1 turn.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct GetPreflightContextTool {
/// Optional namespace. Defaults to 'default'.
@@ -1163,18 +570,6 @@ pub struct AutoSessionCheckpointTool {
#[serde(default = "crate::models::default_namespace")]
pub namespace: String,
}
/// Hybrid BM25 & semantic tag search across stored code snippets, returning ranked code patterns with relevance scores.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct SearchSnippetsHybridTool {
/// Query string (e.g. 'rate limiter', 'websocket client', 'shutdown poll').
pub query: String,
/// Optional tags to filter by.
pub tags: Option<Vec<String>>,
/// Maximum number of snippets to return. Defaults to 10.
pub limit: Option<usize>,
}
// Consolidated Smart Management Tools
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
@@ -1322,124 +717,6 @@ pub struct MilestonesTool {
/// Optional repository name.
pub repo_name: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum PinnedFileAction {
#[serde(alias = "pin", alias = "PIN", alias = "Pin")]
Pin,
#[serde(alias = "unpin", alias = "UNPIN", alias = "Unpin")]
Unpin,
#[serde(alias = "list", alias = "LIST", alias = "List")]
List,
}
/// Pinned working set files management (pin, unpin, list).
/// Actions:
/// - 'pin': Pin file to active focus set. Required: path. Optional: namespace.
/// - 'unpin': Unpin file from focus set. Required: path. Optional: namespace.
/// - 'list': List pinned files. Optional: namespace.
/// Next steps on error: Provide exact file path for pin/unpin operations.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct PinnedFilesTool {
/// Action to perform: 'pin', 'unpin', or 'list'.
pub action: PinnedFileAction,
/// Absolute or workspace relative file path (required for 'pin' and 'unpin').
pub path: Option<String>,
/// Optional namespace.
pub namespace: Option<String>,
/// Optional reason for pinning this file.
pub pin_reason: Option<String>,
/// Optional symbol focus or line range.
pub symbol_or_line: Option<String>,
/// Optional repository name.
pub repo_name: Option<String>,
/// Optional git branch.
pub git_branch: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ContextWorkspaceAction {
#[serde(alias = "save", alias = "SAVE", alias = "Save")]
Save,
#[serde(alias = "load", alias = "LOAD", alias = "Load")]
Load,
#[serde(alias = "list", alias = "LIST", alias = "List")]
List,
#[serde(alias = "delete", alias = "DELETE", alias = "Delete")]
Delete,
#[serde(alias = "diff", alias = "DIFF", alias = "Diff")]
Diff,
}
/// Context workspace snapshots management (save, load, list, delete, diff).
/// Actions:
/// - 'save': Save current pinned files & task IDs. Required: name. Optional: pinned_files, active_task_ids, namespace.
/// - 'load': Restore saved workspace state. Required: name. Optional: namespace.
/// - 'list': List saved context workspaces. Optional: namespace.
/// - 'delete': Delete saved workspace. Required: name. Optional: namespace.
/// - 'diff': Compare two context workspaces. Required: name, other_name. Optional: namespace.
/// Next steps on error: Check workspace name or list existing workspaces first.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ContextWorkspacesTool {
/// Action to perform: 'save', 'load', 'list', 'delete', or 'diff'.
pub action: ContextWorkspaceAction,
/// Workspace name (required for 'save', 'load', 'delete', 'diff').
pub name: Option<String>,
/// Second workspace name (required for 'diff').
pub other_name: Option<String>,
/// Pinned file paths (for 'save').
pub pinned_files: Option<Vec<String>>,
/// Active task IDs (for 'save').
pub active_task_ids: Option<Vec<String>>,
/// Optional namespace.
pub namespace: Option<String>,
/// Optional description of the context workspace purpose.
pub description: Option<String>,
/// Optional git branch associated with this workspace.
pub git_branch: Option<String>,
/// Optional repository name.
pub repo_name: Option<String>,
/// Optional VCS revision or commit hash.
pub vcs_revision: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum PrChecklistAction {
#[serde(alias = "add", alias = "ADD", alias = "Add")]
Add,
#[serde(alias = "get", alias = "GET", alias = "Get")]
Get,
#[serde(alias = "clear", alias = "CLEAR", alias = "Clear")]
Clear,
}
/// Pre-commit and Pull Request checklist management (add, get, clear).
/// Actions:
/// - 'add': Add PR checklist item. Required: description. Optional: namespace.
/// - 'get': Get PR checklist items. Optional: namespace.
/// - 'clear': Clear PR checklist. Optional: namespace.
/// Next steps on error: Provide description for 'add' action.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct PrChecklistTool {
/// Action to perform: 'add', 'get', or 'clear'.
pub action: PrChecklistAction,
/// Checklist item description (required for 'add').
pub description: Option<String>,
/// Optional namespace.
pub namespace: Option<String>,
/// Optional checklist category (e.g. 'Security', 'Testing', 'Formatting').
pub category: Option<String>,
/// Optional automated verification command or script.
pub automated_check: Option<String>,
/// Optional checked state.
pub is_checked: Option<bool>,
/// Optional repository name.
pub repo_name: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum SnippetAction {
@@ -1535,6 +812,12 @@ pub struct DecisionsTool {
pub affected_components: Option<Vec<String>>,
/// Optional alternative designs or libraries considered.
pub alternatives_considered: Option<Vec<String>>,
/// Optional superseded ADR ID.
pub supersedes: Option<String>,
/// Limit the number of query results. Defaults to 20.
pub limit: Option<usize>,
/// Include the full body of the matched decisions. Defaults to true.
pub include_body: Option<bool>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
@@ -1562,8 +845,10 @@ pub struct TechDebtTool {
#[serde(alias = "tech_debt_id", alias = "debt_id")]
pub id: Option<String>,
/// Tech debt description (required for 'log').
#[serde(alias = "desc", alias = "title")]
#[serde(alias = "desc")]
pub description: Option<String>,
/// Optional title or summary of tech debt.
pub title: Option<String>,
/// Ideal solution (optional for 'log').
#[serde(alias = "solution", alias = "fix")]
pub ideal_solution: Option<String>,
@@ -1591,6 +876,10 @@ pub struct TechDebtTool {
pub workaround: Option<String>,
/// Optional effort estimate to fix.
pub effort_estimate: Option<String>,
/// Summary level for list action: 'compact', 'detailed', or 'full'.
pub summary_level: Option<String>,
/// Optional token budget cap for list output.
pub max_tokens: Option<usize>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]