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:
Sam Valladares 2026-06-28 17:37:57 -05:00
parent 8888634740
commit 32e6a6cd8d
3 changed files with 240 additions and 38 deletions

View file

@ -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))

View 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:?}");
}
}
}

View file

@ -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;