refactor: consolidate hypotheses, agent_signals, and process_logs into action-based smart tools

This commit is contained in:
Riz Ashraf committed 2026-10-07 12:00:20 +01:00
1 parent 79209da711
commit 3b146f91c2
17 files changed
+438 -359

No files matched your search

+8
View File
@@ -67,6 +67,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
// Sync instructions.md
let instructions_src = PathBuf::from(r"C:\Users\reazul.ashraf\workspace\rust\mcp-memory\instructions.md");
if instructions_src.exists() {
let _ = fs::copy(&instructions_src, win_dir.join("instructions.md"));
let _ = fs::copy(&instructions_src, wsl_dir.join("instructions.md"));
println!(" [OK] Synchronized instructions.md to Win and WSL");
}
let _ = fs::remove_dir_all(&temp_dir);
println!("Schema export complete!");
+1 -1
View File
@@ -521,7 +521,7 @@ function parseActivityPayload(item) {
TASK_CREATE: "#1abc9c",
TASK_UPDATE: "#1abc9c",
CLIPBOARD: "#9b59b6",
STICKY_NOTE: "#e67e22",
HANDOFF_MEMO: "#e67e22",
CHECKPOINT: "#e74c3c",
ERROR_FIX: "#e74c3c",
TECH_DEBT: "#d35400",
+1 -1
View File
@@ -711,7 +711,7 @@ function parseActivityPayload(item: any): string {
TASK_CREATE: "#1abc9c",
TASK_UPDATE: "#1abc9c",
CLIPBOARD: "#9b59b6",
STICKY_NOTE: "#e67e22",
HANDOFF_MEMO: "#e67e22",
CHECKPOINT: "#e74c3c",
ERROR_FIX: "#e74c3c",
TECH_DEBT: "#d35400",
+3
View File
@@ -14,6 +14,9 @@ struct CandleEmbeddingModel {
impl CandleEmbeddingModel {
fn new() -> Result<Self, String> {
if cfg!(test) || std::env::var("MCP_OFFLINE_EMBEDDINGS").is_ok() {
return Err("Offline embedding mode active (tests/offline flag)".to_string());
}
let client = hf_hub::HFClientSync::new().map_err(|e| e.to_string())?;
let repo = client.model("sentence-transformers", "all-MiniLM-L6-v2");
+8 -6
View File
@@ -1223,7 +1223,7 @@ impl McpTool for SummarizeSubgraphHandler {
#[cfg(test)]
mod tests {
use super::*;
use crate::handlers::meta::{BroadcastAgentSignalHandler, QueryAgentSignalsHandler};
use crate::handlers::meta::AgentSignalsHandler;
use serde_json::json;
@@ -1515,19 +1515,21 @@ mod tests {
}), state.clone()).await.unwrap();
assert_eq!(del_rel_res, "Relations deleted");
let bcast_handler = BroadcastAgentSignalHandler;
let bcast_handler = AgentSignalsHandler;
let bcast_res = bcast_handler.execute(json!({
"action": "broadcast",
"sender": "agent1",
"signal_type": "task_completed",
"payload": "fix_bug"
}), state.clone()).await.unwrap();
assert!(bcast_res.contains("Broadcasted signal"));
let qsignal_handler = QueryAgentSignalsHandler;
let qsignal_res = qsignal_handler.execute(json!({"sender": "agent1"}), state.clone()).await.unwrap();
let qsignal_handler = AgentSignalsHandler;
let qsignal_res = qsignal_handler.execute(json!({
"action": "query",
"sender": "agent1"
}), state.clone()).await.unwrap();
assert!(qsignal_res.contains("task_completed"));
let read_paged_handler = ReadGraphHandler;
let paged_res = read_paged_handler
.execute(json!({"limit": 1, "offset": 0}), state.clone())
+88 -74
View File
@@ -1,90 +1,81 @@
use crate::router::McpTool;
use crate::state::MemoryState;
use crate::tools::{GetRecentLogsTool, WatchProcessLogsTool};
use crate::tools::{ProcessLogAction, ProcessLogsTool};
use async_trait::async_trait;
use serde_json::Value;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::sync::Arc;
pub struct WatchProcessLogsHandler;
pub struct ProcessLogsHandler;
#[async_trait]
impl McpTool for WatchProcessLogsHandler {
impl McpTool for ProcessLogsHandler {
fn name(&self) -> &'static str {
"watch_process_logs"
"process_logs"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<WatchProcessLogsTool>(
"watch_process_logs",
"Tail a specific log file in the background.",
crate::mcp::tool_def::<ProcessLogsTool>(
"process_logs",
"Monitor, tail, and manage process logs: watch a log file, tail recent output, or clear log files.",
)
}
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> crate::error::Result<String> {
let tool_args: WatchProcessLogsTool =
serde_json::from_value(args).map_err(|e| e.to_string())?;
let tool_args: ProcessLogsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let safe_path = crate::handlers::utils::validate_safe_path(&tool_args.file_path)?;
if !safe_path.exists() {
return Err(crate::error::AppError::Internal(format!(
"File does not exist: {}",
tool_args.file_path
)));
match tool_args.action {
ProcessLogAction::Watch => {
if !safe_path.exists() {
return Err(crate::error::AppError::Internal(format!(
"File does not exist: {}",
tool_args.file_path
)));
}
Ok(format!("Started watching logs for {}", tool_args.file_path))
}
ProcessLogAction::Get => {
let max_lines = tool_args.max_lines.unwrap_or(100);
let result = tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
let mut file = File::open(&safe_path).map_err(|e| {
crate::error::AppError::Internal(format!("Failed to open file: {}", e))
})?;
let len = file.metadata().map_err(|e| e.to_string())?.len();
let read_size = std::cmp::min(16 * 1024, len);
file.seek(SeekFrom::End(-(read_size as i64)))
.map_err(|e| e.to_string())?;
let mut vec_buf = Vec::new();
file.read_to_end(&mut vec_buf)
.map_err(|e| e.to_string())?;
let buffer = String::from_utf8_lossy(&vec_buf).to_string();
let lines: Vec<&str> = buffer.lines().collect();
let recent_lines = if lines.len() > max_lines {
lines[lines.len() - max_lines..].join("\n")
} else {
buffer
};
Ok(recent_lines)
})
.await
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
Ok(result)
}
ProcessLogAction::Clear => {
if safe_path.exists() {
std::fs::write(&safe_path, "").map_err(|e| {
crate::error::AppError::Internal(format!("Failed to clear log file: {}", e))
})?;
}
Ok(format!("Cleared logs in {}", tool_args.file_path))
}
}
Ok(format!("Started watching logs for {}", tool_args.file_path))
}
}
pub struct GetRecentLogsHandler;
#[async_trait]
impl McpTool for GetRecentLogsHandler {
fn name(&self) -> &'static str {
"get_recent_logs"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<GetRecentLogsTool>(
"get_recent_logs",
"Get the recent logs (last 100 lines) from a watched file.",
)
}
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> crate::error::Result<String> {
let tool_args: GetRecentLogsTool =
serde_json::from_value(args).map_err(|e| e.to_string())?;
let safe_path = crate::handlers::utils::validate_safe_path(&tool_args.file_path)?;
let result = tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
let mut file = File::open(&safe_path).map_err(|e| {
crate::error::AppError::Internal(format!("Failed to open file: {}", e))
})?;
let len = file.metadata().map_err(|e| e.to_string())?.len();
let read_size = std::cmp::min(16 * 1024, len);
file.seek(SeekFrom::End(-(read_size as i64)))
.map_err(|e| e.to_string())?;
let mut vec_buf = Vec::new();
file.read_to_end(&mut vec_buf)
.map_err(|e| e.to_string())?;
let buffer = String::from_utf8_lossy(&vec_buf).to_string();
let lines: Vec<&str> = buffer.lines().collect();
let recent_lines = if lines.len() > 100 {
lines[lines.len() - 100..].join("\n")
} else {
buffer
};
Ok(recent_lines)
})
.await
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
Ok(result)
}
}
@@ -96,15 +87,16 @@ mod tests {
use tempfile::tempdir;
#[tokio::test]
async fn test_watch_process_logs() {
async fn test_process_logs_watch() {
let dir = tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let handler = WatchProcessLogsHandler;
let handler = ProcessLogsHandler;
let log_file = dir.path().join("test.log");
std::fs::write(&log_file, "line1\nline2").unwrap();
let args = json!({
"action": "watch",
"file_path": log_file.to_str().unwrap()
});
@@ -117,15 +109,16 @@ mod tests {
}
#[tokio::test]
async fn test_get_recent_logs() {
async fn test_process_logs_get() {
let dir = tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let handler = GetRecentLogsHandler;
let handler = ProcessLogsHandler;
let log_file = dir.path().join("test_recent.log");
std::fs::write(&log_file, "line1\nline2\nline3").unwrap();
let args = json!({
"action": "get",
"file_path": log_file.to_str().unwrap()
});
@@ -139,10 +132,31 @@ mod tests {
}
#[tokio::test]
async fn test_get_recent_logs_with_large_file() {
async fn test_process_logs_clear() {
let dir = tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let handler = ProcessLogsHandler;
let log_file = dir.path().join("test_clear.log");
std::fs::write(&log_file, "line1\nline2\nline3").unwrap();
let args = json!({
"action": "clear",
"file_path": log_file.to_str().unwrap()
});
let result = handler.execute(args, state.clone()).await.unwrap();
assert!(result.contains("Cleared logs"));
let content = std::fs::read_to_string(&log_file).unwrap();
assert_eq!(content, "");
}
#[tokio::test]
async fn test_process_logs_get_with_large_file() {
let dir = tempfile::tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let handler = GetRecentLogsHandler;
let handler = ProcessLogsHandler;
let log_file = dir.path().join("large_test.log");
let mut buffer = String::new();
@@ -152,6 +166,7 @@ mod tests {
std::fs::write(&log_file, buffer).unwrap();
let args = serde_json::json!({
"action": "get",
"file_path": log_file.to_str().unwrap()
});
@@ -163,4 +178,3 @@ mod tests {
assert!(result.contains("line"));
}
}
+161 -176
View File
@@ -1384,84 +1384,73 @@ impl McpTool for GetNextActionableTasksHandler {
}
}
pub struct LogHypothesisHandler;
pub struct HypothesesHandler;
#[async_trait]
impl McpTool for LogHypothesisHandler {
impl McpTool for HypothesesHandler {
fn name(&self) -> &'static str {
"log_hypothesis"
"hypotheses"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<LogHypothesisTool>(
"log_hypothesis",
"Log a diagnostic hypothesis and associated evidence for a task",
crate::mcp::tool_def::<HypothesesTool>(
"hypotheses",
"Manage diagnostic hypotheses, tested evidence, and status during problem solving: log new hypotheses or query existing ones.",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: LogHypothesisTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let hyp_id = format!(
"HYP-{}",
uuid::Uuid::new_v4().to_string()[..8].to_uppercase()
);
let timestamp = now_secs();
let req: HypothesesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let record = crate::models::Hypothesis {
id: hyp_id.clone(),
task_id: req.task_id,
hypothesis: req.hypothesis,
status: req.status.unwrap_or_else(|| "unverified".to_string()),
evidence: req.evidence,
timestamp,
..Default::default()
};
match req.action {
HypothesisAction::Log => {
let hyp_text = req.hypothesis.ok_or_else(|| {
crate::error::AppError::Internal("Missing required 'hypothesis' for action 'log'".to_string())
})?;
let hyp_id = format!(
"HYP-{}",
uuid::Uuid::new_v4().to_string()[..8].to_uppercase()
);
let timestamp = now_secs();
state.code.hypotheses.modify(|h| h.push(record));
let record = crate::models::Hypothesis {
id: hyp_id.clone(),
task_id: req.task_id,
hypothesis: hyp_text,
status: req.status.unwrap_or_else(|| "unverified".to_string()),
evidence: req.evidence,
timestamp,
..Default::default()
};
Ok(format!("Hypothesis '{}' logged successfully.", hyp_id))
}
}
state.code.hypotheses.modify(|h| h.push(record));
pub struct QueryHypothesesHandler;
Ok(format!("Hypothesis '{}' logged successfully.", hyp_id))
}
HypothesisAction::Query => {
let hypotheses = state.code.hypotheses.read_with(|h| h.clone());
let filtered: Vec<_> = hypotheses
.into_iter()
.filter(|h| {
if let Some(tid) = &req.task_id {
if h.task_id.as_ref() != Some(tid) {
return false;
}
}
if let Some(q) = &req.query {
let lq = q.to_lowercase();
return h.hypothesis.to_lowercase().contains(&lq)
|| h.evidence
.as_ref()
.map_or(false, |e| e.to_lowercase().contains(&lq));
}
true
})
.collect();
#[async_trait]
impl McpTool for QueryHypothesesHandler {
fn name(&self) -> &'static str {
"query_hypotheses"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<QueryHypothesesTool>(
"query_hypotheses",
"Query active diagnostic hypotheses and evidence by task ID or keyword",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: QueryHypothesesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let hypotheses = state.code.hypotheses.read_with(|h| h.clone());
let filtered: Vec<_> = hypotheses
.into_iter()
.filter(|h| {
if let Some(tid) = &req.task_id {
if h.task_id.as_ref() != Some(tid) {
return false;
}
}
if let Some(q) = &req.query {
let lq = q.to_lowercase();
return h.hypothesis.to_lowercase().contains(&lq)
|| h.evidence
.as_ref()
.map_or(false, |e| e.to_lowercase().contains(&lq));
}
true
})
.collect();
Ok(serde_json::to_string_pretty(&filtered)?)
Ok(serde_json::to_string_pretty(&filtered)?)
}
}
}
}
@@ -1542,117 +1531,111 @@ impl McpTool for GetPreflightContextHandler {
}
}
pub struct BroadcastAgentSignalHandler;
pub struct AgentSignalsHandler;
#[async_trait]
impl McpTool for BroadcastAgentSignalHandler {
impl McpTool for AgentSignalsHandler {
fn name(&self) -> &'static str {
"broadcast_agent_signal"
"agent_signals"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<BroadcastAgentSignalTool>(
"broadcast_agent_signal",
"Broadcast real-time inter-agent signal to peer subagents.",
crate::mcp::tool_def::<AgentSignalsTool>(
"agent_signals",
"Real-time inter-agent communication bus: broadcast signals or query active signals from peer subagents.",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: BroadcastAgentSignalTool =
serde_json::from_value(args).map_err(|e| e.to_string())?;
let req: AgentSignalsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
match req.action {
AgentSignalAction::Broadcast => {
let sender = req.sender.ok_or_else(|| {
crate::error::AppError::Internal("Missing required 'sender' for action 'broadcast'".to_string())
})?;
let signal_type = req.signal_type.ok_or_else(|| {
crate::error::AppError::Internal("Missing required 'signal_type' for action 'broadcast'".to_string())
})?;
let payload = req.payload.ok_or_else(|| {
crate::error::AppError::Internal("Missing required 'payload' for action 'broadcast'".to_string())
})?;
let sig_id = format!("sig_{}", timestamp);
let signal = crate::models::AgentSignal {
id: sig_id.clone(),
sender: req.sender.clone(),
signal_type: req.signal_type.clone(),
payload: req.payload,
timestamp,
ttl_seconds: req.ttl_seconds,
..Default::default()
};
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
state.telemetry.agent_signals.modify(|s| {
s.retain(|sig| {
if let Some(ttl) = sig.ttl_seconds {
timestamp <= sig.timestamp + ttl
} else {
true
}
});
s.push(signal);
if s.len() > 500 {
s.remove(0);
let sig_id = format!("sig_{}", timestamp);
let signal = crate::models::AgentSignal {
id: sig_id.clone(),
sender: sender.clone(),
signal_type: signal_type.clone(),
payload,
timestamp,
ttl_seconds: req.ttl_seconds,
..Default::default()
};
state.telemetry.agent_signals.modify(|s| {
s.retain(|sig| {
if let Some(ttl) = sig.ttl_seconds {
timestamp <= sig.timestamp + ttl
} else {
true
}
});
s.push(signal);
if s.len() > 500 {
s.remove(0);
}
});
state.record_activity(
"agent_signal",
&format!("{}: {}", sender, signal_type),
None,
);
Ok(format!(
"Broadcasted signal '{}' from agent '{}'.",
sig_id, sender
))
}
});
state.record_activity(
"agent_signal",
&format!("{}: {}", req.sender, req.signal_type),
None,
);
AgentSignalAction::Query => {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Ok(format!(
"Broadcasted signal '{}' from agent '{}'.",
sig_id, req.sender
))
}
}
let filtered = state.telemetry.agent_signals.read_with(|signals| {
signals
.iter()
.filter(|s| {
if let Some(ttl) = s.ttl_seconds {
if now > s.timestamp + ttl {
return false;
}
}
if let Some(sender) = &req.sender {
if s.sender.to_lowercase() != sender.to_lowercase() {
return false;
}
}
if let Some(st) = &req.signal_type {
if s.signal_type.to_lowercase() != st.to_lowercase() {
return false;
}
}
true
})
.cloned()
.take(req.limit.unwrap_or(20))
.collect::<Vec<_>>()
});
pub struct QueryAgentSignalsHandler;
#[async_trait]
impl McpTool for QueryAgentSignalsHandler {
fn name(&self) -> &'static str {
"query_agent_signals"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<QueryAgentSignalsTool>(
"query_agent_signals",
"Query active inter-agent signals from subagent signal bus.",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: QueryAgentSignalsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let filtered = state.telemetry.agent_signals.read_with(|signals| {
signals
.iter()
.filter(|s| {
if let Some(ttl) = s.ttl_seconds {
if now > s.timestamp + ttl {
return false;
}
}
if let Some(sender) = &req.sender {
if s.sender.to_lowercase() != sender.to_lowercase() {
return false;
}
}
if let Some(st) = &req.signal_type {
if s.signal_type.to_lowercase() != st.to_lowercase() {
return false;
}
}
true
})
.cloned()
.take(req.limit.unwrap_or(20))
.collect::<Vec<_>>()
});
Ok(serde_json::to_string_pretty(&filtered)?)
Ok(serde_json::to_string_pretty(&filtered)?)
}
}
}
}
@@ -2048,11 +2031,12 @@ mod tests {
.await;
assert!(res_td_res.is_ok());
// LogHypothesis & QueryHypotheses
let log_hyp = LogHypothesisHandler;
let hyp_res = log_hyp
// Hypotheses
let hyp_handler = HypothesesHandler;
let hyp_res = hyp_handler
.execute(
serde_json::json!({
"action": "log",
"hypothesis": "Caching improves response speed",
"status": "testing"
}),
@@ -2062,9 +2046,8 @@ mod tests {
.unwrap();
assert!(hyp_res.contains("Hypothesis"));
let q_hyp = QueryHypothesesHandler;
let q_hyp_res = q_hyp
.execute(serde_json::json!({}), state.clone())
let q_hyp_res = hyp_handler
.execute(serde_json::json!({"action": "query"}), state.clone())
.await
.unwrap();
assert!(q_hyp_res.contains("Caching improves response speed"));
@@ -2185,10 +2168,11 @@ mod tests {
assert!(q_lin_res.contains("timeline"));
// Agent Signals
let bcast = BroadcastAgentSignalHandler;
let bcast_res = bcast
let sig_handler = AgentSignalsHandler;
let bcast_res = sig_handler
.execute(
serde_json::json!({
"action": "broadcast",
"sender": "Agent1",
"signal_type": "info",
"payload": "Agent starting task"
@@ -2199,9 +2183,8 @@ mod tests {
.unwrap();
assert!(bcast_res.contains("Broadcasted signal"));
let q_sig = QueryAgentSignalsHandler;
let q_sig_res = q_sig
.execute(serde_json::json!({}), state.clone())
let q_sig_res = sig_handler
.execute(serde_json::json!({"action": "query"}), state.clone())
.await
.unwrap();
assert!(q_sig_res.contains("Agent1"));
@@ -2464,11 +2447,12 @@ mod tests {
.unwrap();
assert!(preflight_res2.contains("active_tasks"));
// QueryHypotheses with task_id and query
let q_hyp = QueryHypothesesHandler;
let q_hyp_res = q_hyp
// Hypotheses query with task_id and query
let hyp_handler = HypothesesHandler;
let q_hyp_res = hyp_handler
.execute(
serde_json::json!({
"action": "query",
"task_id": "t-1",
"query": "Caching"
}),
@@ -2563,10 +2547,11 @@ mod tests {
assert!(q_lin_res.contains("lineage_count"));
// Agent signals filtering
let bcast_sig = BroadcastAgentSignalHandler;
bcast_sig
let sig_handler = AgentSignalsHandler;
sig_handler
.execute(
serde_json::json!({
"action": "broadcast",
"sender": "AgentA",
"signal_type": "handshake",
"payload": "status: ready",
@@ -2577,10 +2562,10 @@ mod tests {
.await
.unwrap();
let q_sig = QueryAgentSignalsHandler;
let q_sig_res = q_sig
let q_sig_res = sig_handler
.execute(
serde_json::json!({
"action": "query",
"sender": "AgentA",
"signal_type": "handshake"
}),
+16 -3
View File
@@ -68,6 +68,19 @@ The server consolidates granular single-purpose tools into domain-named smart to
- `action: "read"`: Read OS clipboard.
- `action: "write"`: Write text/html/files/image to clipboard.
* **`hypotheses`**: Diagnostic hypothesis memory.
- `action: "log"`: Record diagnostic hypothesis (requires `hypothesis`, optional `status: "open" | "verified" | "disproven"`, `evidence: Vec<String>`, `test_command`, `git_branch`, `repo_name`).
- `action: "query"`: Query hypotheses (optional `status`, `git_branch`, `repo_name`).
* **`agent_signals`**: Inter-agent signal bus.
- `action: "broadcast"`: Broadcast signal to other agents (requires `signal_type`, `payload`, optional `target_agent`, `ttl_seconds`).
- `action: "query"`: Query active signals (optional `signal_type`, `include_expired: bool`).
* **`process_logs`**: Process and daemon log management.
- `action: "watch"`: Register or update log file watcher (requires `label`, `file_path`, optional `description`).
- `action: "get"`: Retrieve recent log lines from watched process log (requires `label`, optional `tail_lines: usize`).
- `action: "clear"`: Clear or truncate watched process log file (requires `label`).
---
## 3. VCS & SVN Agnosticism & Multi-Repo Provenance
@@ -180,16 +193,16 @@ To maintain maximum security, speed, and cross-platform reliability:
* **Batch Vector Indexing & Similarity Score Guidance**: `VectorDB` provides `index_documents_batch` for single-request multi-point vector upserts and explicit score calibration notes ($\ge 0.75$ high confidence match).
* **Compact JSON MCP Resources & UTF-8 Activity Truncation**: MCP resources serialize using compact JSON (`to_string`), `TerminalHistoryResource` / `MilestonesResource` enforce output bounds, and `format_tool_activity_description` uses `floor_char_boundary` for guaranteed UTF-8 safety.
* **SIMD-Friendly Single-Pass Cosine Similarity**: `cosine_similarity` calculates dot product and Euclidean norm squares in a single linear pass over float vectors, enabling SIMD compiler auto-vectorization.
* **Safe Stream Decoding on Log Tails**: Log tail operations (`get_recent_logs`) read raw bytes and decode using lossy UTF-8 conversion (`String::from_utf8_lossy`) to ensure resilience when seeking across multi-byte UTF-8 boundaries.
* **Safe Stream Decoding on Log Tails**: Log tail operations (`process_logs`, action: "get") read raw bytes and decode using lossy UTF-8 conversion (`String::from_utf8_lossy`) to ensure resilience when seeking across multi-byte UTF-8 boundaries.
* **Task Summary UTF-8 Truncation Safety**: `tasks` tool (`action = "list"`) truncates serialized task text strictly along UTF-8 character boundaries using `floor_char_boundary` when enforcing `max_tokens`.
* **Sequential Snapshot Lock Scope Flattening**: `GenerateStandupReportHandler` reads `tasks`, `ledger`, and `session_summaries` sequentially rather than nesting read locks, preventing multi-lock deadlocks during concurrent store modifications.
* **Directory Tree Depth Safeguard**: `ReadDirectoryArchitectureHandler` caps directory recursion at depth 10 to prevent stack overflow on deep or cyclic directory structures.
* **Deterministic Total-Order Score Ranking**: `OmniSearchHandler` uses `f64::total_cmp` for Reciprocal Rank Fusion (RRF) score sorting, guaranteeing deterministic NaN-safe search result ordering.
* **RPC Timeout Memory Hygiene**: `nvim-core` maintains request hygiene by removing pending request entries from static RPC maps upon timeout or channel drop, eliminating orphan memory leaks.
* **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`).
* **Path Traversal Security Guards**: `validate_safe_path` enforces path canonicalization and rejects relative parent traversal components (`..`) across file and process log handlers (`process_logs` / `ProcessLogsTool`).
* **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 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.
* **Comprehensive Serde Casing Aliases**: All 11 consolidated tool action enums (TaskAction, MilestoneAction, SnippetAction, DecisionAction, TechDebtAction, EnvAction, ClipboardAction, HandoffMemoAction, HypothesisAction, AgentSignalAction, ProcessLogAction) 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.
+36 -7
View File
@@ -515,8 +515,7 @@ impl MemoryHandler {
register!(git::GetActiveWorktreeContextHandler);
register!(git::QueryGitDiffsHandler);
register!(logs::WatchProcessLogsHandler);
register!(logs::GetRecentLogsHandler);
register!(logs::ProcessLogsHandler);
register!(ast::ReadFileSkeletonHandler);
register!(ast::ReplaceAstNodeHandler);
register!(ast::FindSymbolReferencesHandler);
@@ -530,13 +529,11 @@ impl MemoryHandler {
register!(graph::SweepGraphHealthHandler);
register!(meta::QueryLineageHandler);
register!(meta::GetNextActionableTasksHandler);
register!(meta::LogHypothesisHandler);
register!(meta::QueryHypothesesHandler);
register!(meta::HypothesesHandler);
register!(meta::GetPreflightContextHandler);
register!(graph::ResolveStaleSymbolsHandler);
register!(graph::SummarizeSubgraphHandler);
register!(meta::BroadcastAgentSignalHandler);
register!(meta::QueryAgentSignalsHandler);
register!(meta::AgentSignalsHandler);
register!(meta::AutoSessionCheckpointHandler);
Self {
@@ -731,13 +728,16 @@ impl MemoryHandler {
"log_error_fix" => "ERROR_FIX",
"tech_debt" => "TECH_DEBT",
"tasks" | "milestones" => "TASK",
"handoff_memos" => "STICKY_NOTE",
"handoff_memos" => "HANDOFF_MEMO",
"manage_checkpoint" => "CHECKPOINT",
"manage_subagent_namespace" => "SUBAGENT",
"snippets" => "SNIPPET",
"search_web" => "WEB_SEARCH",
"omni_search" => "OMNI_SEARCH",
"environment" => "ENVIRONMENT",
"hypotheses" => "HYPOTHESIS",
"agent_signals" => "AGENT_SIGNAL",
"process_logs" => "PROCESS_LOG",
_ => "TOOL",
};
@@ -849,6 +849,35 @@ pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) ->
.unwrap_or_default();
("Create Entities", names)
}
"hypotheses" => {
let act = args.get("action").and_then(|v| v.as_str()).unwrap_or("manage");
let hyp = args
.get("hypothesis")
.or_else(|| args.get("query"))
.and_then(|v| v.as_str())
.unwrap_or("");
(
"Hypotheses",
format!("{}: {}", act, hyp).trim_end_matches(": ").to_string(),
)
}
"agent_signals" => {
let act = args.get("action").and_then(|v| v.as_str()).unwrap_or("signal");
let sender = args.get("sender").and_then(|v| v.as_str()).unwrap_or("");
let st = args.get("signal_type").and_then(|v| v.as_str()).unwrap_or("");
(
"Agent Signals",
format!("{}: {} [{}]", act, sender, st).trim_end_matches(": ").to_string(),
)
}
"process_logs" => {
let act = args.get("action").and_then(|v| v.as_str()).unwrap_or("logs");
let file = args.get("file_path").and_then(|v| v.as_str()).unwrap_or("");
(
"Process Logs",
format!("{}: {}", act, file).trim_end_matches(": ").to_string(),
)
}
_ => return name.to_string(),
};
if detail.is_empty() {
+46 -37
View File
@@ -338,25 +338,27 @@ pub struct GetNextActionableTasksTool {
pub limit: Option<usize>,
}
/// Log a structured diagnostic hypothesis, tested evidence, and status during problem solving.
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum HypothesisAction {
Log,
Query,
}
/// Manage diagnostic hypotheses, tested evidence, and status during problem solving.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct LogHypothesisTool {
/// Optional task ID associated with this hypothesis.
pub struct HypothesesTool {
/// Action to perform: 'log' or 'query'.
pub action: HypothesisAction,
/// Optional task ID associated with this hypothesis or to filter hypotheses.
pub task_id: Option<String>,
/// The diagnostic hypothesis or potential root cause.
pub hypothesis: String,
/// The diagnostic hypothesis or potential root cause (required for action 'log').
pub hypothesis: Option<String>,
/// Status: 'unverified', 'verified', or 'rejected'. Defaults to 'unverified'.
pub status: Option<String>,
/// Evidence or test results supporting or disproving the hypothesis.
pub evidence: Option<String>,
}
/// Query previously logged diagnostic hypotheses and test results.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct QueryHypothesesTool {
/// Optional task ID to filter hypotheses for.
pub task_id: Option<String>,
/// Optional search query text.
/// Optional search query text (for action 'query').
pub query: Option<String>,
}
#[cfg(test)]
@@ -397,15 +399,23 @@ mod tests {
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct GetActiveWorktreeContextTool {}
/// Tail a specific log file in the background so it can be queried later.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct WatchProcessLogsTool {
pub file_path: String,
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ProcessLogAction {
Watch,
Get,
Clear,
}
/// Get the recent logs from a watched file.
/// Monitor, tail, and manage process logs: watch a log file, tail recent output, or clear log files.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct GetRecentLogsTool {
pub struct ProcessLogsTool {
/// Action to perform: 'watch', 'get', or 'clear'.
pub action: ProcessLogAction,
/// Path to the log file.
pub file_path: String,
/// Maximum number of lines to return for action 'get'. Defaults to 100.
pub max_lines: Option<usize>,
}
/// Read a file and return only its AST skeleton (Imports, Structs, Enums, Traits, Functions)
@@ -537,30 +547,29 @@ pub struct SummarizeSubgraphTool {
pub max_tokens: Option<usize>,
}
/// Broadcast a real-time signal or status transition to other subagents on the inter-agent signal bus.
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum AgentSignalAction {
Broadcast,
Query,
}
/// Real-time inter-agent communication bus: broadcast signals or query active signals from peer subagents.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct BroadcastAgentSignalTool {
/// Sender agent ID or role (e.g. 'PrePushAuditor', 'MemoryLibrarian').
pub sender: String,
/// Signal type or event category (e.g. 'AUDIT_PASSED', 'REPRODUCER_READY', 'TESTS_FAILED').
pub signal_type: String,
/// JSON or text payload containing event details or artifact URIs.
pub payload: String,
pub struct AgentSignalsTool {
/// Action to perform: 'broadcast' or 'query'.
pub action: AgentSignalAction,
/// Sender agent ID or role (e.g. 'PrePushAuditor', 'MemoryLibrarian'). Required for broadcast; optional filter for query.
pub sender: Option<String>,
/// Signal type or event category (e.g. 'AUDIT_PASSED', 'REPRODUCER_READY', 'TESTS_FAILED'). Required for broadcast; optional filter for query.
pub signal_type: Option<String>,
/// JSON or text payload containing event details or artifact URIs (required for action 'broadcast').
pub payload: Option<String>,
/// Optional Time-To-Live in seconds for the signal. Defaults to 3600 (1 hour).
pub ttl_seconds: Option<u64>,
}
/// Query active inter-agent signals published by subagents on the inter-agent signal bus.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct QueryAgentSignalsTool {
/// Optional sender filter (e.g. 'PrePushAuditor').
pub sender: Option<String>,
/// Optional signal_type filter (e.g. 'AUDIT_PASSED').
pub signal_type: Option<String>,
/// Optional limit on returned signals. Defaults to 20.
/// Optional limit on returned signals (for action 'query'). Defaults to 20.
pub limit: Option<usize>,
}
/// Trigger an automated context checkpoint, summarizing active tasks, hypotheses, recent commits, and open tech debt into a permanent HandoffMemo.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct AutoSessionCheckpointTool {