mirror of
https://github.com/samvallad33/vestige.git
synced 2026-05-08 23:32:37 +02:00
feat(v2.0.5): Intentional Amnesia — active forgetting via top-down inhibitory control
First AI memory system to model forgetting as a neuroscience-grounded PROCESS rather than passive decay. Adds the `suppress` MCP tool (#24), Rac1 cascade worker, migration V10, and dashboard forgetting indicators. Based on: - Anderson, Hanslmayr & Quaegebeur (2025), Nat Rev Neurosci — right lateral PFC as the domain-general inhibitory controller; SIF compounds with each stopping attempt. - Cervantes-Sandoval et al. (2020), Front Cell Neurosci PMC7477079 — Rac1 GTPase as the active synaptic destabilization mechanism. What's new: * `suppress` MCP tool — each call compounds `suppression_count` and subtracts a `0.15 × count` penalty (saturating at 80%) from retrieval scores during hybrid search. Distinct from delete (removes) and demote (one-shot). * Rac1 cascade worker — background sweep piggybacks the 6h consolidation loop, walks `memory_connections` edges from recently-suppressed seeds, applies attenuated FSRS decay to co-activated neighbors. You don't just forget Jake — you fade the café, the roommate, the birthday. * 24h labile window — reversible via `suppress({id, reverse: true})` within 24 hours. Matches Nader reconsolidation semantics. * Migration V10 — additive-only (`suppression_count`, `suppressed_at` + partial indices). All v2.0.x DBs upgrade seamlessly on first launch. * Dashboard: `ForgettingIndicator.svelte` pulses when suppressions are active. 3D graph nodes dim to 20% opacity when suppressed. New WebSocket events: `MemorySuppressed`, `MemoryUnsuppressed`, `Rac1CascadeSwept`. Heartbeat carries `suppressed_count`. * Search pipeline: SIF penalty inserted into the accessibility stage so it stacks on top of passive FSRS decay. * Tool count bumped 23 → 24. Cognitive modules 29 → 30. Memories persist — they are INHIBITED, not erased. `memory.get(id)` returns full content through any number of suppressions. The 24h labile window is a grace period for regret. Also fixes issue #31 (dashboard graph view buggy) as a companion UI bug discovered during the v2.0.5 audit cycle: * Root cause: node glow `SpriteMaterial` had no `map`, so `THREE.Sprite` rendered as a solid-coloured 1×1 plane. Additive blending + `UnrealBloomPass(0.8, 0.4, 0.85)` amplified the square edges into hard-edged glowing cubes. * Fix: shared 128×128 radial-gradient `CanvasTexture` singleton used as the sprite map. Retuned bloom to `(0.55, 0.6, 0.2)`. Halved fog density (0.008 → 0.0035). Edges bumped from dark navy `0x4a4a7a` to brand violet `0x8b5cf6` with higher opacity. Added explicit `scene.background` and a 2000-point starfield for depth. * 21 regression tests added in `ui-fixes.test.ts` locking every invariant in (shared texture singleton, depthWrite:false, scale ×6, bloom magic numbers via source regex, starfield presence). Tests: 1,284 Rust (+47) + 171 Vitest (+21) = 1,455 total, 0 failed Clippy: clean across all targets, zero warnings Release binary: 22.6MB, `cargo build --release -p vestige-mcp` green Versions: workspace aligned at 2.0.5 across all 6 crates/packages Closes #31
This commit is contained in:
parent
95bde93b49
commit
8178beb961
359 changed files with 8277 additions and 3416 deletions
|
|
@ -17,17 +17,17 @@ use axum::response::IntoResponse;
|
|||
use axum::routing::{delete, post};
|
||||
use axum::{Json, Router};
|
||||
use subtle::ConstantTimeEq;
|
||||
use tokio::sync::{broadcast, Mutex, RwLock};
|
||||
use tokio::sync::{Mutex, RwLock, broadcast};
|
||||
use tower::ServiceBuilder;
|
||||
use tower::limit::ConcurrencyLimitLayer;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::cognitive::CognitiveEngine;
|
||||
use crate::dashboard::events::VestigeEvent;
|
||||
use crate::protocol::types::JsonRpcRequest;
|
||||
use crate::server::McpServer;
|
||||
use vestige_core::Storage;
|
||||
use crate::dashboard::events::VestigeEvent;
|
||||
|
||||
/// Maximum concurrent sessions.
|
||||
const MAX_SESSIONS: usize = 100;
|
||||
|
|
@ -95,7 +95,11 @@ pub async fn start_http_transport(
|
|||
});
|
||||
let removed = before - map.len();
|
||||
if removed > 0 {
|
||||
info!("Session reaper: removed {} idle sessions ({} active)", removed, map.len());
|
||||
info!(
|
||||
"Session reaper: removed {} idle sessions ({} active)",
|
||||
removed,
|
||||
map.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -119,8 +123,15 @@ pub async fn start_http_transport(
|
|||
.filter_map(|s| s.parse().ok())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.allow_methods([axum::http::Method::POST, axum::http::Method::DELETE, axum::http::Method::OPTIONS])
|
||||
.allow_headers([axum::http::header::CONTENT_TYPE, axum::http::header::AUTHORIZATION])
|
||||
.allow_methods([
|
||||
axum::http::Method::POST,
|
||||
axum::http::Method::DELETE,
|
||||
axum::http::Method::OPTIONS,
|
||||
])
|
||||
.allow_headers([
|
||||
axum::http::header::CONTENT_TYPE,
|
||||
axum::http::header::AUTHORIZATION,
|
||||
]),
|
||||
),
|
||||
)
|
||||
.with_state(state);
|
||||
|
|
@ -156,9 +167,10 @@ fn validate_auth(headers: &HeaderMap, expected: &str) -> Result<(), (StatusCode,
|
|||
.and_then(|v| v.to_str().ok())
|
||||
.ok_or((StatusCode::UNAUTHORIZED, "Missing Authorization header"))?;
|
||||
|
||||
let token = header
|
||||
.strip_prefix("Bearer ")
|
||||
.ok_or((StatusCode::UNAUTHORIZED, "Invalid Authorization scheme (expected Bearer)"))?;
|
||||
let token = header.strip_prefix("Bearer ").ok_or((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"Invalid Authorization scheme (expected Bearer)",
|
||||
))?;
|
||||
|
||||
// Constant-time comparison: prevents timing side-channel attacks.
|
||||
// We first check lengths match (length itself is not secret since UUIDs
|
||||
|
|
@ -209,11 +221,7 @@ async fn post_mcp(
|
|||
// Take write lock immediately to avoid TOCTOU race on MAX_SESSIONS check.
|
||||
let mut sessions = state.sessions.write().await;
|
||||
if sessions.len() >= MAX_SESSIONS {
|
||||
return (
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"Too many active sessions",
|
||||
)
|
||||
.into_response();
|
||||
return (StatusCode::SERVICE_UNAVAILABLE, "Too many active sessions").into_response();
|
||||
}
|
||||
|
||||
let server = McpServer::new_with_events(
|
||||
|
|
@ -242,13 +250,23 @@ async fn post_mcp(
|
|||
match response {
|
||||
Some(resp) => {
|
||||
let mut resp_headers = HeaderMap::new();
|
||||
resp_headers.insert("mcp-session-id", session_id.parse().unwrap_or_else(|_| axum::http::HeaderValue::from_static("invalid")));
|
||||
resp_headers.insert(
|
||||
"mcp-session-id",
|
||||
session_id
|
||||
.parse()
|
||||
.unwrap_or_else(|_| axum::http::HeaderValue::from_static("invalid")),
|
||||
);
|
||||
(StatusCode::OK, resp_headers, Json(resp)).into_response()
|
||||
}
|
||||
None => {
|
||||
// Notifications return 202
|
||||
let mut resp_headers = HeaderMap::new();
|
||||
resp_headers.insert("mcp-session-id", session_id.parse().unwrap_or_else(|_| axum::http::HeaderValue::from_static("invalid")));
|
||||
resp_headers.insert(
|
||||
"mcp-session-id",
|
||||
session_id
|
||||
.parse()
|
||||
.unwrap_or_else(|_| axum::http::HeaderValue::from_static("invalid")),
|
||||
);
|
||||
(StatusCode::ACCEPTED, resp_headers).into_response()
|
||||
}
|
||||
}
|
||||
|
|
@ -273,11 +291,7 @@ async fn post_mcp(
|
|||
let session = match session {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::NOT_FOUND,
|
||||
"Session not found or expired",
|
||||
)
|
||||
.into_response();
|
||||
return (StatusCode::NOT_FOUND, "Session not found or expired").into_response();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -288,7 +302,12 @@ async fn post_mcp(
|
|||
};
|
||||
|
||||
let mut resp_headers = HeaderMap::new();
|
||||
resp_headers.insert("mcp-session-id", session_id.parse().unwrap_or_else(|_| axum::http::HeaderValue::from_static("invalid")));
|
||||
resp_headers.insert(
|
||||
"mcp-session-id",
|
||||
session_id
|
||||
.parse()
|
||||
.unwrap_or_else(|_| axum::http::HeaderValue::from_static("invalid")),
|
||||
);
|
||||
|
||||
match response {
|
||||
Some(resp) => (StatusCode::OK, resp_headers, Json(resp)).into_response(),
|
||||
|
|
@ -308,7 +327,13 @@ async fn delete_mcp(
|
|||
|
||||
let session_id = match session_id_from_headers(&headers) {
|
||||
Some(id) => id,
|
||||
None => return (StatusCode::BAD_REQUEST, "Missing or invalid Mcp-Session-Id header").into_response(),
|
||||
None => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Missing or invalid Mcp-Session-Id header",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let mut sessions = state.sessions.write().await;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ pub struct JsonRpcRequest {
|
|||
pub params: Option<Value>,
|
||||
}
|
||||
|
||||
|
||||
/// JSON-RPC Response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JsonRpcResponse {
|
||||
|
|
@ -129,7 +128,10 @@ impl JsonRpcError {
|
|||
|
||||
#[allow(dead_code)] // Reserved for future resource handling
|
||||
pub fn resource_not_found(uri: &str) -> Self {
|
||||
Self::new(ErrorCode::ResourceNotFound, &format!("Resource not found: {}", uri))
|
||||
Self::new(
|
||||
ErrorCode::ResourceNotFound,
|
||||
&format!("Resource not found: {}", uri),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue