mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-22 23:31:02 +02:00
Release v2.1.0
Some checks are pending
CI / Test (macos-latest) (push) Waiting to run
CI / Test (ubuntu-latest) (push) Waiting to run
CI / Release Build (aarch64-apple-darwin) (push) Blocked by required conditions
CI / Release Build (x86_64-unknown-linux-gnu) (push) Blocked by required conditions
CI / Release Build (x86_64-apple-darwin) (push) Blocked by required conditions
Test Suite / Unit Tests (push) Waiting to run
Test Suite / MCP E2E Tests (push) Waiting to run
Test Suite / User Journey Tests (push) Blocked by required conditions
Test Suite / Dashboard Build (push) Waiting to run
Test Suite / Code Coverage (push) Waiting to run
Some checks are pending
CI / Test (macos-latest) (push) Waiting to run
CI / Test (ubuntu-latest) (push) Waiting to run
CI / Release Build (aarch64-apple-darwin) (push) Blocked by required conditions
CI / Release Build (x86_64-unknown-linux-gnu) (push) Blocked by required conditions
CI / Release Build (x86_64-apple-darwin) (push) Blocked by required conditions
Test Suite / Unit Tests (push) Waiting to run
Test Suite / MCP E2E Tests (push) Waiting to run
Test Suite / User Journey Tests (push) Blocked by required conditions
Test Suite / Dashboard Build (push) Waiting to run
Test Suite / Code Coverage (push) Waiting to run
This commit is contained in:
parent
694e837898
commit
d4313df759
106 changed files with 2900 additions and 128 deletions
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "vestige-core"
|
||||
version = "2.0.9"
|
||||
version = "2.1.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.91"
|
||||
authors = ["Vestige Team"]
|
||||
|
|
|
|||
|
|
@ -738,7 +738,10 @@ mod tests {
|
|||
|
||||
// 1. schema_version advanced to V11
|
||||
let version = get_current_version(&conn).expect("read schema_version");
|
||||
assert_eq!(version, 11, "schema_version must be 11 after all migrations");
|
||||
assert_eq!(
|
||||
version, 11,
|
||||
"schema_version must be 11 after all migrations"
|
||||
);
|
||||
|
||||
// 2. knowledge_edges is gone (V11 drops it)
|
||||
let knowledge_edges_rows: i64 = conn
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "vestige-mcp"
|
||||
version = "2.0.9"
|
||||
version = "2.1.0"
|
||||
edition = "2024"
|
||||
description = "Cognitive memory MCP server for Claude - FSRS-6, spreading activation, synaptic tagging, 3D dashboard, and 130 years of memory research"
|
||||
authors = ["samvallad33"]
|
||||
|
|
@ -44,7 +44,7 @@ path = "src/bin/cli.rs"
|
|||
# Only `bundled-sqlite` is always on. `embeddings` and `vector-search` are
|
||||
# toggled via vestige-mcp's own feature flags below so `--no-default-features`
|
||||
# actually works (previously hardcoded here, which silently defeated the flag).
|
||||
vestige-core = { version = "2.0.8", path = "../vestige-core", default-features = false, features = ["bundled-sqlite"] }
|
||||
vestige-core = { version = "2.1.0", path = "../vestige-core", default-features = false, features = ["bundled-sqlite"] }
|
||||
|
||||
# ============================================================================
|
||||
# MCP Server Dependencies
|
||||
|
|
|
|||
|
|
@ -153,10 +153,8 @@ pub fn spawn(
|
|||
backoff_secs = SUPERVISOR_RESTART_BACKOFF_SECS,
|
||||
"Autopilot event subscriber panicked — supervisor restarting"
|
||||
);
|
||||
tokio::time::sleep(Duration::from_secs(
|
||||
SUPERVISOR_RESTART_BACKOFF_SECS,
|
||||
))
|
||||
.await;
|
||||
tokio::time::sleep(Duration::from_secs(SUPERVISOR_RESTART_BACKOFF_SECS))
|
||||
.await;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(error = ?e, "Autopilot event subscriber join error — exiting");
|
||||
|
|
@ -187,10 +185,8 @@ pub fn spawn(
|
|||
backoff_secs = SUPERVISOR_RESTART_BACKOFF_SECS,
|
||||
"Autopilot prospective poller panicked — supervisor restarting"
|
||||
);
|
||||
tokio::time::sleep(Duration::from_secs(
|
||||
SUPERVISOR_RESTART_BACKOFF_SECS,
|
||||
))
|
||||
.await;
|
||||
tokio::time::sleep(Duration::from_secs(SUPERVISOR_RESTART_BACKOFF_SECS))
|
||||
.await;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(error = ?e, "Autopilot prospective poller join error — exiting");
|
||||
|
|
@ -218,14 +214,7 @@ async fn run_event_subscriber(
|
|||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(event) => {
|
||||
handle_event(
|
||||
event,
|
||||
&cognitive,
|
||||
&storage,
|
||||
&event_tx,
|
||||
&mut dedup_state,
|
||||
)
|
||||
.await;
|
||||
handle_event(event, &cognitive, &storage, &event_tx, &mut dedup_state).await;
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||
warn!("Autopilot lagged {n} events — increase channel capacity if this persists");
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@
|
|||
//!
|
||||
//! v2.0: Adds cognitive operation endpoints (dream, explore, predict, importance, consolidation)
|
||||
|
||||
use std::cmp::Reverse;
|
||||
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{Json, Redirect};
|
||||
use chrono::{Duration, Utc};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
|
|
@ -373,6 +375,13 @@ pub struct TimelineParams {
|
|||
pub limit: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ChangelogParams {
|
||||
pub start: Option<String>,
|
||||
pub end: Option<String>,
|
||||
pub limit: Option<i32>,
|
||||
}
|
||||
|
||||
/// Get timeline data
|
||||
pub async fn get_timeline(
|
||||
State(state): State<AppState>,
|
||||
|
|
@ -428,6 +437,108 @@ pub async fn get_timeline(
|
|||
})))
|
||||
}
|
||||
|
||||
/// Recent cognitive events in the same envelope used by the WebSocket event
|
||||
/// stream. The pulse hook polls this endpoint once per Claude wake, so keep it
|
||||
/// cheap, bounded, and tolerant of empty history.
|
||||
pub async fn get_changelog(
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<ChangelogParams>,
|
||||
) -> Result<Json<Value>, StatusCode> {
|
||||
let limit = params.limit.unwrap_or(50).clamp(1, 100);
|
||||
let start = parse_changelog_bound(params.start.as_deref())?;
|
||||
let end = parse_changelog_bound(params.end.as_deref())?;
|
||||
let fetch_limit = if start.is_some() || end.is_some() {
|
||||
limit.saturating_mul(4)
|
||||
} else {
|
||||
limit
|
||||
};
|
||||
|
||||
let mut events: Vec<(DateTime<Utc>, Value)> = Vec::new();
|
||||
|
||||
let dreams = state
|
||||
.storage
|
||||
.get_dream_history(fetch_limit)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
for dream in dreams {
|
||||
if changelog_window_contains(dream.dreamed_at, start.as_ref(), end.as_ref()) {
|
||||
events.push((dream.dreamed_at, dream_changelog_event(&dream)));
|
||||
}
|
||||
}
|
||||
|
||||
// Connections are currently persisted as graph edges rather than as audit
|
||||
// rows, so filter by created_at from the connection table.
|
||||
let connections = state
|
||||
.storage
|
||||
.get_all_connections()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
for conn in connections {
|
||||
if changelog_window_contains(conn.created_at, start.as_ref(), end.as_ref()) {
|
||||
events.push((conn.created_at, connection_changelog_event(&conn)));
|
||||
}
|
||||
}
|
||||
|
||||
events.sort_by_key(|event| Reverse(event.0));
|
||||
events.truncate(limit as usize);
|
||||
let formatted_events: Vec<Value> = events.into_iter().map(|(_, event)| event).collect();
|
||||
let total_events = formatted_events.len();
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"events": formatted_events,
|
||||
"totalEvents": total_events,
|
||||
"filter": {
|
||||
"start": start.as_ref().map(DateTime::to_rfc3339),
|
||||
"end": end.as_ref().map(DateTime::to_rfc3339),
|
||||
"limit": limit,
|
||||
},
|
||||
})))
|
||||
}
|
||||
|
||||
fn parse_changelog_bound(raw: Option<&str>) -> Result<Option<DateTime<Utc>>, StatusCode> {
|
||||
match raw {
|
||||
Some(value) if !value.trim().is_empty() => DateTime::parse_from_rfc3339(value)
|
||||
.map(|dt| Some(dt.with_timezone(&Utc)))
|
||||
.map_err(|_| StatusCode::BAD_REQUEST),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn changelog_window_contains(
|
||||
ts: DateTime<Utc>,
|
||||
start: Option<&DateTime<Utc>>,
|
||||
end: Option<&DateTime<Utc>>,
|
||||
) -> bool {
|
||||
start.is_none_or(|s| ts >= *s) && end.is_none_or(|e| ts <= *e)
|
||||
}
|
||||
|
||||
fn dream_changelog_event(dream: &vestige_core::DreamHistoryRecord) -> Value {
|
||||
serde_json::json!({
|
||||
"type": "DreamCompleted",
|
||||
"timestamp": dream.dreamed_at.to_rfc3339(),
|
||||
"data": {
|
||||
"memories_replayed": dream.memories_replayed,
|
||||
"connections_found": dream.connections_found,
|
||||
"connections_persisted": dream.connections_found,
|
||||
"insights_generated": dream.insights_generated,
|
||||
"duration_ms": dream.duration_ms,
|
||||
"timestamp": dream.dreamed_at.to_rfc3339(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn connection_changelog_event(conn: &vestige_core::ConnectionRecord) -> Value {
|
||||
serde_json::json!({
|
||||
"type": "ConnectionDiscovered",
|
||||
"timestamp": conn.created_at.to_rfc3339(),
|
||||
"data": {
|
||||
"source_id": &conn.source_id,
|
||||
"target_id": &conn.target_id,
|
||||
"connection_type": &conn.link_type,
|
||||
"weight": conn.strength,
|
||||
"timestamp": conn.created_at.to_rfc3339(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Health check
|
||||
pub async fn health_check(State(state): State<AppState>) -> Result<Json<Value>, StatusCode> {
|
||||
let stats = state
|
||||
|
|
@ -537,7 +648,9 @@ pub async fn get_graph(
|
|||
&& edges.is_empty()
|
||||
&& let Ok(fallback) = default_center_id(&state.storage, GraphSort::Connected)
|
||||
&& fallback != center_id
|
||||
&& let Ok((n2, e2)) = state.storage.get_memory_subgraph(&fallback, depth, max_nodes)
|
||||
&& let Ok((n2, e2)) = state
|
||||
.storage
|
||||
.get_memory_subgraph(&fallback, depth, max_nodes)
|
||||
&& n2.len() > nodes.len()
|
||||
{
|
||||
center_id = fallback;
|
||||
|
|
@ -793,14 +906,38 @@ pub async fn trigger_dream(State(state): State<AppState>) -> Result<Json<Value>,
|
|||
}
|
||||
|
||||
let duration_ms = start.elapsed().as_millis() as u64;
|
||||
let completed_at = Utc::now();
|
||||
let insights_generated = insights.len();
|
||||
|
||||
if let Err(e) = state
|
||||
.storage
|
||||
.save_dream_history(&vestige_core::DreamHistoryRecord {
|
||||
dreamed_at: completed_at,
|
||||
duration_ms: duration_ms as i64,
|
||||
memories_replayed: dream_memories.len() as i32,
|
||||
connections_found: connections_persisted as i32,
|
||||
insights_generated: insights_generated as i32,
|
||||
memories_strengthened: dream_result.memories_strengthened as i32,
|
||||
memories_compressed: dream_result.memories_compressed as i32,
|
||||
phase_nrem1_ms: None,
|
||||
phase_nrem3_ms: None,
|
||||
phase_rem_ms: None,
|
||||
phase_integration_ms: None,
|
||||
summaries_generated: None,
|
||||
emotional_memories_processed: None,
|
||||
creative_connections_found: None,
|
||||
})
|
||||
{
|
||||
tracing::warn!("Failed to persist dashboard dream history: {}", e);
|
||||
}
|
||||
|
||||
// Emit completion event
|
||||
state.emit(VestigeEvent::DreamCompleted {
|
||||
memories_replayed: dream_memories.len(),
|
||||
connections_found: connections_persisted as usize,
|
||||
insights_generated: insights.len(),
|
||||
insights_generated,
|
||||
duration_ms,
|
||||
timestamp: Utc::now(),
|
||||
timestamp: completed_at,
|
||||
});
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
|
|
@ -1303,7 +1440,7 @@ mod tests {
|
|||
use std::sync::Arc;
|
||||
use tempfile::tempdir;
|
||||
use vestige_core::memory::IngestInput;
|
||||
use vestige_core::{ConnectionRecord, Storage};
|
||||
use vestige_core::{ConnectionRecord, DreamHistoryRecord, Storage};
|
||||
|
||||
#[test]
|
||||
fn graph_sort_parse_defaults_to_recent() {
|
||||
|
|
@ -1322,6 +1459,51 @@ mod tests {
|
|||
assert_eq!(GraphSort::parse(Some("Connected")), GraphSort::Connected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changelog_dream_event_uses_pulse_compatible_shape() {
|
||||
let now = Utc::now();
|
||||
let event = dream_changelog_event(&DreamHistoryRecord {
|
||||
dreamed_at: now,
|
||||
duration_ms: 1234,
|
||||
memories_replayed: 12,
|
||||
connections_found: 3,
|
||||
insights_generated: 2,
|
||||
memories_strengthened: 0,
|
||||
memories_compressed: 0,
|
||||
phase_nrem1_ms: None,
|
||||
phase_nrem3_ms: None,
|
||||
phase_rem_ms: None,
|
||||
phase_integration_ms: None,
|
||||
summaries_generated: None,
|
||||
emotional_memories_processed: None,
|
||||
creative_connections_found: None,
|
||||
});
|
||||
|
||||
assert_eq!(event["type"], "DreamCompleted");
|
||||
assert_eq!(event["data"]["insights_generated"], 2);
|
||||
assert_eq!(event["data"]["connections_persisted"], 3);
|
||||
assert_eq!(event["data"]["timestamp"], now.to_rfc3339());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changelog_connection_event_uses_pulse_compatible_shape() {
|
||||
let now = Utc::now();
|
||||
let event = connection_changelog_event(&ConnectionRecord {
|
||||
source_id: "source-memory".to_string(),
|
||||
target_id: "target-memory".to_string(),
|
||||
strength: 0.82,
|
||||
link_type: "semantic".to_string(),
|
||||
created_at: now,
|
||||
last_activated: now,
|
||||
activation_count: 1,
|
||||
});
|
||||
|
||||
assert_eq!(event["type"], "ConnectionDiscovered");
|
||||
assert_eq!(event["data"]["source_id"], "source-memory");
|
||||
assert_eq!(event["data"]["target_id"], "target-memory");
|
||||
assert_eq!(event["data"]["connection_type"], "semantic");
|
||||
}
|
||||
|
||||
fn seed_storage() -> (tempfile::TempDir, Arc<Storage>) {
|
||||
let dir = tempdir().unwrap();
|
||||
let db_path = dir.path().join("test.db");
|
||||
|
|
|
|||
|
|
@ -142,7 +142,10 @@ fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) {
|
|||
// since v2.0.5 despite having full graph event handlers; this closes
|
||||
// the gap so dashboard users can trigger inhibition without dropping
|
||||
// to the MCP layer.
|
||||
.route("/api/memories/{id}/suppress", post(handlers::suppress_memory))
|
||||
.route(
|
||||
"/api/memories/{id}/suppress",
|
||||
post(handlers::suppress_memory),
|
||||
)
|
||||
.route(
|
||||
"/api/memories/{id}/unsuppress",
|
||||
post(handlers::unsuppress_memory),
|
||||
|
|
@ -154,6 +157,7 @@ fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) {
|
|||
.route("/api/health", get(handlers::health_check))
|
||||
// Timeline
|
||||
.route("/api/timeline", get(handlers::get_timeline))
|
||||
.route("/api/changelog", get(handlers::get_changelog))
|
||||
// Graph
|
||||
.route("/api/graph", get(handlers::get_graph))
|
||||
// Cognitive operations (v2.0)
|
||||
|
|
@ -171,10 +175,7 @@ fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) {
|
|||
// Reasoning Theater (v2.0.8) — 8-stage cognitive pipeline surface.
|
||||
// Wraps crate::tools::cross_reference::execute. Emits
|
||||
// DeepReferenceCompleted so Graph3D can glide, pulse, and arc.
|
||||
.route(
|
||||
"/api/deep_reference",
|
||||
post(handlers::deep_reference_query),
|
||||
)
|
||||
.route("/api/deep_reference", post(handlers::deep_reference_query))
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
.concurrency_limit(50)
|
||||
|
|
|
|||
|
|
@ -31,8 +31,11 @@ use vestige_mcp::cognitive;
|
|||
use vestige_mcp::protocol;
|
||||
use vestige_mcp::server;
|
||||
|
||||
use directories::BaseDirs;
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Component, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{Level, error, info, warn};
|
||||
|
|
@ -44,6 +47,9 @@ use vestige_core::Storage;
|
|||
use protocol::stdio::StdioTransport;
|
||||
use server::McpServer;
|
||||
|
||||
const DATA_DIR_ENV: &str = "VESTIGE_DATA_DIR";
|
||||
const DATABASE_FILE: &str = "vestige.db";
|
||||
|
||||
/// Parsed CLI configuration.
|
||||
struct Config {
|
||||
data_dir: Option<PathBuf>,
|
||||
|
|
@ -51,11 +57,24 @@ struct Config {
|
|||
dashboard_enabled: bool,
|
||||
}
|
||||
|
||||
fn data_dir_from_env() -> Option<PathBuf> {
|
||||
std::env::var_os(DATA_DIR_ENV).and_then(|value| {
|
||||
if value.as_os_str().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(PathBuf::from(value))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse command-line arguments into a `Config`.
|
||||
/// Exits the process if `--help` or `--version` is requested.
|
||||
fn parse_args() -> Config {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let mut data_dir: Option<PathBuf> = None;
|
||||
parse_args_from(std::env::args_os().collect(), data_dir_from_env())
|
||||
}
|
||||
|
||||
fn parse_args_from(args: Vec<OsString>, env_data_dir: Option<PathBuf>) -> Config {
|
||||
let mut data_dir = env_data_dir;
|
||||
let mut http_port: u16 = std::env::var("VESTIGE_HTTP_PORT")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
|
|
@ -66,7 +85,8 @@ fn parse_args() -> Config {
|
|||
let mut i = 1;
|
||||
|
||||
while i < args.len() {
|
||||
match args[i].as_str() {
|
||||
let arg = args[i].to_string_lossy();
|
||||
match arg.as_ref() {
|
||||
"--help" | "-h" => {
|
||||
println!("Vestige MCP Server v{}", env!("CARGO_PKG_VERSION"));
|
||||
println!();
|
||||
|
|
@ -78,10 +98,15 @@ fn parse_args() -> Config {
|
|||
println!("OPTIONS:");
|
||||
println!(" -h, --help Print help information");
|
||||
println!(" -V, --version Print version information");
|
||||
println!(" --data-dir <PATH> Custom data directory");
|
||||
println!(
|
||||
" --data-dir <PATH> Custom data directory (overrides VESTIGE_DATA_DIR)"
|
||||
);
|
||||
println!(" --http-port <PORT> HTTP transport port (default: 3928)");
|
||||
println!();
|
||||
println!("ENVIRONMENT:");
|
||||
println!(
|
||||
" VESTIGE_DATA_DIR Data directory fallback (stores vestige.db inside)"
|
||||
);
|
||||
println!(
|
||||
" RUST_LOG Log level filter (e.g., debug, info, warn, error)"
|
||||
);
|
||||
|
|
@ -98,6 +123,7 @@ fn parse_args() -> Config {
|
|||
println!("EXAMPLES:");
|
||||
println!(" vestige-mcp");
|
||||
println!(" vestige-mcp --data-dir /custom/path");
|
||||
println!(" VESTIGE_DATA_DIR=~/.vestige vestige-mcp");
|
||||
println!(" vestige-mcp --http-port 8080");
|
||||
println!(" RUST_LOG=debug vestige-mcp");
|
||||
std::process::exit(0);
|
||||
|
|
@ -113,6 +139,11 @@ fn parse_args() -> Config {
|
|||
eprintln!("Usage: vestige-mcp --data-dir <PATH>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
if args[i].as_os_str().is_empty() {
|
||||
eprintln!("error: --data-dir requires a non-empty path argument");
|
||||
eprintln!("Usage: vestige-mcp --data-dir <PATH>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
data_dir = Some(PathBuf::from(&args[i]));
|
||||
}
|
||||
arg if arg.starts_with("--data-dir=") => {
|
||||
|
|
@ -132,10 +163,11 @@ fn parse_args() -> Config {
|
|||
eprintln!("Usage: vestige-mcp --http-port <PORT>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
http_port = match args[i].parse() {
|
||||
let port = args[i].to_string_lossy();
|
||||
http_port = match port.parse() {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("error: invalid port number '{}'", args[i]);
|
||||
eprintln!("error: invalid port number '{}'", port);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
|
@ -167,6 +199,42 @@ fn parse_args() -> Config {
|
|||
}
|
||||
}
|
||||
|
||||
fn expand_tilde(path: PathBuf) -> PathBuf {
|
||||
let rest = {
|
||||
let mut components = path.components();
|
||||
match components.next() {
|
||||
Some(Component::Normal(first)) if first == "~" => {
|
||||
Some(components.as_path().to_path_buf())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
match rest {
|
||||
Some(rest) => BaseDirs::new()
|
||||
.map(|dirs| dirs.home_dir().join(rest))
|
||||
.unwrap_or(path),
|
||||
None => path,
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_storage_path(data_dir: Option<PathBuf>) -> io::Result<Option<PathBuf>> {
|
||||
let Some(data_dir) = data_dir else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let data_dir = expand_tilde(data_dir);
|
||||
fs::create_dir_all(&data_dir)?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let _ = fs::set_permissions(&data_dir, fs::Permissions::from_mode(0o700));
|
||||
}
|
||||
|
||||
Ok(Some(data_dir.join(DATABASE_FILE)))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Parse CLI arguments first (before logging init, so --help/--version work cleanly)
|
||||
|
|
@ -185,8 +253,17 @@ async fn main() {
|
|||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
|
||||
// Initialize storage with optional custom data directory
|
||||
let storage = match Storage::new(config.data_dir) {
|
||||
let storage_path = match prepare_storage_path(config.data_dir) {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
error!("Failed to prepare storage data directory: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize storage with optional custom data directory.
|
||||
// Storage::new(Some(...)) expects a DB file path, so map data dirs to vestige.db here.
|
||||
let storage = match Storage::new(storage_path) {
|
||||
Ok(s) => {
|
||||
info!("Storage initialized successfully");
|
||||
|
||||
|
|
@ -417,3 +494,57 @@ async fn main() {
|
|||
|
||||
info!("Vestige MCP Server shutting down");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn os_args(args: &[&str]) -> Vec<OsString> {
|
||||
args.iter().map(OsString::from).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vestige_data_dir_env_is_used_when_cli_data_dir_is_absent() {
|
||||
let config = parse_args_from(
|
||||
os_args(&["vestige-mcp"]),
|
||||
Some(PathBuf::from("/tmp/vestige-env")),
|
||||
);
|
||||
|
||||
assert_eq!(config.data_dir, Some(PathBuf::from("/tmp/vestige-env")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_data_dir_takes_precedence_over_env_data_dir() {
|
||||
let config = parse_args_from(
|
||||
os_args(&["vestige-mcp", "--data-dir", "/tmp/vestige-cli"]),
|
||||
Some(PathBuf::from("/tmp/vestige-env")),
|
||||
);
|
||||
|
||||
assert_eq!(config.data_dir, Some(PathBuf::from("/tmp/vestige-cli")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_storage_path_creates_dir_and_points_to_vestige_db() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let data_dir = temp.path().join("nested").join("vestige");
|
||||
|
||||
let db_path = prepare_storage_path(Some(data_dir.clone())).unwrap();
|
||||
|
||||
assert!(data_dir.is_dir());
|
||||
assert_eq!(db_path, Some(data_dir.join(DATABASE_FILE)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_tilde_expands_current_users_home_only() {
|
||||
let home = BaseDirs::new().unwrap().home_dir().to_path_buf();
|
||||
|
||||
assert_eq!(
|
||||
expand_tilde(PathBuf::from("~/vestige")),
|
||||
home.join("vestige")
|
||||
);
|
||||
assert_eq!(
|
||||
expand_tilde(PathBuf::from("~other/vestige")),
|
||||
PathBuf::from("~other/vestige")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -694,11 +694,9 @@ pub async fn execute(
|
|||
// embedding space — even though the winning memory contains neither
|
||||
// "FSRS-6" nor anything about spaced repetition).
|
||||
const TOPIC_STOPWORDS: &[&str] = &[
|
||||
"how", "what", "when", "where", "why", "who", "which",
|
||||
"does", "did", "is", "are", "was", "were", "will",
|
||||
"the", "and", "for", "with", "this", "that",
|
||||
"work", "works", "use", "uses", "used", "using",
|
||||
"about", "from", "into", "than", "then",
|
||||
"how", "what", "when", "where", "why", "who", "which", "does", "did", "is", "are", "was",
|
||||
"were", "will", "the", "and", "for", "with", "this", "that", "work", "works", "use",
|
||||
"uses", "used", "using", "about", "from", "into", "than", "then",
|
||||
];
|
||||
let topic_terms: Vec<String> = args
|
||||
.query
|
||||
|
|
@ -762,15 +760,12 @@ pub async fn execute(
|
|||
&non_superseded_all
|
||||
};
|
||||
|
||||
let recommended = primary_pool
|
||||
.iter()
|
||||
.copied()
|
||||
.max_by(|a, b| {
|
||||
composite(a)
|
||||
.partial_cmp(&composite(b))
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
.then_with(|| a.updated_at.cmp(&b.updated_at))
|
||||
});
|
||||
let recommended = primary_pool.iter().copied().max_by(|a, b| {
|
||||
composite(a)
|
||||
.partial_cmp(&composite(b))
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
.then_with(|| a.updated_at.cmp(&b.updated_at))
|
||||
});
|
||||
|
||||
// ====================================================================
|
||||
// STAGE 7: Relation Assessment (per-pair, using trust + temporal + similarity)
|
||||
|
|
|
|||
|
|
@ -114,15 +114,18 @@ pub async fn execute(
|
|||
degraded = true;
|
||||
Vec::new()
|
||||
});
|
||||
let accuracy = cog.predictive_memory.prediction_accuracy().unwrap_or_else(|e| {
|
||||
tracing::warn!(
|
||||
target: "vestige::predict",
|
||||
error = %e,
|
||||
"prediction_accuracy failed; returning 0.0"
|
||||
);
|
||||
degraded = true;
|
||||
0.0
|
||||
});
|
||||
let accuracy = cog
|
||||
.predictive_memory
|
||||
.prediction_accuracy()
|
||||
.unwrap_or_else(|e| {
|
||||
tracing::warn!(
|
||||
target: "vestige::predict",
|
||||
error = %e,
|
||||
"prediction_accuracy failed; returning 0.0"
|
||||
);
|
||||
degraded = true;
|
||||
0.0
|
||||
});
|
||||
|
||||
// Build speculative context
|
||||
let speculative_context = vestige_core::PredictionContext {
|
||||
|
|
|
|||
|
|
@ -67,26 +67,25 @@ pub async fn execute(storage: &Arc<Storage>, args: Option<Value>) -> Result<Valu
|
|||
|
||||
// Try parsing as wrapped format first (MCP response wrapper),
|
||||
// then fall back to direct RecallResult
|
||||
let memories: Vec<MemoryBackup> = if let Ok(wrapper) =
|
||||
serde_json::from_str::<Vec<BackupWrapper>>(&backup_content)
|
||||
{
|
||||
if let Some(first) = wrapper.first() {
|
||||
let recall: RecallResult = serde_json::from_str(&first.text)
|
||||
.map_err(|e| format!("Failed to parse backup contents: {}", e))?;
|
||||
let memories: Vec<MemoryBackup> =
|
||||
if let Ok(wrapper) = serde_json::from_str::<Vec<BackupWrapper>>(&backup_content) {
|
||||
if let Some(first) = wrapper.first() {
|
||||
let recall: RecallResult = serde_json::from_str(&first.text)
|
||||
.map_err(|e| format!("Failed to parse backup contents: {}", e))?;
|
||||
recall.results
|
||||
} else {
|
||||
return Err("Empty backup file".to_string());
|
||||
}
|
||||
} else if let Ok(recall) = serde_json::from_str::<RecallResult>(&backup_content) {
|
||||
recall.results
|
||||
} else if let Ok(nodes) = serde_json::from_str::<Vec<MemoryBackup>>(&backup_content) {
|
||||
nodes
|
||||
} else {
|
||||
return Err("Empty backup file".to_string());
|
||||
}
|
||||
} else if let Ok(recall) = serde_json::from_str::<RecallResult>(&backup_content) {
|
||||
recall.results
|
||||
} else if let Ok(nodes) = serde_json::from_str::<Vec<MemoryBackup>>(&backup_content) {
|
||||
nodes
|
||||
} else {
|
||||
return Err(
|
||||
return Err(
|
||||
"Unrecognized backup format. Expected MCP wrapper, RecallResult, or array of memories."
|
||||
.to_string(),
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
let total = memories.len();
|
||||
if total == 0 {
|
||||
|
|
|
|||
|
|
@ -207,12 +207,7 @@ mod tests {
|
|||
|
||||
/// Ingest with explicit node_type and tags. Used by the sparse-filter
|
||||
/// regression tests so the dominant and sparse sets can be told apart.
|
||||
async fn ingest_typed(
|
||||
storage: &Arc<Storage>,
|
||||
content: &str,
|
||||
node_type: &str,
|
||||
tags: &[&str],
|
||||
) {
|
||||
async fn ingest_typed(storage: &Arc<Storage>, content: &str, node_type: &str, tags: &[&str]) {
|
||||
storage
|
||||
.ingest(vestige_core::IngestInput {
|
||||
content: content.to_string(),
|
||||
|
|
@ -392,11 +387,23 @@ mod tests {
|
|||
|
||||
// Dominant set: 10 facts
|
||||
for i in 0..10 {
|
||||
ingest_typed(&storage, &format!("Dominant memory {}", i), "fact", &["alpha"]).await;
|
||||
ingest_typed(
|
||||
&storage,
|
||||
&format!("Dominant memory {}", i),
|
||||
"fact",
|
||||
&["alpha"],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
// Sparse set: 2 concepts
|
||||
for i in 0..2 {
|
||||
ingest_typed(&storage, &format!("Sparse memory {}", i), "concept", &["beta"]).await;
|
||||
ingest_typed(
|
||||
&storage,
|
||||
&format!("Sparse memory {}", i),
|
||||
"concept",
|
||||
&["beta"],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Limit 5 against 12 total — before the fix, `retain` on `concept`
|
||||
|
|
@ -426,7 +433,13 @@ mod tests {
|
|||
|
||||
// Dominant set: 10 memories with tag "common"
|
||||
for i in 0..10 {
|
||||
ingest_typed(&storage, &format!("Common memory {}", i), "fact", &["common"]).await;
|
||||
ingest_typed(
|
||||
&storage,
|
||||
&format!("Common memory {}", i),
|
||||
"fact",
|
||||
&["common"],
|
||||
)
|
||||
.await;
|
||||
}
|
||||
// Sparse set: 2 memories with tag "rare"
|
||||
for i in 0..2 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue