mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-02 22:01:01 +02:00
feat(mcp): consolidate status/temporal into memory_status (4→1)
Tool Consolidation v2.2.0, Layer 1 commit 3/6 (3a). Advertised tools 27 → 24. Folds system_status + memory_health + memory_timeline + memory_changelog into one view-dispatched tool: view = health (default) | retention | timeline | changelog - Thin facade: each view forwards the same args envelope to the existing handler. No underlying arg struct uses deny_unknown_fields, so the `view` discriminator is ignored by each handler and per-view fields validate as before. The cognitive lock is never held across a forwarded call. - view='health' returns the byte-for-byte system_status shape (audit scripts parse it), incl. schema_introspection passthrough — verified by test_default_view_is_health asserting equality with execute_system_status. - All 4 old names remain hidden warn!+redirect aliases (removed v2.3.0). - Size annotation moved: memory_timeline (200k) → memory_status, kept in sync across the real loop, expected_max_result_size(), and both annotation tests. - Tests: count 27→24, 4 negative asserts, test_memory_status_views_and_aliases exercising all 4 views + 4 aliases. Gates: cargo test --workspace, cargo clippy -D warnings — clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8888634740
commit
32e6a6cd8d
3 changed files with 240 additions and 38 deletions
|
|
@ -290,29 +290,19 @@ impl McpServer {
|
|||
..Default::default()
|
||||
},
|
||||
// ================================================================
|
||||
// TEMPORAL TOOLS (v1.2+)
|
||||
// STATUS / TEMPORAL — unified `memory_status` tool (v2.2)
|
||||
// Folds system_status + memory_health + memory_timeline +
|
||||
// memory_changelog into one view-dispatched surface.
|
||||
// ================================================================
|
||||
ToolDescription {
|
||||
name: "memory_timeline".to_string(),
|
||||
description: Some("Browse memories chronologically. Returns memories in a time range, grouped by day. Defaults to last 7 days.".to_string()),
|
||||
input_schema: tools::timeline::schema(),
|
||||
..Default::default()
|
||||
},
|
||||
ToolDescription {
|
||||
name: "memory_changelog".to_string(),
|
||||
description: Some("View audit trail of memory changes. Per-memory: state transitions. System-wide: consolidations + recent state changes.".to_string()),
|
||||
input_schema: tools::changelog::schema(),
|
||||
name: "memory_status".to_string(),
|
||||
description: Some("Memory status & history. Views: 'health' (default — full system health + stats + FSRS preview + cognitive-module health + warnings + recommendations), 'retention' (lightweight retention dashboard: avg, distribution, trend), 'timeline' (browse memories chronologically, grouped by day), 'changelog' (audit trail of memory state changes — per-memory transitions or system-wide).".to_string()),
|
||||
input_schema: tools::memory_status::schema(),
|
||||
..Default::default()
|
||||
},
|
||||
// ================================================================
|
||||
// MAINTENANCE TOOLS (v1.7: system_status replaces health_check + stats)
|
||||
// ================================================================
|
||||
ToolDescription {
|
||||
name: "system_status".to_string(),
|
||||
description: Some("Combined system health and statistics. Returns status (healthy/degraded/critical/empty), full stats, FSRS preview, cognitive module health, state distribution, warnings, and recommendations.".to_string()),
|
||||
input_schema: tools::maintenance::system_status_schema(),
|
||||
..Default::default()
|
||||
},
|
||||
ToolDescription {
|
||||
name: "consolidate".to_string(),
|
||||
description: Some("Run FSRS-6 memory consolidation cycle. Applies decay, generates embeddings, and performs maintenance. Use when memories seem stale.".to_string()),
|
||||
|
|
@ -399,13 +389,8 @@ impl McpServer {
|
|||
},
|
||||
// ================================================================
|
||||
// AUTONOMIC TOOLS (v1.9+)
|
||||
// (memory_health folded into `memory_status` view='retention' in v2.2)
|
||||
// ================================================================
|
||||
ToolDescription {
|
||||
name: "memory_health".to_string(),
|
||||
description: Some("Retention dashboard. Returns avg retention, retention distribution (buckets: 0-20%, 20-40%, etc.), trend (improving/declining/stable), and recommendation. Lightweight alternative to full system_status focused on memory quality.".to_string()),
|
||||
input_schema: tools::health::schema(),
|
||||
..Default::default()
|
||||
},
|
||||
ToolDescription {
|
||||
name: "memory_graph".to_string(),
|
||||
description: Some("Subgraph export for visualization. Input: center_id or query, depth (1-3), max_nodes. Returns nodes with force-directed layout positions and edges with weights. Powers memory graph visualization.".to_string()),
|
||||
|
|
@ -473,7 +458,7 @@ impl McpServer {
|
|||
for tool in tools.iter_mut() {
|
||||
let max_chars: Option<u64> = match tool.name.as_str() {
|
||||
"search" => Some(300_000),
|
||||
"memory_timeline" => Some(200_000),
|
||||
"memory_status" => Some(200_000),
|
||||
"memory" => Some(100_000),
|
||||
"codebase" => Some(100_000),
|
||||
// v2.2: dedup action='scan' returns duplicate clusters +
|
||||
|
|
@ -649,9 +634,23 @@ impl McpServer {
|
|||
}
|
||||
|
||||
// ================================================================
|
||||
// SYSTEM STATUS (v1.7: replaces health_check + stats)
|
||||
// MEMORY STATUS — unified status/temporal tool (v2.2)
|
||||
// view = health (default) | retention | timeline | changelog
|
||||
// ================================================================
|
||||
"memory_status" => {
|
||||
tools::memory_status::execute(
|
||||
&self.storage,
|
||||
&self.cognitive,
|
||||
&self.output_config,
|
||||
request.arguments,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
// DEPRECATED (v2.2): folded into `memory_status`. Hidden aliases —
|
||||
// each calls the same underlying handler verbatim.
|
||||
"system_status" => {
|
||||
warn!("Tool 'system_status' is deprecated in v2.2. Use 'memory_status' (view='health').");
|
||||
tools::maintenance::execute_system_status(
|
||||
&self.storage,
|
||||
&self.cognitive,
|
||||
|
|
@ -939,13 +938,18 @@ impl McpServer {
|
|||
}
|
||||
|
||||
// ================================================================
|
||||
// TEMPORAL TOOLS (v1.2+)
|
||||
// TEMPORAL TOOLS (v1.2+) — DEPRECATED (v2.2): folded into
|
||||
// `memory_status` (view='timeline' / view='changelog'). Hidden aliases.
|
||||
// ================================================================
|
||||
"memory_timeline" => {
|
||||
warn!("Tool 'memory_timeline' is deprecated in v2.2. Use 'memory_status' (view='timeline').");
|
||||
tools::timeline::execute(&self.storage, &self.output_config, request.arguments)
|
||||
.await
|
||||
}
|
||||
"memory_changelog" => tools::changelog::execute(&self.storage, request.arguments).await,
|
||||
"memory_changelog" => {
|
||||
warn!("Tool 'memory_changelog' is deprecated in v2.2. Use 'memory_status' (view='changelog').");
|
||||
tools::changelog::execute(&self.storage, request.arguments).await
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// MAINTENANCE TOOLS (v1.2+, non-deprecated)
|
||||
|
|
@ -1043,7 +1047,11 @@ impl McpServer {
|
|||
// ================================================================
|
||||
// AUTONOMIC TOOLS (v1.9+)
|
||||
// ================================================================
|
||||
"memory_health" => tools::health::execute(&self.storage, request.arguments).await,
|
||||
// DEPRECATED (v2.2): folded into `memory_status` (view='retention').
|
||||
"memory_health" => {
|
||||
warn!("Tool 'memory_health' is deprecated in v2.2. Use 'memory_status' (view='retention').");
|
||||
tools::health::execute(&self.storage, request.arguments).await
|
||||
}
|
||||
"memory_graph" => tools::graph::execute(&self.storage, request.arguments).await,
|
||||
"composed_graph" => {
|
||||
tools::composed_graph::execute(&self.storage, request.arguments).await
|
||||
|
|
@ -1815,7 +1823,11 @@ mod tests {
|
|||
// v2.2 Tool Consolidation (Layer 1): 34 → 27 after `dedup` folds
|
||||
// find_duplicates + the 7 Phase-3 merge tools (8 → 1). Old names remain
|
||||
// dispatchable as hidden back-compat aliases but drop off the advertised list.
|
||||
assert_eq!(tools.len(), 27, "Expected exactly 27 tools after dedup consolidation");
|
||||
assert_eq!(
|
||||
tools.len(),
|
||||
24,
|
||||
"Expected exactly 24 tools after dedup + memory_status consolidation"
|
||||
);
|
||||
|
||||
let tool_names: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect();
|
||||
|
||||
|
|
@ -1849,12 +1861,21 @@ mod tests {
|
|||
"demote_memory should be removed in v1.7"
|
||||
);
|
||||
|
||||
// Temporal tools (v1.2)
|
||||
assert!(tool_names.contains(&"memory_timeline"));
|
||||
assert!(tool_names.contains(&"memory_changelog"));
|
||||
|
||||
// Maintenance tools (v1.7: system_status replaces health_check + stats)
|
||||
assert!(tool_names.contains(&"system_status"));
|
||||
// Status / temporal — unified `memory_status` tool (v2.2).
|
||||
// system_status + memory_health + memory_timeline + memory_changelog
|
||||
// folded in; old names dispatch as hidden aliases but are off the list.
|
||||
assert!(tool_names.contains(&"memory_status"));
|
||||
for old in [
|
||||
"system_status",
|
||||
"memory_health",
|
||||
"memory_timeline",
|
||||
"memory_changelog",
|
||||
] {
|
||||
assert!(
|
||||
!tool_names.contains(&old),
|
||||
"{old} should be folded into 'memory_status' in v2.2"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!tool_names.contains(&"health_check"),
|
||||
"health_check should be removed in v1.7"
|
||||
|
|
@ -1904,8 +1925,7 @@ mod tests {
|
|||
"session_context renamed to 'session_start' in v2.2"
|
||||
);
|
||||
|
||||
// Autonomic tools (v1.9)
|
||||
assert!(tool_names.contains(&"memory_health"));
|
||||
// Autonomic tools (v1.9) — memory_health folded into memory_status (v2.2)
|
||||
assert!(tool_names.contains(&"memory_graph"));
|
||||
assert!(tool_names.contains(&"composed_graph"));
|
||||
|
||||
|
|
@ -1956,6 +1976,41 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// v2.2: the 4 tools folded into `memory_status` must still dispatch, and
|
||||
/// each `view` of the new tool must resolve.
|
||||
#[tokio::test]
|
||||
async fn test_memory_status_views_and_aliases() {
|
||||
let (mut server, _dir) = test_server().await;
|
||||
let init_request = make_request("initialize", Some(init_params()));
|
||||
server.handle_request(init_request).await;
|
||||
|
||||
let calls: Vec<(&str, serde_json::Value)> = vec![
|
||||
// Deprecated aliases must still dispatch.
|
||||
("system_status", serde_json::json!({})),
|
||||
("memory_health", serde_json::json!({})),
|
||||
("memory_timeline", serde_json::json!({})),
|
||||
("memory_changelog", serde_json::json!({})),
|
||||
// New unified views.
|
||||
("memory_status", serde_json::json!({})), // default view = health
|
||||
("memory_status", serde_json::json!({"view": "retention"})),
|
||||
("memory_status", serde_json::json!({"view": "timeline"})),
|
||||
("memory_status", serde_json::json!({"view": "changelog"})),
|
||||
];
|
||||
|
||||
for (name, args) in calls {
|
||||
let request = make_request(
|
||||
"tools/call",
|
||||
Some(serde_json::json!({ "name": name, "arguments": args })),
|
||||
);
|
||||
let response = server.handle_request(request).await.unwrap();
|
||||
assert!(
|
||||
response.error.is_none(),
|
||||
"'{name}' {args} should resolve, got error: {:?}",
|
||||
response.error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tools_have_descriptions_and_schemas() {
|
||||
let (mut server, _dir) = test_server().await;
|
||||
|
|
@ -2171,7 +2226,9 @@ mod tests {
|
|||
fn expected_max_result_size(name: &str) -> Option<u64> {
|
||||
match name {
|
||||
"search" => Some(300_000),
|
||||
"memory_timeline" => Some(200_000),
|
||||
// v2.2: memory_timeline folded into memory_status (view='timeline');
|
||||
// the high-payload annotation moved with it.
|
||||
"memory_status" => Some(200_000),
|
||||
"memory" => Some(100_000),
|
||||
"codebase" => Some(100_000),
|
||||
// v2.2: dedup action='scan' returns clusters + candidates + policy.
|
||||
|
|
@ -2191,7 +2248,7 @@ mod tests {
|
|||
let result = response.result.unwrap();
|
||||
let tools = result["tools"].as_array().unwrap();
|
||||
|
||||
for name in ["search", "memory_timeline", "memory", "codebase", "dedup"] {
|
||||
for name in ["search", "memory_status", "memory", "codebase", "dedup"] {
|
||||
let tool = tools
|
||||
.iter()
|
||||
.find(|t| t["name"].as_str() == Some(name))
|
||||
|
|
|
|||
141
crates/vestige-mcp/src/tools/memory_status.rs
Normal file
141
crates/vestige-mcp/src/tools/memory_status.rs
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
//! Unified `memory_status` Tool (v2.2 — Tool Consolidation)
|
||||
//!
|
||||
//! Folds four read-only status/health/temporal tools into one
|
||||
//! view-dispatched surface:
|
||||
//!
|
||||
//! view = health (default) | retention | timeline | changelog
|
||||
//!
|
||||
//! - `health` → full system health + statistics (the former `system_status`).
|
||||
//! Returns the byte-for-byte `system_status` shape (audit scripts parse it),
|
||||
//! including `schema_introspection` passthrough.
|
||||
//! - `retention` → the lightweight retention dashboard (former `memory_health`).
|
||||
//! - `timeline` → chronological browse (former `memory_timeline`).
|
||||
//! - `changelog` → audit trail of memory changes (former `memory_changelog`).
|
||||
//!
|
||||
//! This is a thin facade: each view forwards the *same* args envelope to the
|
||||
//! existing handler. None of the underlying arg structs use
|
||||
//! `deny_unknown_fields`, so the discriminator `view` is simply ignored by each
|
||||
//! handler — no lossy re-scoping required, and per-view fields validate as
|
||||
//! before. The `cognitive` lock is never held across a forwarded call.
|
||||
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use vestige_core::{OutputConfig, Storage};
|
||||
|
||||
use crate::cognitive::CognitiveEngine;
|
||||
|
||||
/// Discriminated-union schema for the unified `memory_status` tool.
|
||||
pub fn schema() -> Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"view": {
|
||||
"type": "string",
|
||||
"enum": ["health", "retention", "timeline", "changelog"],
|
||||
"default": "health",
|
||||
"description": "Which status view. 'health' (default): full system health + stats + FSRS preview + warnings + recommendations. 'retention': lightweight retention dashboard (avg/distribution/trend). 'timeline': browse memories chronologically. 'changelog': audit trail of memory state changes."
|
||||
},
|
||||
// --- [health view] ---
|
||||
"schema_introspection": {
|
||||
"type": "boolean",
|
||||
"description": "[health view] Include the response-schema description in the output."
|
||||
},
|
||||
// --- [timeline view] ---
|
||||
"start": { "type": "string", "description": "[timeline/changelog view] Start of range (ISO 8601 date or datetime)." },
|
||||
"end": { "type": "string", "description": "[timeline/changelog view] End of range (ISO 8601 date or datetime)." },
|
||||
"node_type": { "type": "string", "description": "[timeline view] Filter by node type (e.g. 'fact', 'decision')." },
|
||||
"tags": { "type": "array", "items": { "type": "string" }, "description": "[timeline view] Filter by tags (ANY match)." },
|
||||
"detail_level": {
|
||||
"type": "string", "enum": ["brief", "summary", "full"],
|
||||
"description": "[timeline view] Level of detail (default 'summary')."
|
||||
},
|
||||
// --- [changelog view] ---
|
||||
"memory_id": { "type": "string", "description": "[changelog view] Per-memory mode: state transitions for this memory id." },
|
||||
// --- shared: limit (per-view ranges differ; clamped internally) ---
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Max results. [timeline] default 50, max 200. [changelog] default 20, clamped to 100. Ignored by health/retention.",
|
||||
"minimum": 1, "maximum": 200
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Unified dispatcher for `memory_status`. Routes on `view` (default `health`).
|
||||
pub async fn execute(
|
||||
storage: &Arc<Storage>,
|
||||
cognitive: &Arc<Mutex<CognitiveEngine>>,
|
||||
output_config: &OutputConfig,
|
||||
args: Option<Value>,
|
||||
) -> Result<Value, String> {
|
||||
let view = args
|
||||
.as_ref()
|
||||
.and_then(|a| a.get("view"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("health")
|
||||
.to_string();
|
||||
|
||||
match view.as_str() {
|
||||
// Byte-for-byte system_status shape (incl. schema_introspection passthrough).
|
||||
"health" => super::maintenance::execute_system_status(storage, cognitive, args).await,
|
||||
"retention" => super::health::execute(storage, args).await,
|
||||
"timeline" => super::timeline::execute(storage, output_config, args).await,
|
||||
"changelog" => super::changelog::execute(storage, args).await,
|
||||
other => Err(format!(
|
||||
"Unknown memory_status view '{other}'. Use health|retention|timeline|changelog."
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::cognitive::CognitiveEngine;
|
||||
|
||||
fn test_storage() -> Arc<Storage> {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let storage = Storage::new(Some(dir.path().join("test.db"))).unwrap();
|
||||
// Keep the tempdir alive for the duration of the process by leaking it;
|
||||
// these are short-lived unit tests.
|
||||
std::mem::forget(dir);
|
||||
Arc::new(storage)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schema_views() {
|
||||
let s = schema();
|
||||
let views = s["properties"]["view"]["enum"].as_array().unwrap();
|
||||
assert_eq!(views.len(), 4);
|
||||
assert_eq!(s["properties"]["view"]["default"], "health");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_default_view_is_health() {
|
||||
let storage = test_storage();
|
||||
let cognitive = Arc::new(Mutex::new(CognitiveEngine::new()));
|
||||
let oc = OutputConfig::default();
|
||||
// No args → health view → must match system_status output exactly.
|
||||
let unified = execute(&storage, &cognitive, &oc, None).await.unwrap();
|
||||
let direct = super::super::maintenance::execute_system_status(&storage, &cognitive, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
unified, direct,
|
||||
"memory_status view=health must equal system_status byte-for-byte"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_all_views_resolve() {
|
||||
let storage = test_storage();
|
||||
let cognitive = Arc::new(Mutex::new(CognitiveEngine::new()));
|
||||
let oc = OutputConfig::default();
|
||||
for view in ["health", "retention", "timeline", "changelog"] {
|
||||
let args = Some(serde_json::json!({ "view": view }));
|
||||
let r = execute(&storage, &cognitive, &oc, args).await;
|
||||
assert!(r.is_ok(), "view={view} should resolve, got {r:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,10 @@ pub mod timeline;
|
|||
// v1.2: Maintenance tools
|
||||
pub mod maintenance;
|
||||
|
||||
// v2.2: Unified status surface — folds system_status + memory_health +
|
||||
// memory_timeline + memory_changelog into one view-dispatched tool.
|
||||
pub mod memory_status;
|
||||
|
||||
// v1.3: Auto-save and dedup tools
|
||||
pub mod dedup;
|
||||
pub mod importance;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue