v2.0.9 "Autopilot" — backend event-subscriber + 3,091 LOC orphan cleanup (#46)
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

* feat(v2.0.9): Autopilot — backend event-subscriber routes 6 live events into cognitive hooks

The single architectural change that flips 14 dormant cognitive primitives
into active ones. Before this commit, Vestige's 20-event WebSocket bus had
zero backend subscribers — every emitted event flowed to the dashboard
animation layer and terminated. Cognitive modules with fully-built trigger
methods (synaptic_tagging.trigger_prp, predictive_memory.record_*,
activation_network.activate, prospective_memory.check_triggers, the 6h
auto-consolidation dreamer path) were never actually called from the bus.

New module `crates/vestige-mcp/src/autopilot.rs` spawns two tokio tasks at
startup:

1. Event subscriber — consumes the broadcast::Receiver, routes:
   - MemoryCreated  → synaptic_tagging.trigger_prp(CrossReference)
                    + predictive_memory.record_memory_access(id, preview, tags)
   - SearchPerformed → predictive_memory.record_query(q, [])
                    + record_memory_access on top 10 result_ids
   - MemoryPromoted → activation_network.activate(id, 0.3) spread
   - MemorySuppressed → emit Rac1CascadeSwept (was declared-never-emitted)
   - ImportanceScored (composite > 0.85 AND memory_id present)
                    → storage.promote_memory + re-emit MemoryPromoted
   - Heartbeat (memory_count > 700, 6h cooldown)
                    → spawned find_duplicates sweep (rate-limited)
   The loop holds the CognitiveEngine mutex only per-handler, never across
   an await, so MCP tool dispatch is never starved.

2. Prospective poller — 60s tokio::interval calls
   prospective_memory.check_triggers(Context { timestamp: now, .. }).
   Matched intentions are logged at info! level today; v2.5 "Autonomic"
   upgrades this to MCP sampling/createMessage for agent-side notifications.

ImportanceScored event gained optional `memory_id: Option<String>` field
(#[serde(default)], backward-compatible) so auto-promote has the id to
target. Both existing emit sites (server.rs tool dispatch, dashboard
handlers::score_importance) pass None because they score arbitrary content,
not stored memories — matches current semantics.

docs/VESTIGE_STATE_AND_PLAN.md §15 POST-v2.0.8 ADDENDUM records the full
three-agent audit that produced this architecture (2026-SOTA research,
active-vs-passive module audit, competitor landscape), the v2.0.9/v2.5/v2.6
ship order, and the one-line thesis: "the bottleneck was one missing
event-subscriber task; wiring it flips Vestige from memory library to
cognitive agent that acts on the host LLM."

Verified:
  - cargo check --workspace        clean
  - cargo clippy --workspace -- -D warnings  clean (let-chain on Rust 1.91+)
  - cargo test -p vestige-mcp --lib  356/356 passing, 0 failed

* fix(autopilot): supervisor + dedup race + opt-out env var

Three blockers from the 5-agent v2.0.9 audit, all in autopilot.rs.

1. Supervisor loops around both tokio tasks (event subscriber + prospective
   poller). Previously, if a cognitive hook panicked on a single bad memory,
   the spawned task died permanently and silently — every future event lost.
   Now the outer supervisor catches JoinError::is_panic(), logs the panic
   with full error detail, sleeps 5s, and respawns the inner task. Turns
   a permanent silent failure into a transient hiccup.

2. DedupSweepState struct replaces the bare Option<Instant> timestamp. It
   tracks the in-flight JoinHandle so the next Heartbeat skips spawning a
   second sweep while the first is still running. Previously, the cooldown
   timestamp was set BEFORE spawning the async sweep, which allowed two
   concurrent find_duplicates scans on 100k+ memory DBs where the sweep
   could exceed the 6h cooldown window. is_running() drops finished handles
   so a long-dead sweep doesn't block the next legitimate tick.

3. VESTIGE_AUTOPILOT_ENABLED=0 opt-out. v2.0.8 users updating in place
   can preserve the passive-library contract by setting the env var to
   any of {0, false, no, off}. Any other value (unset, 1, true, etc.)
   enables the default v2.0.9 Autopilot behavior. spawn() early-returns
   with an info! log before any task is spawned.

Audit breakdown:
- Agent 1 (internals): NO-GO → fixed (1, 2)
- Agent 2 (backward compat): NO-GO → fixed (3)
- Agent 3 (orphan cleanup): GO clean
- Agent 4 (runtime safety): GO clean
- Agent 5 (release prep): GO, procedural note logged

Verification:
- cargo check -p vestige-mcp: clean
- cargo test -p vestige-mcp --lib: 373 passed, 0 failed
- cargo clippy -p vestige-mcp --lib --bins -- -D warnings: clean

* chore(release): v2.0.9 "Autopilot"

Bump workspace + vestige-core + vestige-mcp + apps/dashboard to 2.0.9.
CHANGELOG [2.0.9] entry + README hero banner rewrite to "Autopilot".

Scope (two commits on top of v2.0.8):
- 0e9b260: 3,091 LOC orphan-code cleanup
- fe7a68c: Autopilot backend event-subscriber
- HEAD (this branch): supervisor + dedup race + opt-out env var hardening

Pure backend release — tool count unchanged (24), schema unchanged,
JSON-RPC shape unchanged, CLI flags unchanged. Only visible behavior
change is the Autopilot task running in the background, which is
VESTIGE_AUTOPILOT_ENABLED=0-gated.

Test gate: 1,223 passing / 0 failed (workspace, no-fail-fast).
Clippy: clean on vestige-mcp lib + bins with -D warnings.
Audit: 5 parallel agents (internals, backward compat, orphan cleanup,
runtime safety, release prep) — all GO after hardening commit.
This commit is contained in:
Sam Valladares 2026-04-24 02:00:00 -05:00 committed by GitHub
parent 0e9b260518
commit da8c40935e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 724 additions and 11 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "vestige-core"
version = "2.0.8"
version = "2.0.9"
edition = "2024"
rust-version = "1.91"
authors = ["Vestige Team"]

View file

@ -1,6 +1,6 @@
[package]
name = "vestige-mcp"
version = "2.0.8"
version = "2.0.9"
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"]

View file

@ -0,0 +1,504 @@
//! Autopilot — v2.0.9 event-subscriber task.
//!
//! Subscribes to the shared `VestigeEvent` broadcast bus and routes every
//! live event into the cognitive modules that already have trigger methods
//! implemented. Without this layer, Vestige's 30 cognitive modules are a
//! passive library that only responds to MCP tool queries — the event bus
//! emits 20 event types but every one of them terminates at the dashboard.
//!
//! This module closes that gap. It turns Vestige from "fast retrieval with
//! neuroscience modules" into "self-managing cognitive surface that acts
//! without being asked." See `docs/VESTIGE_STATE_AND_PLAN.md` §15 for the
//! full architectural rationale.
//!
//! ## What fires autonomously after v2.0.9
//!
//! - **`MemoryCreated`** → `synaptic_tagging.trigger_prp()` (9h retroactive
//! PRP window on every save) + `predictive_memory.record_memory_access()`
//! (pattern learning for `predict` tool).
//! - **`SearchPerformed`** → `predictive_memory.record_query()` (keeps the
//! query-interest model warm without waiting for the next `predict` call).
//! - **`MemoryPromoted`** → `activation_network.activate()` (spreads a small
//! reinforcement ripple from the promoted node to its neighbors).
//! - **`MemorySuppressed`** → emits the previously-declared-never-emitted
//! `Rac1CascadeSwept` event so the dashboard can render the cascade wave.
//! - **`ImportanceScored` with `composite_score > 0.85`** → auto-`promote`
//! when the score refers to a stored memory.
//! - **`Heartbeat` with `memory_count > DUPLICATES_THRESHOLD`** →
//! opportunistic `find_duplicates` sweep (rate-limited).
//!
//! ## What polls on a timer
//!
//! A 60-second `tokio::interval` calls `prospective_memory.check_triggers()`
//! with the best context we can infer from recent WebSocket activity.
//! Matched intentions are logged at `info!` level today; v2.5 "Autonomic"
//! will promote this to MCP sampling/createMessage notifications that
//! actually reach the agent mid-session.
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::{Mutex, broadcast};
use tracing::{debug, info, warn};
use vestige_core::Storage;
use vestige_core::neuroscience::prospective_memory::Context as ProspectiveContext;
use vestige_core::neuroscience::synaptic_tagging::{ImportanceEvent, ImportanceEventType};
use crate::cognitive::CognitiveEngine;
use crate::dashboard::events::VestigeEvent;
/// Composite-score threshold above which `ImportanceScored` auto-promotes
/// the referenced memory. Conservative default — tune in telemetry.
const AUTO_PROMOTE_THRESHOLD: f64 = 0.85;
/// Memory-count threshold above which a `Heartbeat` triggers a
/// `find_duplicates` sweep. Matches the CLAUDE.md guidance ("totalMemories > 700").
const DUPLICATES_THRESHOLD: usize = 700;
/// Minimum interval between autopilot-triggered `find_duplicates` sweeps,
/// regardless of Heartbeat cadence. Prevents sweep-storms when the count
/// hovers near the threshold.
const DUPLICATES_SWEEP_COOLDOWN_SECS: u64 = 6 * 3600; // 6 hours
/// Interval for polling `prospective_memory.check_triggers()`.
const PROSPECTIVE_POLL_SECS: u64 = 60;
/// Backoff between supervisor restarts after a panicked child task. Short
/// enough that a single bad memory doesn't meaningfully degrade the system,
/// long enough to avoid a tight crash loop if the panic source is persistent.
const SUPERVISOR_RESTART_BACKOFF_SECS: u64 = 5;
/// Tracks an in-flight Heartbeat-triggered dedup sweep so the next Heartbeat
/// can skip spawning a second sweep while the first is still running. The
/// previous implementation stored only the *start* time, which allowed two
/// concurrent scans on databases where `find_duplicates` exceeds the 6h
/// cooldown window.
struct DedupSweepState {
last_fired: Option<Instant>,
in_flight: Option<tokio::task::JoinHandle<()>>,
}
impl DedupSweepState {
fn new() -> Self {
Self {
last_fired: None,
in_flight: None,
}
}
/// True if a previous sweep is still running. Drops a finished handle so
/// a long-dead sweep doesn't keep us from firing the next one.
fn is_running(&mut self) -> bool {
match &self.in_flight {
Some(h) if !h.is_finished() => true,
_ => {
self.in_flight = None;
false
}
}
}
}
/// Launch the Autopilot event-subscriber task + prospective-memory poller.
///
/// Both tasks are supervised: if the inner loop panics on a single bad
/// memory, the supervisor logs the panic and restarts it after a short
/// backoff. This turns a permanent silent-failure mode ("task dies, every
/// future cognitive event lost") into a transient hiccup ("one bad memory
/// skipped, subsystem resumes"). The event loop holds the `CognitiveEngine`
/// mutex only for the duration of a single handler, and never inside an
/// `await`, so it never starves MCP tool dispatch.
pub fn spawn(
cognitive: Arc<Mutex<CognitiveEngine>>,
storage: Arc<Storage>,
event_tx: broadcast::Sender<VestigeEvent>,
) {
// Opt-out: users upgrading in place from v2.0.8 may want to keep the
// "passive library" contract. Set VESTIGE_AUTOPILOT_ENABLED=0 to skip
// spawning both background tasks. Anything else (unset, "1", "true", etc.)
// enables the default v2.0.9 Autopilot behavior.
match std::env::var("VESTIGE_AUTOPILOT_ENABLED").as_deref() {
Ok("0") | Ok("false") | Ok("no") | Ok("off") => {
info!(
"Autopilot disabled via VESTIGE_AUTOPILOT_ENABLED — \
cognitive modules remain passive (v2.0.8 behavior)"
);
return;
}
_ => {}
}
// Event-subscriber supervisor.
{
let cognitive = cognitive.clone();
let storage = storage.clone();
let event_tx = event_tx.clone();
tokio::spawn(async move {
loop {
let rx = event_tx.subscribe();
let cog = cognitive.clone();
let sto = storage.clone();
let etx = event_tx.clone();
let handle = tokio::spawn(async move {
run_event_subscriber(rx, cog, sto, etx).await;
});
match handle.await {
Ok(()) => {
info!("Autopilot event subscriber exited cleanly");
break;
}
Err(e) if e.is_panic() => {
warn!(
error = ?e,
backoff_secs = SUPERVISOR_RESTART_BACKOFF_SECS,
"Autopilot event subscriber panicked — supervisor restarting"
);
tokio::time::sleep(Duration::from_secs(
SUPERVISOR_RESTART_BACKOFF_SECS,
))
.await;
}
Err(e) => {
warn!(error = ?e, "Autopilot event subscriber join error — exiting");
break;
}
}
}
});
}
// Prospective-memory poller supervisor — symmetric restart semantics.
{
let cognitive = cognitive.clone();
tokio::spawn(async move {
loop {
let cog = cognitive.clone();
let handle = tokio::spawn(async move {
run_prospective_poller(cog).await;
});
match handle.await {
Ok(()) => {
info!("Autopilot prospective poller exited cleanly");
break;
}
Err(e) if e.is_panic() => {
warn!(
error = ?e,
backoff_secs = SUPERVISOR_RESTART_BACKOFF_SECS,
"Autopilot prospective poller panicked — supervisor restarting"
);
tokio::time::sleep(Duration::from_secs(
SUPERVISOR_RESTART_BACKOFF_SECS,
))
.await;
}
Err(e) => {
warn!(error = ?e, "Autopilot prospective poller join error — exiting");
break;
}
}
}
});
}
info!("Autopilot spawned (event-subscriber + prospective poller, supervised)");
}
async fn run_event_subscriber(
mut rx: broadcast::Receiver<VestigeEvent>,
cognitive: Arc<Mutex<CognitiveEngine>>,
storage: Arc<Storage>,
event_tx: broadcast::Sender<VestigeEvent>,
) {
// Tracks Heartbeat-triggered auto-sweeps so the next Heartbeat skips
// spawning a second sweep while the first is still running — essential
// on large DBs where `find_duplicates` can outrun the cooldown.
let mut dedup_state = DedupSweepState::new();
loop {
match rx.recv().await {
Ok(event) => {
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");
}
Err(broadcast::error::RecvError::Closed) => {
info!("Autopilot event bus closed — subscriber exiting");
break;
}
}
}
}
async fn handle_event(
event: VestigeEvent,
cognitive: &Arc<Mutex<CognitiveEngine>>,
storage: &Arc<Storage>,
event_tx: &broadcast::Sender<VestigeEvent>,
dedup_state: &mut DedupSweepState,
) {
match event {
VestigeEvent::MemoryCreated {
id,
content_preview,
tags,
timestamp,
..
} => {
// Synaptic tagging: every save is a CrossReference event candidate
// for Frey & Morris 1997 PRP (retroactive importance within a 9h
// window). The system dedups internally, so firing per-save is safe.
let ev = ImportanceEvent {
event_type: ImportanceEventType::CrossReference,
memory_id: Some(id.clone()),
timestamp,
strength: 0.5,
context: None,
};
let tag_outcome = {
let mut cog = cognitive.lock().await;
let outcome = cog.synaptic_tagging.trigger_prp(ev);
// Predictive memory learns the ingested tags for pattern-match
// against future `predict` queries. Method is `&self` (interior
// RwLock), so we keep the cognitive mutex guard for ordering
// but don't actually need &mut on this call.
let _ = cog
.predictive_memory
.record_memory_access(&id, &content_preview, &tags);
outcome
};
debug!(
memory_id = %id,
captured = ?tag_outcome,
"Autopilot: MemoryCreated routed to synaptic_tagging + predictive_memory"
);
}
VestigeEvent::SearchPerformed {
query, result_ids, ..
} => {
// Feed the search into the predictive-retrieval model so the
// speculative prefetch path warms up for the NEXT query. The
// event doesn't carry per-result content, so we record with an
// empty preview — the model only needs the id + tag signal.
let cog = cognitive.lock().await;
let empty_tags_str: [&str; 0] = [];
let empty_tags_string: [String; 0] = [];
let _ = cog.predictive_memory.record_query(&query, &empty_tags_str);
for mid in result_ids.iter().take(10) {
let _ = cog
.predictive_memory
.record_memory_access(mid, "", &empty_tags_string);
}
debug!(
query = %query,
n_results = result_ids.len(),
"Autopilot: SearchPerformed routed to predictive_memory"
);
}
VestigeEvent::MemoryPromoted { id, .. } => {
// Spread a small activation ripple from the promoted node. The
// ActivationNetwork internally handles decay (0.7/hop) so this
// cannot over-amplify.
let mut cog = cognitive.lock().await;
let spread = cog.activation_network.activate(&id, 0.3);
debug!(
memory_id = %id,
n_activated = spread.len(),
"Autopilot: MemoryPromoted triggered activation spread"
);
}
VestigeEvent::MemorySuppressed {
id,
estimated_cascade,
timestamp,
..
} => {
// Surface the previously-declared-never-emitted Rac1CascadeSwept
// event so the dashboard's cascade animation actually fires. The
// per-suppress work happens synchronously inside `suppress_memory`
// on the handler path; this is the observable shadow for the UI.
let _ = event_tx.send(VestigeEvent::Rac1CascadeSwept {
seeds: 1,
neighbors_affected: estimated_cascade,
timestamp,
});
debug!(
memory_id = %id,
cascade_size = estimated_cascade,
"Autopilot: MemorySuppressed → Rac1CascadeSwept emitted"
);
}
VestigeEvent::ImportanceScored {
memory_id,
composite_score,
..
} => {
// Auto-promote only when the score refers to a stored memory AND
// exceeds the threshold. None means "score was computed for
// arbitrary content via the importance tool" — nothing to promote.
if let Some(mid) = memory_id
&& composite_score > AUTO_PROMOTE_THRESHOLD
{
match storage.promote_memory(&mid) {
Ok(node) => {
info!(
memory_id = %mid,
composite_score,
new_retention = node.retention_strength,
"Autopilot: auto-promoted memory with composite > {AUTO_PROMOTE_THRESHOLD}"
);
let _ = event_tx.send(VestigeEvent::MemoryPromoted {
id: node.id,
new_retention: node.retention_strength,
timestamp: chrono::Utc::now(),
});
}
Err(e) => {
warn!(
memory_id = %mid,
error = %e,
"Autopilot: auto-promote failed"
);
}
}
}
}
VestigeEvent::Heartbeat { memory_count, .. } => {
if memory_count <= DUPLICATES_THRESHOLD {
return;
}
// If a prior sweep is still running (possible on very large DBs
// where `find_duplicates` exceeds the 6h cooldown), skip this
// tick rather than spawn a concurrent second scan.
if dedup_state.is_running() {
debug!(
memory_count,
"Autopilot: dedup sweep already in flight — skipping Heartbeat tick"
);
return;
}
let now = Instant::now();
let cooldown_elapsed = dedup_state
.last_fired
.map(|t| now.duration_since(t).as_secs() >= DUPLICATES_SWEEP_COOLDOWN_SECS)
.unwrap_or(true);
if !cooldown_elapsed {
return;
}
dedup_state.last_fired = Some(now);
// Fire the find_duplicates tool with conservative defaults.
// Running on the heartbeat task keeps this off the critical
// MCP-dispatch path. Result is logged only — the user's client
// can still call the tool explicitly for an interactive run.
let storage = storage.clone();
let handle = tokio::spawn(async move {
let args = serde_json::json!({
"similarity_threshold": 0.85,
"limit": 50,
});
match crate::tools::dedup::execute(&storage, Some(args)).await {
Ok(result) => {
let clusters = result
.get("duplicate_clusters")
.and_then(|v| v.as_array())
.map(|a| a.len())
.unwrap_or(0);
if clusters > 0 {
info!(
memory_count,
clusters,
"Autopilot: Heartbeat-triggered find_duplicates surfaced clusters"
);
}
}
Err(e) => {
warn!(
memory_count,
error = %e,
"Autopilot: Heartbeat-triggered find_duplicates failed"
);
}
}
});
dedup_state.in_flight = Some(handle);
}
// Events that carry no autopilot work today. Explicit pass-through so
// adding a new event variant upstream produces a non_exhaustive_match
// compiler nudge here.
VestigeEvent::MemoryUpdated { .. }
| VestigeEvent::MemoryDeleted { .. }
| VestigeEvent::MemoryDemoted { .. }
| VestigeEvent::MemoryUnsuppressed { .. }
| VestigeEvent::Rac1CascadeSwept { .. }
| VestigeEvent::DeepReferenceCompleted { .. }
| VestigeEvent::DreamStarted { .. }
| VestigeEvent::DreamProgress { .. }
| VestigeEvent::DreamCompleted { .. }
| VestigeEvent::ConsolidationStarted { .. }
| VestigeEvent::ConsolidationCompleted { .. }
| VestigeEvent::RetentionDecayed { .. }
| VestigeEvent::ConnectionDiscovered { .. }
| VestigeEvent::ActivationSpread { .. } => {}
}
}
/// Background task that polls `prospective_memory.check_triggers()` every
/// `PROSPECTIVE_POLL_SECS` seconds. Today triggers are logged at info!
/// level; v2.5 "Autonomic" upgrades this to fire MCP sampling/createMessage
/// notifications so the agent sees intentions mid-conversation.
async fn run_prospective_poller(cognitive: Arc<Mutex<CognitiveEngine>>) {
// Short delay on startup so hydration + other init settles first.
tokio::time::sleep(Duration::from_secs(10)).await;
let mut ticker = tokio::time::interval(Duration::from_secs(PROSPECTIVE_POLL_SECS));
// Skip the immediate first tick that `interval` fires.
ticker.tick().await;
loop {
ticker.tick().await;
let context = ProspectiveContext {
timestamp: chrono::Utc::now(),
..Default::default()
};
let triggered = {
let cog = cognitive.lock().await;
cog.prospective_memory.check_triggers(&context)
};
match triggered {
Ok(intentions) if !intentions.is_empty() => {
info!(
n_triggered = intentions.len(),
ids = ?intentions.iter().map(|i| i.id.as_str()).collect::<Vec<_>>(),
"Autopilot: prospective memory triggered intentions"
);
// v2.5 "Autonomic" will emit MCP sampling/createMessage here
// so the agent actually sees the intention mid-conversation.
}
Ok(_) => {
// No triggers — silent. This runs every 60s and the common
// case is no work to do.
}
Err(e) => {
warn!(error = %e, "Autopilot: prospective check_triggers failed");
}
}
}
}

View file

@ -142,6 +142,12 @@ pub enum VestigeEvent {
// -- Importance --
ImportanceScored {
/// v2.0.9: memory the score refers to, if the score was computed for a
/// stored memory (None when scoring arbitrary content via importance tool).
/// Required so the Autopilot event-subscriber can auto-promote on
/// composite_score > 0.85 without having to re-query by content.
#[serde(default)]
memory_id: Option<String>,
content_preview: String,
composite_score: f64,
novelty: f64,

View file

@ -972,6 +972,7 @@ pub async fn score_importance(
let attention = score.attention;
state.emit(VestigeEvent::ImportanceScored {
memory_id: None, // /api/importance scores arbitrary content, not a stored memory
content_preview: req.content.chars().take(80).collect(),
composite_score: composite,
novelty,

View file

@ -2,6 +2,7 @@
//!
//! Shared modules accessible to all binaries in the crate.
pub mod autopilot;
pub mod cognitive;
pub mod dashboard;
pub mod protocol;

View file

@ -309,6 +309,20 @@ async fn main() {
let (event_tx, _) =
tokio::sync::broadcast::channel::<vestige_mcp::dashboard::events::VestigeEvent>(1024);
// v2.0.9 "Autopilot" — spawn the backend event-subscriber that routes
// every live WebSocket event into the cognitive modules that already
// have trigger methods implemented. Without this, the 20 event types
// terminate at the dashboard and the cognitive engine is a passive
// library that only responds to MCP tool queries.
//
// See `crates/vestige-mcp/src/autopilot.rs` for the routing table and
// `docs/VESTIGE_STATE_AND_PLAN.md` §15 for the architectural rationale.
vestige_mcp::autopilot::spawn(
Arc::clone(&cognitive),
Arc::clone(&storage),
event_tx.clone(),
);
// Spawn dashboard HTTP server alongside MCP server (now with CognitiveEngine access)
if config.dashboard_enabled {
let dashboard_port = std::env::var("VESTIGE_DASHBOARD_PORT")

View file

@ -1328,6 +1328,7 @@ impl McpServer {
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
self.emit(VestigeEvent::ImportanceScored {
memory_id: None, // importance_score tool runs on arbitrary content
content_preview: preview,
composite_score: composite,
novelty,