diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 412f083..b35a9a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,22 +56,20 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v4 with: - version: 9 + version: 10 - name: Install Node.js uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm - cache-dependency-path: apps/dashboard/pnpm-lock.yaml + cache-dependency-path: pnpm-lock.yaml - name: Install dependencies run: pnpm install --frozen-lockfile - working-directory: apps/dashboard - name: Build dashboard - run: pnpm build - working-directory: apps/dashboard + run: pnpm --filter dashboard build release-build: name: Release Build (${{ matrix.target }}) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dac0725..9ff80b7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,7 +43,7 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v4 with: - version: 9 + version: 10 - name: Install Node.js uses: actions/setup-node@v4 @@ -52,9 +52,8 @@ jobs: - name: Build dashboard run: | - cd apps/dashboard pnpm install --frozen-lockfile - pnpm build + pnpm --filter dashboard build - name: Install Rust uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c7b750b..7f169eb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,7 @@ name: Test Suite on: push: - branches: [main, develop] + branches: [main] pull_request: branches: [main] @@ -52,11 +52,11 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 with: - version: 9 + version: 10 - uses: actions/setup-node@v4 with: node-version: 22 - - run: cd apps/dashboard && pnpm install --frozen-lockfile && pnpm build + - run: pnpm install --frozen-lockfile && pnpm --filter dashboard build coverage: name: Code Coverage diff --git a/CLAUDE.md b/CLAUDE.md index 9600966..a6acaf8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ -# Vestige v1.8.0 — Cognitive Memory System +# Vestige v2.0.0 — Cognitive Memory System -Vestige is your long-term memory. It implements real neuroscience: FSRS-6 spaced repetition, synaptic tagging, prediction error gating, hippocampal indexing, spreading activation, and 28 stateful cognitive modules. **Use it automatically.** +Vestige is your long-term memory. It implements real neuroscience: FSRS-6 spaced repetition, synaptic tagging, prediction error gating, hippocampal indexing, spreading activation, and 29 stateful cognitive modules. **Use it automatically.** --- @@ -27,9 +27,9 @@ Say "Remembering..." then retrieve context before answering. --- -## The 19 Tools +## The 21 Tools -### Context Packets (1 tool) — v1.8.0 +### Context Packets (1 tool) | Tool | When to Use | |------|-------------| | `session_context` | **One-call session initialization.** Replaces 5 separate calls (search × 2, intention check, system_status, predict) with a single token-budgeted response. Returns markdown context + `automationTriggers` (needsDream/needsBackup/needsGc) + `expandable` IDs for on-demand full retrieval. Params: `queries` (string[]), `token_budget` (100-10000, default 1000), `context` ({codebase, topics, file}), `include_status/include_intentions/include_predictions` (bool). | @@ -53,7 +53,7 @@ Say "Remembering..." then retrieve context before answering. | `memory_timeline` | Browse memories chronologically. Grouped by day. Filter by type, tags, date range. When user references a time period ("last week", "yesterday"). | | `memory_changelog` | Audit trail. Per-memory: state transitions. System-wide: consolidations + recent changes. When debugging memory issues. | -### Cognitive (3 tools) — v1.5.0 +### Cognitive (3 tools) | Tool | When to Use | |------|-------------| | `dream` | Trigger memory consolidation — replays recent memories to discover hidden connections and synthesize insights. At session start if >24h since last dream, after every 50 saves. | @@ -66,6 +66,12 @@ Say "Remembering..." then retrieve context before answering. | `importance_score` | Score content importance before deciding whether to save. 4-channel model: novelty, arousal, reward, attention. Composite > 0.6 = worth saving. | | `find_duplicates` | Find near-duplicate memory clusters via cosine similarity. Returns merge/review suggestions. Run when memory count > 700 or on user request. | +### Autonomic (2 tools) +| Tool | When to Use | +|------|-------------| +| `memory_health` | Retention dashboard — avg retention, distribution buckets, trend (improving/declining/stable), recommendation. Lightweight alternative to system_status focused on memory quality. | +| `memory_graph` | 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. | + ### Maintenance (5 tools) | Tool | When to Use | |------|-------------| @@ -168,11 +174,11 @@ smart_ingest({ --- -## CognitiveEngine — 28 Modules +## CognitiveEngine — 29 Modules All modules persist across tool calls as stateful instances: -**Neuroscience (15):** ActivationNetwork, SynapticTaggingSystem, HippocampalIndex, ContextMatcher, AccessibilityCalculator, CompetitionManager, StateUpdateService, ImportanceSignals, NoveltySignal, ArousalSignal, RewardSignal, AttentionSignal, PredictiveMemory, ProspectiveMemory, IntentionParser +**Neuroscience (16):** ActivationNetwork, SynapticTaggingSystem, HippocampalIndex, ContextMatcher, AccessibilityCalculator, CompetitionManager, StateUpdateService, ImportanceSignals, NoveltySignal, ArousalSignal, RewardSignal, AttentionSignal, EmotionalMemory, PredictiveMemory, ProspectiveMemory, IntentionParser **Advanced (11):** ImportanceTracker, ReconsolidationManager, IntentDetector, ActivityTracker, MemoryDreamer, MemoryChainBuilder, MemoryCompressor, CrossProjectLearner, AdaptiveEmbedder, SpeculativeRetriever, ConsolidationScheduler @@ -209,12 +215,12 @@ Memory is retrieval. Searching strengthens memory. Search liberally, save aggres ## Development -- **Crate:** `vestige-mcp` v1.8.0, Rust 2024 edition, Rust 1.93.1 -- **Tests:** 651 tests (313 core + 338 mcp), zero warnings +- **Crate:** `vestige-mcp` v2.0.1, Rust 2024 edition, MSRV 1.91 +- **Tests:** 1,238 tests, zero warnings - **Build:** `cargo build --release -p vestige-mcp` - **Features:** `embeddings` + `vector-search` (default on) - **Architecture:** `McpServer` holds `Arc` + `Arc>` - **Storage:** Interior mutability — `Storage` uses `Mutex` for reader/writer split, all methods take `&self`. WAL mode for concurrent reads + writes. - **Entry:** `src/main.rs` → stdio JSON-RPC server - **Tools:** `src/tools/` — one file per tool, each exports `schema()` + `execute()` -- **Cognitive:** `src/cognitive.rs` — 28-field struct, initialized once at startup +- **Cognitive:** `src/cognitive.rs` — 29-field struct, initialized once at startup diff --git a/Cargo.lock b/Cargo.lock index fce87fe..dc7f5eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -158,17 +158,6 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -194,7 +183,7 @@ dependencies = [ "log", "num-rational", "num-traits", - "pastey 0.1.1", + "pastey", "rayon", "thiserror 2.0.18", "v_frame", @@ -839,18 +828,8 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", + "darling_core", + "darling_macro", ] [[package]] @@ -867,37 +846,13 @@ dependencies = [ "syn", ] -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core 0.20.11", - "quote", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core 0.23.0", + "darling_core", "quote", "syn", ] @@ -942,7 +897,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "darling 0.20.11", + "darling", "proc-macro2", "quote", "syn", @@ -1019,12 +974,6 @@ dependencies = [ "syn", ] -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - [[package]] name = "dyn-stack" version = "0.13.2" @@ -1303,21 +1252,6 @@ dependencies = [ "libc", ] -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - [[package]] name = "futures-channel" version = "0.3.31" @@ -1325,7 +1259,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", - "futures-sink", ] [[package]] @@ -1334,17 +1267,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - [[package]] name = "futures-io" version = "0.3.31" @@ -1380,7 +1302,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -3070,12 +2991,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" -[[package]] -name = "pastey" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" - [[package]] name = "pathdiff" version = "0.2.3" @@ -3464,26 +3379,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "regex" version = "1.12.3" @@ -3576,41 +3471,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rmcp" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a621b37a548ff6ab6292d57841eb25785a7f146d89391a19c9f199414bd13da" -dependencies = [ - "async-trait", - "base64 0.22.1", - "chrono", - "futures", - "pastey 0.2.1", - "pin-project-lite", - "rmcp-macros", - "schemars", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "rmcp-macros" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b79ed92303f9262db79575aa8c3652581668e9d136be6fd0b9ededa78954c95" -dependencies = [ - "darling 0.23.0", - "proc-macro2", - "quote", - "serde_json", - "syn", -] - [[package]] name = "rsqlite-vfs" version = "0.1.0" @@ -3737,32 +3597,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "schemars" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" -dependencies = [ - "chrono", - "dyn-clone", - "ref-cast", - "schemars_derive", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -3840,17 +3674,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "serde_json" version = "1.0.149" @@ -4671,7 +4494,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vestige-core" -version = "2.0.0" +version = "2.0.1" dependencies = [ "chrono", "criterion", @@ -4706,7 +4529,7 @@ dependencies = [ [[package]] name = "vestige-mcp" -version = "2.0.0" +version = "2.0.1" dependencies = [ "anyhow", "axum", @@ -4718,7 +4541,6 @@ dependencies = [ "include_dir", "mime_guess", "open", - "rmcp", "rusqlite", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 27994a3..348506f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ exclude = [ ] [workspace.package] -version = "1.9.2" +version = "2.0.1" edition = "2024" license = "AGPL-3.0-only" repository = "https://github.com/samvallad33/vestige" diff --git a/README.md b/README.md index 8dc57b1..ed218fc 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![GitHub stars](https://img.shields.io/github/stars/samvallad33/vestige?style=social)](https://github.com/samvallad33/vestige) [![Release](https://img.shields.io/github/v/release/samvallad33/vestige)](https://github.com/samvallad33/vestige/releases/latest) -[![Tests](https://img.shields.io/badge/tests-734%20passing-brightgreen)](https://github.com/samvallad33/vestige/actions) +[![Tests](https://img.shields.io/badge/tests-1238%20passing-brightgreen)](https://github.com/samvallad33/vestige/actions) [![License](https://img.shields.io/badge/license-AGPL--3.0-blue)](LICENSE) [![MCP Compatible](https://img.shields.io/badge/MCP-compatible-green)](https://modelcontextprotocol.io) @@ -25,10 +25,10 @@ Built on 130 years of memory research — FSRS-6 spaced repetition, prediction e - **3D Memory Dashboard** — SvelteKit + Three.js neural visualization with real-time WebSocket events, bloom post-processing, force-directed graph layout. Watch your AI's mind in real-time. - **WebSocket Event Bus** — Every cognitive operation broadcasts events: memory creation, search, dreaming, consolidation, retention decay - **HyDE Query Expansion** — Template-based Hypothetical Document Embeddings for dramatically improved search quality on conceptual queries -- **Nomic v2 MoE Ready** — fastembed 5.11 with optional Nomic Embed Text v2 MoE (475M params, 8 experts) + Metal GPU acceleration +- **Nomic v2 MoE (experimental)** — fastembed 5.11 with optional Nomic Embed Text v2 MoE (475M params, 8 experts) + Metal GPU acceleration. Default: v1.5 (8192 token context) - **Command Palette** — `Cmd+K` navigation, keyboard shortcuts, responsive mobile layout, PWA installable - **FSRS Decay Visualization** — SVG retention curves with predicted decay at 1d/7d/30d, endangered memory alerts -- **29 cognitive modules** — 734 tests, 77,840+ LOC +- **29 cognitive modules** — 1,238 tests, 79,600+ LOC --- @@ -68,10 +68,10 @@ sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ **npm:** ```bash -npm install -g vestige-mcp +npm install -g vestige-mcp-server ``` -**Build from source:** +**Build from source (requires Rust 1.91+):** ```bash git clone https://github.com/samvallad33/vestige && cd vestige cargo build --release -p vestige-mcp @@ -267,8 +267,8 @@ At the start of every session: | Metric | Value | |--------|-------| -| **Language** | Rust 2024 edition | -| **Codebase** | 77,840+ lines, 734 tests | +| **Language** | Rust 2024 edition (MSRV 1.91) | +| **Codebase** | 79,600+ lines, 1,238 tests | | **Binary size** | ~20MB | | **Embeddings** | Nomic Embed Text v1.5 (768d → 256d Matryoshka, 8192 context) | | **Vector search** | USearch HNSW (20x faster than FAISS) | @@ -276,7 +276,7 @@ At the start of every session: | **Storage** | SQLite + FTS5 (optional SQLCipher encryption) | | **Dashboard** | SvelteKit 2 + Svelte 5 + Three.js + Tailwind CSS 4 | | **Transport** | MCP stdio (JSON-RPC 2.0) + WebSocket | -| **Cognitive modules** | 29 stateful (15 neuroscience, 12 advanced, 2 search) | +| **Cognitive modules** | 29 stateful (16 neuroscience, 11 advanced, 2 search) | | **First run** | Downloads embedding model (~130MB), then fully offline | | **Platforms** | macOS (ARM/Intel), Linux (x86_64), Windows | @@ -376,5 +376,5 @@ AGPL-3.0 — free to use, modify, and self-host. If you offer Vestige as a netwo

Built by @samvallad33
- 77,840+ lines of Rust · 29 cognitive modules · 130 years of memory research · one 22MB binary + 79,600+ lines of Rust · 29 cognitive modules · 130 years of memory research · one 22MB binary

diff --git a/crates/vestige-core/Cargo.toml b/crates/vestige-core/Cargo.toml index 0177c30..b8aeab6 100644 --- a/crates/vestige-core/Cargo.toml +++ b/crates/vestige-core/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "vestige-core" -version = "2.0.0" +version = "2.0.1" edition = "2024" -rust-version = "1.85" +rust-version = "1.91" authors = ["Vestige Team"] description = "Cognitive memory engine - FSRS-6 spaced repetition, semantic embeddings, and temporal memory" license = "AGPL-3.0-only" @@ -37,11 +37,6 @@ qwen3-reranker = ["embeddings", "fastembed/qwen3"] # Metal GPU acceleration on Apple Silicon (significantly faster inference) metal = ["fastembed/metal"] -# Full feature set including MCP protocol support -full = ["embeddings", "vector-search"] - -# MCP (Model Context Protocol) support for Claude integration -mcp = [] [dependencies] # Serialization diff --git a/crates/vestige-core/src/advanced/cross_project.rs b/crates/vestige-core/src/advanced/cross_project.rs index d8fc0d9..126463c 100644 --- a/crates/vestige-core/src/advanced/cross_project.rs +++ b/crates/vestige-core/src/advanced/cross_project.rs @@ -431,12 +431,11 @@ impl CrossProjectLearner { // Check each trigger for trigger in &pattern.pattern.triggers { - if let Some((matches, reason)) = self.check_trigger(trigger, context) { - if matches { + if let Some((matches, reason)) = self.check_trigger(trigger, context) + && matches { match_scores.push(trigger.confidence); match_reasons.push(reason); } - } } if match_scores.is_empty() { @@ -547,12 +546,11 @@ impl CrossProjectLearner { let success_rate = success_count as f64 / total_count as f64; - if let Ok(mut patterns) = self.patterns.write() { - if let Some(pattern) = patterns.get_mut(pattern_id) { + if let Ok(mut patterns) = self.patterns.write() + && let Some(pattern) = patterns.get_mut(pattern_id) { pattern.success_rate = success_rate; pattern.application_count = total_count as u32; } - } } fn extract_patterns_from_category( @@ -596,8 +594,8 @@ impl CrossProjectLearner { // Create a potential pattern (simplified) let pattern_id = format!("auto-{}-{}", category_to_string(&category), keyword); - if let Ok(mut patterns) = self.patterns.write() { - if !patterns.contains_key(&pattern_id) { + if let Ok(mut patterns) = self.patterns.write() + && !patterns.contains_key(&pattern_id) { patterns.insert( pattern_id.clone(), UniversalPattern { @@ -629,7 +627,6 @@ impl CrossProjectLearner { }, ); } - } } } } diff --git a/crates/vestige-core/src/advanced/dreams.rs b/crates/vestige-core/src/advanced/dreams.rs index 5ba10fb..88a86ce 100644 --- a/crates/vestige-core/src/advanced/dreams.rs +++ b/crates/vestige-core/src/advanced/dreams.rs @@ -454,11 +454,10 @@ impl ConsolidationScheduler { if let Ok(mut graph) = self.connections.write() { // Strengthen connections between sequentially replayed memories for window in replay.sequence.windows(2) { - if let [id_a, id_b] = window { - if graph.strengthen_connection(id_a, id_b, 0.1) { + if let [id_a, id_b] = window + && graph.strengthen_connection(id_a, id_b, 0.1) { strengthened += 1; } - } } // Also strengthen based on discovered patterns @@ -704,13 +703,12 @@ impl ConnectionGraph { let mut strengthened = false; for (a, b) in [(from_id, to_id), (to_id, from_id)] { - if let Some(connections) = self.connections.get_mut(a) { - if let Some(conn) = connections.iter_mut().find(|c| c.target_id == b) { + if let Some(connections) = self.connections.get_mut(a) + && let Some(conn) = connections.iter_mut().find(|c| c.target_id == b) { conn.strength = (conn.strength + boost).min(2.0); conn.last_strengthened = now; strengthened = true; } - } } strengthened @@ -1478,11 +1476,10 @@ impl MemoryDreamer { } // Try to generate insight from this cluster - if let Some(insight) = self.generate_insight_from_cluster(&cluster_memories) { - if insight.novelty_score >= self.config.min_novelty { + if let Some(insight) = self.generate_insight_from_cluster(&cluster_memories) + && insight.novelty_score >= self.config.min_novelty { insights.push(insight); } - } if insights.len() >= self.config.max_insights { break; diff --git a/crates/vestige-core/src/advanced/importance.rs b/crates/vestige-core/src/advanced/importance.rs index 451316e..3579626 100644 --- a/crates/vestige-core/src/advanced/importance.rs +++ b/crates/vestige-core/src/advanced/importance.rs @@ -230,13 +230,11 @@ impl ImportanceTracker { self.on_retrieved(memory_id, was_helpful); // Store context with event - if let Ok(mut events) = self.recent_events.write() { - if let Some(event) = events.last_mut() { - if event.memory_id == memory_id { + if let Ok(mut events) = self.recent_events.write() + && let Some(event) = events.last_mut() + && event.memory_id == memory_id { event.context = Some(context.to_string()); } - } - } } /// Apply importance decay to all memories diff --git a/crates/vestige-core/src/advanced/intent.rs b/crates/vestige-core/src/advanced/intent.rs index ec6c15b..61cfacc 100644 --- a/crates/vestige-core/src/advanced/intent.rs +++ b/crates/vestige-core/src/advanced/intent.rs @@ -561,11 +561,10 @@ impl IntentDetector { score += 0.2; } ActionType::FileOpened | ActionType::FileEdited => { - if let Some(file) = &action.file { - if let Some(name) = file.file_name() { + if let Some(file) = &action.file + && let Some(name) = file.file_name() { suspected_area = name.to_string_lossy().to_string(); } - } } _ => {} } diff --git a/crates/vestige-core/src/advanced/reconsolidation.rs b/crates/vestige-core/src/advanced/reconsolidation.rs index 00c2912..7b201e2 100644 --- a/crates/vestige-core/src/advanced/reconsolidation.rs +++ b/crates/vestige-core/src/advanced/reconsolidation.rs @@ -516,15 +516,14 @@ impl ReconsolidationManager { return false; } - if let Some(state) = self.labile_memories.get_mut(memory_id) { - if state.is_within_window(self.labile_window) { + if let Some(state) = self.labile_memories.get_mut(memory_id) + && state.is_within_window(self.labile_window) { let success = state.add_modification(modification); if success { self.stats.total_modifications += 1; } return success; } - } false } @@ -690,15 +689,14 @@ impl ReconsolidationManager { if let Ok(history) = self.retrieval_history.read() { for record in history.iter() { - if record.memory_id == memory_id { - if let Some(context) = &record.context { + if record.memory_id == memory_id + && let Some(context) = &record.context { for co_id in &context.co_retrieved { if co_id != memory_id { *co_retrieved.entry(co_id.clone()).or_insert(0) += 1; } } } - } } } @@ -772,7 +770,7 @@ fn truncate(s: &str, max_len: usize) -> &str { if s.len() <= max_len { s } else { - &s[..max_len] + &s[..s.floor_char_boundary(max_len)] } } diff --git a/crates/vestige-core/src/advanced/speculative.rs b/crates/vestige-core/src/advanced/speculative.rs index 30846ba..7b9442e 100644 --- a/crates/vestige-core/src/advanced/speculative.rs +++ b/crates/vestige-core/src/advanced/speculative.rs @@ -265,13 +265,12 @@ impl SpeculativeRetriever { } // Update file-memory associations - if let Some(file) = file_context { - if let Ok(mut map) = self.file_memory_map.write() { + if let Some(file) = file_context + && let Ok(mut map) = self.file_memory_map.write() { map.entry(file.to_string()) .or_insert_with(Vec::new) .push(memory_id.to_string()); } - } } /// Get cached predictions diff --git a/crates/vestige-core/src/codebase/context.rs b/crates/vestige-core/src/codebase/context.rs index 6a6eb52..a8ff103 100644 --- a/crates/vestige-core/src/codebase/context.rs +++ b/crates/vestige-core/src/codebase/context.rs @@ -586,11 +586,10 @@ impl ContextCapture { } // Java Spring - if let Ok(content) = fs::read_to_string(self.project_root.join("pom.xml")) { - if content.contains("spring") { + if let Ok(content) = fs::read_to_string(self.project_root.join("pom.xml")) + && content.contains("spring") { frameworks.push(Framework::Spring); } - } // Ruby Rails if self.file_exists("config/routes.rb") { @@ -613,30 +612,27 @@ impl ContextCapture { /// Detect the project name from config files fn detect_project_name(&self) -> Result> { // Try Cargo.toml - if let Ok(content) = fs::read_to_string(self.project_root.join("Cargo.toml")) { - if let Some(name) = self.extract_toml_value(&content, "name") { + if let Ok(content) = fs::read_to_string(self.project_root.join("Cargo.toml")) + && let Some(name) = self.extract_toml_value(&content, "name") { return Ok(Some(name)); } - } // Try package.json - if let Ok(content) = fs::read_to_string(self.project_root.join("package.json")) { - if let Some(name) = self.extract_json_value(&content, "name") { + if let Ok(content) = fs::read_to_string(self.project_root.join("package.json")) + && let Some(name) = self.extract_json_value(&content, "name") { return Ok(Some(name)); } - } // Try pyproject.toml - if let Ok(content) = fs::read_to_string(self.project_root.join("pyproject.toml")) { - if let Some(name) = self.extract_toml_value(&content, "name") { + if let Ok(content) = fs::read_to_string(self.project_root.join("pyproject.toml")) + && let Some(name) = self.extract_toml_value(&content, "name") { return Ok(Some(name)); } - } // Try go.mod - if let Ok(content) = fs::read_to_string(self.project_root.join("go.mod")) { - if let Some(line) = content.lines().next() { - if line.starts_with("module ") { + if let Ok(content) = fs::read_to_string(self.project_root.join("go.mod")) + && let Some(line) = content.lines().next() + && line.starts_with("module ") { let name = line .trim_start_matches("module ") .split('/') @@ -647,8 +643,6 @@ impl ContextCapture { return Ok(Some(name)); } } - } - } // Fall back to directory name Ok(self @@ -734,8 +728,8 @@ impl ContextCapture { // Check test directories for test_dir in test_dirs { let test_path = self.project_root.join(test_dir); - if test_path.exists() { - if let Ok(entries) = fs::read_dir(&test_path) { + if test_path.exists() + && let Ok(entries) = fs::read_dir(&test_path) { for entry in entries.filter_map(|e| e.ok()) { let entry_path = entry.path(); if let Some(entry_stem) = entry_path.file_stem() { @@ -746,7 +740,6 @@ impl ContextCapture { } } } - } } // For Rust, look for mod.rs in same directory @@ -799,9 +792,9 @@ impl ContextCapture { /// Detect the module a file belongs to fn detect_module(&self, path: &Path) -> Option { // For Rust, use the parent directory name relative to src/ - if path.extension().map(|e| e == "rs").unwrap_or(false) { - if let Ok(relative) = path.strip_prefix(&self.project_root) { - if let Ok(src_relative) = relative.strip_prefix("src") { + if path.extension().map(|e| e == "rs").unwrap_or(false) + && let Ok(relative) = path.strip_prefix(&self.project_root) + && let Ok(src_relative) = relative.strip_prefix("src") { // Get the module path let components: Vec<_> = src_relative .parent()? @@ -813,16 +806,13 @@ impl ContextCapture { return Some(components.join("::")); } } - } - } // For TypeScript/JavaScript, use the parent directory if path .extension() .map(|e| e == "ts" || e == "tsx" || e == "js" || e == "jsx") .unwrap_or(false) - { - if let Ok(relative) = path.strip_prefix(&self.project_root) { + && let Ok(relative) = path.strip_prefix(&self.project_root) { // Skip src/ or lib/ prefix let relative = relative .strip_prefix("src") @@ -836,7 +826,6 @@ impl ContextCapture { } } } - } None } @@ -874,14 +863,12 @@ impl ContextCapture { fn extract_toml_value(&self, content: &str, key: &str) -> Option { for line in content.lines() { let trimmed = line.trim(); - if trimmed.starts_with(&format!("{} ", key)) - || trimmed.starts_with(&format!("{}=", key)) - { - if let Some(value) = trimmed.split('=').nth(1) { + if (trimmed.starts_with(&format!("{} ", key)) + || trimmed.starts_with(&format!("{}=", key))) + && let Some(value) = trimmed.split('=').nth(1) { let value = value.trim().trim_matches('"').trim_matches('\''); return Some(value.to_string()); } - } } None } diff --git a/crates/vestige-core/src/codebase/git.rs b/crates/vestige-core/src/codebase/git.rs index fde40e1..2b7e21d 100644 --- a/crates/vestige-core/src/codebase/git.rs +++ b/crates/vestige-core/src/codebase/git.rs @@ -274,11 +274,10 @@ impl GitAnalyzer { if let Some(path) = delta.new_file().path() { files.push(path.to_path_buf()); } - if let Some(path) = delta.old_file().path() { - if !files.contains(&path.to_path_buf()) { + if let Some(path) = delta.old_file().path() + && !files.contains(&path.to_path_buf()) { files.push(path.to_path_buf()); } - } } } @@ -492,11 +491,10 @@ impl GitAnalyzer { .single() .unwrap_or_else(Utc::now); - if let Some(since_time) = since { - if commit_time < since_time { + if let Some(since_time) = since + && commit_time < since_time { continue; } - } let message = commit.message().map(|m| m.to_string()).unwrap_or_default(); diff --git a/crates/vestige-core/src/codebase/patterns.rs b/crates/vestige-core/src/codebase/patterns.rs index dd0e94a..ae9e972 100644 --- a/crates/vestige-core/src/codebase/patterns.rs +++ b/crates/vestige-core/src/codebase/patterns.rs @@ -209,8 +209,8 @@ impl PatternDetector { .collect(); for pattern in relevant_patterns { - if let Some(confidence) = self.calculate_match_confidence(code, &code_lower, pattern) { - if confidence >= 0.3 { + if let Some(confidence) = self.calculate_match_confidence(code, &code_lower, pattern) + && confidence >= 0.3 { matches.push(PatternMatch { pattern: pattern.clone(), confidence, @@ -218,7 +218,6 @@ impl PatternDetector { suggestions: self.generate_suggestions(pattern, code), }); } - } } // Sort by confidence diff --git a/crates/vestige-core/src/codebase/watcher.rs b/crates/vestige-core/src/codebase/watcher.rs index bd87cdc..187bb72 100644 --- a/crates/vestige-core/src/codebase/watcher.rs +++ b/crates/vestige-core/src/codebase/watcher.rs @@ -337,14 +337,13 @@ impl CodebaseWatcher { } // Detect patterns if enabled - if config.detect_patterns { - if let Ok(content) = std::fs::read_to_string(path) { + if config.detect_patterns + && let Ok(content) = std::fs::read_to_string(path) { let language = Self::detect_language(path); if let Ok(detector) = detector.try_read() { let _ = detector.detect_patterns(&content, &language); } } - } } FileEventKind::Deleted => { // File was deleted, remove from session @@ -576,13 +575,12 @@ impl ManualEventHandler { } // Detect patterns - if self.config.detect_patterns { - if let Ok(content) = std::fs::read_to_string(path) { + if self.config.detect_patterns + && let Ok(content) = std::fs::read_to_string(path) { let language = CodebaseWatcher::detect_language(path); let detector = self.detector.read().await; let _ = detector.detect_patterns(&content, &language); } - } Ok(()) } diff --git a/crates/vestige-core/src/consolidation/phases.rs b/crates/vestige-core/src/consolidation/phases.rs index 1b13d1c..71f3724 100644 --- a/crates/vestige-core/src/consolidation/phases.rs +++ b/crates/vestige-core/src/consolidation/phases.rs @@ -333,11 +333,10 @@ impl DreamEngine { emotion: &EmotionCategory, ) -> TriageCategory { // High emotional content - if matches!(emotion, EmotionCategory::Frustration | EmotionCategory::Urgency | EmotionCategory::Joy | EmotionCategory::Surprise) { - if node.sentiment_magnitude > 0.4 { + if matches!(emotion, EmotionCategory::Frustration | EmotionCategory::Urgency | EmotionCategory::Joy | EmotionCategory::Surprise) + && node.sentiment_magnitude > 0.4 { return TriageCategory::Emotional; } - } // Future-relevant (intentions, TODOs) let content_lower = node.content.to_lowercase(); @@ -386,7 +385,7 @@ impl DreamEngine { .collect(); // Process replay queue in oscillation waves - let wave_count = (replay_queue.len() + self.wave_batch_size - 1) / self.wave_batch_size; + let wave_count = replay_queue.len().div_ceil(self.wave_batch_size); for wave_idx in 0..wave_count { let wave_start = wave_idx * self.wave_batch_size; @@ -659,8 +658,8 @@ impl DreamEngine { if indices.len() >= 3 && indices.len() <= 10 { pattern_count += 1; // Create a connection between the first and last memory sharing this pattern - if let (Some(&first), Some(&last)) = (indices.first(), indices.last()) { - if first != last { + if let (Some(&first), Some(&last)) = (indices.first(), indices.last()) + && first != last { connections.push(CreativeConnection { memory_a_id: triaged[first].id.clone(), memory_b_id: triaged[last].id.clone(), @@ -672,7 +671,6 @@ impl DreamEngine { connection_type: CreativeConnectionType::CrossDomain, }); } - } } } diff --git a/crates/vestige-core/src/embeddings/local.rs b/crates/vestige-core/src/embeddings/local.rs index c8b4cee..a727a45 100644 --- a/crates/vestige-core/src/embeddings/local.rs +++ b/crates/vestige-core/src/embeddings/local.rs @@ -181,7 +181,7 @@ impl Embedding { /// Create from bytes pub fn from_bytes(bytes: &[u8]) -> Option { - if bytes.len() % 4 != 0 { + if !bytes.len().is_multiple_of(4) { return None; } let vector: Vec = bytes @@ -260,9 +260,13 @@ impl EmbeddingService { let mut model = get_model()?; - // Truncate if too long + // Truncate if too long (char-boundary safe) let text = if text.len() > MAX_TEXT_LENGTH { - &text[..MAX_TEXT_LENGTH] + let mut end = MAX_TEXT_LENGTH; + while !text.is_char_boundary(end) && end > 0 { + end -= 1; + } + &text[..end] } else { text }; @@ -295,7 +299,11 @@ impl EmbeddingService { .iter() .map(|t| { if t.len() > MAX_TEXT_LENGTH { - &t[..MAX_TEXT_LENGTH] + let mut end = MAX_TEXT_LENGTH; + while !t.is_char_boundary(end) && end > 0 { + end -= 1; + } + &t[..end] } else { *t } diff --git a/crates/vestige-core/src/fsrs/scheduler.rs b/crates/vestige-core/src/fsrs/scheduler.rs index eaa611e..1c7edc7 100644 --- a/crates/vestige-core/src/fsrs/scheduler.rs +++ b/crates/vestige-core/src/fsrs/scheduler.rs @@ -241,17 +241,15 @@ impl FSRSScheduler { }; // Apply sentiment boost - if self.enable_sentiment_boost { - if let Some(sentiment) = sentiment_boost { - if sentiment > 0.0 { + if self.enable_sentiment_boost + && let Some(sentiment) = sentiment_boost + && sentiment > 0.0 { new_state.stability = apply_sentiment_boost( new_state.stability, sentiment, self.max_sentiment_boost, ); } - } - } let mut interval = next_interval_with_decay(new_state.stability, self.params.desired_retention, w20) diff --git a/crates/vestige-core/src/neuroscience/context_memory.rs b/crates/vestige-core/src/neuroscience/context_memory.rs index ef2ef1c..e220f9a 100644 --- a/crates/vestige-core/src/neuroscience/context_memory.rs +++ b/crates/vestige-core/src/neuroscience/context_memory.rs @@ -910,39 +910,34 @@ impl ContextMatcher { let mut score = 0.0; // Same session is a very strong match - if let (Some(e_id), Some(r_id)) = (&encoding.session_id, &retrieval.session_id) { - if e_id == r_id { + if let (Some(e_id), Some(r_id)) = (&encoding.session_id, &retrieval.session_id) + && e_id == r_id { return 1.0; } - } // Project match (0.4 weight) - if let (Some(e_proj), Some(r_proj)) = (&encoding.project, &retrieval.project) { - if e_proj == r_proj { + if let (Some(e_proj), Some(r_proj)) = (&encoding.project, &retrieval.project) + && e_proj == r_proj { score += 0.4; } - } // Activity type match (0.3 weight) - if let (Some(e_act), Some(r_act)) = (&encoding.activity_type, &retrieval.activity_type) { - if e_act == r_act { + if let (Some(e_act), Some(r_act)) = (&encoding.activity_type, &retrieval.activity_type) + && e_act == r_act { score += 0.3; } - } // Git branch match (0.2 weight) - if let (Some(e_br), Some(r_br)) = (&encoding.git_branch, &retrieval.git_branch) { - if e_br == r_br { + if let (Some(e_br), Some(r_br)) = (&encoding.git_branch, &retrieval.git_branch) + && e_br == r_br { score += 0.2; } - } // Active file match (0.1 weight) - if let (Some(e_file), Some(r_file)) = (&encoding.active_file, &retrieval.active_file) { - if e_file == r_file { + if let (Some(e_file), Some(r_file)) = (&encoding.active_file, &retrieval.active_file) + && e_file == r_file { score += 0.1; } - } score } diff --git a/crates/vestige-core/src/neuroscience/hippocampal_index.rs b/crates/vestige-core/src/neuroscience/hippocampal_index.rs index 7738787..a8fa0f9 100644 --- a/crates/vestige-core/src/neuroscience/hippocampal_index.rs +++ b/crates/vestige-core/src/neuroscience/hippocampal_index.rs @@ -1075,11 +1075,10 @@ impl ContentStore { pub fn retrieve(&self, pointer: &ContentPointer) -> Result> { // Check cache first let cache_key = self.cache_key(pointer); - if let Ok(cache) = self.cache.read() { - if let Some(data) = cache.get(&cache_key) { + if let Ok(cache) = self.cache.read() + && let Some(data) = cache.get(&cache_key) { return Ok(data.clone()); } - } // Retrieve from storage let data = match &pointer.storage_location { @@ -1131,8 +1130,8 @@ impl ContentStore { return; } - if let Ok(mut cache) = self.cache.write() { - if let Ok(mut size) = self.current_cache_size.write() { + if let Ok(mut cache) = self.cache.write() + && let Ok(mut size) = self.current_cache_size.write() { // Evict if necessary while *size + data_size > self.max_cache_size && !cache.is_empty() { // Simple eviction: remove first entry @@ -1148,7 +1147,6 @@ impl ContentStore { cache.insert(key.to_string(), data.to_vec()); *size += data_size; } - } } /// Retrieve from SQLite (placeholder - to be integrated with Storage) @@ -1394,8 +1392,8 @@ impl HippocampalIndex { let mut match_result = IndexMatch::new(index.clone()); // Calculate semantic score - if let Some(ref query_embedding) = query.semantic_embedding { - if !index.semantic_summary.is_empty() { + if let Some(ref query_embedding) = query.semantic_embedding + && !index.semantic_summary.is_empty() { let query_compressed = self.compress_embedding(query_embedding); match_result.semantic_score = self.cosine_similarity(&query_compressed, &index.semantic_summary); @@ -1404,7 +1402,6 @@ impl HippocampalIndex { continue; } } - } // Calculate text score if let Some(ref text_query) = query.text_query { @@ -1444,25 +1441,22 @@ impl HippocampalIndex { /// Check if an index passes query filters fn passes_filters(&self, index: &MemoryIndex, query: &IndexQuery) -> bool { // Time range filter - if let Some((start, end)) = query.time_range { - if index.temporal_marker.created_at < start || index.temporal_marker.created_at > end { + if let Some((start, end)) = query.time_range + && (index.temporal_marker.created_at < start || index.temporal_marker.created_at > end) { return false; } - } // Importance flags filter - if let Some(ref required) = query.required_flags { - if !index.matches_importance(required.to_bits()) { + if let Some(ref required) = query.required_flags + && !index.matches_importance(required.to_bits()) { return false; } - } // Node type filter - if let Some(ref types) = query.node_types { - if !types.contains(&index.node_type) { + if let Some(ref types) = query.node_types + && !types.contains(&index.node_type) { return false; } - } true } @@ -1579,11 +1573,10 @@ impl HippocampalIndex { let mut memories = Vec::with_capacity(matches.len()); for m in matches { // Record access - if let Ok(mut indices) = self.indices.write() { - if let Some(index) = indices.get_mut(&m.index.memory_id) { + if let Ok(mut indices) = self.indices.write() + && let Some(index) = indices.get_mut(&m.index.memory_id) { index.record_access(); } - } match self.retrieve_content(&m.index) { Ok(memory) => memories.push(memory), @@ -1887,20 +1880,19 @@ impl HippocampalIndex { sentiment_magnitude: f64, ) -> Result { // Check if already indexed - if let Ok(indices) = self.indices.read() { - if indices.contains_key(node_id) { + if let Ok(indices) = self.indices.read() + && indices.contains_key(node_id) { return Err(HippocampalIndexError::MigrationError( "Node already indexed".to_string(), )); } - } // Create the index let barcode = self.index_memory(node_id, content, node_type, created_at, embedding)?; // Update importance flags based on existing data - if let Ok(mut indices) = self.indices.write() { - if let Some(index) = indices.get_mut(node_id) { + if let Ok(mut indices) = self.indices.write() + && let Some(index) = indices.get_mut(node_id) { // Set high retention flag if applicable if retention_strength > 0.7 { index.importance_flags.set_high_retention(true); @@ -1919,7 +1911,6 @@ impl HippocampalIndex { ContentType::Text, )); } - } Ok(barcode) } diff --git a/crates/vestige-core/src/neuroscience/importance_signals.rs b/crates/vestige-core/src/neuroscience/importance_signals.rs index 5a335e5..cf26e63 100644 --- a/crates/vestige-core/src/neuroscience/importance_signals.rs +++ b/crates/vestige-core/src/neuroscience/importance_signals.rs @@ -358,8 +358,8 @@ impl PredictionModel { fn learn(&self, content: &str) { let ngrams = self.extract_ngrams(content); - if let Ok(mut patterns) = self.patterns.write() { - if let Ok(mut total) = self.total_count.write() { + if let Ok(mut patterns) = self.patterns.write() + && let Ok(mut total) = self.total_count.write() { for ngram in ngrams { *patterns.entry(ngram).or_insert(0) += 1; *total += 1; @@ -370,7 +370,6 @@ impl PredictionModel { self.apply_decay(&mut patterns); } } - } } fn compute_prediction_error(&self, content: &str) -> f64 { diff --git a/crates/vestige-core/src/neuroscience/memory_states.rs b/crates/vestige-core/src/neuroscience/memory_states.rs index fd5ffad..8f1bf33 100644 --- a/crates/vestige-core/src/neuroscience/memory_states.rs +++ b/crates/vestige-core/src/neuroscience/memory_states.rs @@ -1266,15 +1266,14 @@ impl MemoryStateInfo { ); } MemoryState::Unavailable => { - if let Some(until) = lifecycle.suppression_until { - if until > now { + if let Some(until) = lifecycle.suppression_until + && until > now { recommendations.push(format!( "This memory is temporarily suppressed. \ It will become accessible again after {}.", until.format("%Y-%m-%d %H:%M UTC") )); } - } } MemoryState::Dormant => { if duration_since_access.num_days() > 20 { diff --git a/crates/vestige-core/src/neuroscience/prospective_memory.rs b/crates/vestige-core/src/neuroscience/prospective_memory.rs index 0fe2dd6..868e1e5 100644 --- a/crates/vestige-core/src/neuroscience/prospective_memory.rs +++ b/crates/vestige-core/src/neuroscience/prospective_memory.rs @@ -694,18 +694,16 @@ impl Intention { } // Check snoozed - if let Some(snoozed_until) = self.snoozed_until { - if Utc::now() < snoozed_until { + if let Some(snoozed_until) = self.snoozed_until + && Utc::now() < snoozed_until { return false; } - } // Check minimum interval - if let Some(last) = self.last_reminded_at { - if (Utc::now() - last) < Duration::minutes(MIN_REMINDER_INTERVAL_MINUTES) { + if let Some(last) = self.last_reminded_at + && (Utc::now() - last) < Duration::minutes(MIN_REMINDER_INTERVAL_MINUTES) { return false; } - } true } @@ -1267,13 +1265,11 @@ impl ProspectiveMemory { // Skip non-active intentions if intention.status != IntentionStatus::Active { // Check if snoozed intention should wake - if intention.status == IntentionStatus::Snoozed { - if let Some(until) = intention.snoozed_until { - if Utc::now() >= until { + if intention.status == IntentionStatus::Snoozed + && let Some(until) = intention.snoozed_until + && Utc::now() >= until { intention.wake(); } - } - } continue; } diff --git a/crates/vestige-core/src/neuroscience/spreading_activation.rs b/crates/vestige-core/src/neuroscience/spreading_activation.rs index fa1af18..55a1fad 100644 --- a/crates/vestige-core/src/neuroscience/spreading_activation.rs +++ b/crates/vestige-core/src/neuroscience/spreading_activation.rs @@ -287,11 +287,10 @@ impl ActivationNetwork { self.edges.insert((source.clone(), target.clone()), edge); // Update node's edge list - if let Some(node) = self.nodes.get_mut(&source) { - if !node.edges.contains(&target) { + if let Some(node) = self.nodes.get_mut(&source) + && !node.edges.contains(&target) { node.edges.push(target); } - } } /// Activate a node and spread activation through the network @@ -314,11 +313,10 @@ impl ActivationNetwork { while let Some((current_id, current_activation, hops, path)) = queue.pop() { // Skip if we've visited this node with higher activation - if let Some(&prev_activation) = visited.get(¤t_id) { - if prev_activation >= current_activation { + if let Some(&prev_activation) = visited.get(¤t_id) + && prev_activation >= current_activation { continue; } - } visited.insert(current_id.clone(), current_activation); // Check hop limit diff --git a/crates/vestige-core/src/storage/sqlite.rs b/crates/vestige-core/src/storage/sqlite.rs index 48aee67..52f4c50 100644 --- a/crates/vestige-core/src/storage/sqlite.rs +++ b/crates/vestige-core/src/storage/sqlite.rs @@ -609,13 +609,13 @@ impl Storage { node_id, embedding.to_bytes(), EMBEDDING_DIMENSIONS as i32, - "all-MiniLM-L6-v2", + "nomic-embed-text-v1.5", now.to_rfc3339(), ], )?; writer.execute( - "UPDATE knowledge_nodes SET has_embedding = 1, embedding_model = 'all-MiniLM-L6-v2' WHERE id = ?1", + "UPDATE knowledge_nodes SET has_embedding = 1, embedding_model = 'nomic-embed-text-v1.5' WHERE id = ?1", params![node_id], )?; } @@ -639,7 +639,7 @@ impl Storage { .prepare("SELECT * FROM knowledge_nodes WHERE id = ?1")?; let node = stmt - .query_row(params![id], |row| Self::row_to_node(row)) + .query_row(params![id], Self::row_to_node) .optional()?; Ok(node) } @@ -1058,7 +1058,7 @@ impl Storage { LIMIT ?2", )?; - let nodes = stmt.query_map(params![now, limit], |row| Self::row_to_node(row))?; + let nodes = stmt.query_map(params![now, limit], Self::row_to_node)?; let mut result = Vec::new(); for node in nodes { @@ -1150,7 +1150,7 @@ impl Storage { )?; let embedding_model: Option = if nodes_with_embeddings > 0 { - Some("all-MiniLM-L6-v2".to_string()) + Some("nomic-embed-text-v1.5".to_string()) } else { None }; @@ -1182,6 +1182,14 @@ impl Storage { .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; let rows = writer .execute("DELETE FROM knowledge_nodes WHERE id = ?1", params![id])?; + + // Clean up vector index to prevent stale search results + #[cfg(all(feature = "embeddings", feature = "vector-search"))] + if rows > 0 + && let Ok(mut index) = self.vector_index.lock() { + let _ = index.remove(id); + } + Ok(rows > 0) } @@ -1199,7 +1207,7 @@ impl Storage { LIMIT ?2", )?; - let nodes = stmt.query_map(params![sanitized_query, limit], |row| Self::row_to_node(row))?; + let nodes = stmt.query_map(params![sanitized_query, limit], Self::row_to_node)?; let mut result = Vec::new(); for node in nodes { @@ -1218,7 +1226,7 @@ impl Storage { LIMIT ?1 OFFSET ?2", )?; - let nodes = stmt.query_map(params![limit, offset], |row| Self::row_to_node(row))?; + let nodes = stmt.query_map(params![limit, offset], Self::row_to_node)?; let mut result = Vec::new(); for node in nodes { @@ -1268,7 +1276,7 @@ impl Storage { ORDER BY retention_strength DESC, created_at DESC LIMIT ?2", )?; - let rows = stmt.query_map(params![node_type, limit], |row| Self::row_to_node(row))?; + let rows = stmt.query_map(params![node_type, limit], Self::row_to_node)?; let mut nodes = Vec::new(); for node in rows.flatten() { nodes.push(node); @@ -1641,7 +1649,7 @@ impl Storage { LIMIT ?2", )?; - let nodes = stmt.query_map(params![timestamp, limit], |row| Self::row_to_node(row))?; + let nodes = stmt.query_map(params![timestamp, limit], Self::row_to_node)?; let mut result = Vec::new(); for node in nodes { @@ -1704,7 +1712,7 @@ impl Storage { .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let mut stmt = reader.prepare(query)?; let params_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect(); - let nodes = stmt.query_map(params_refs.as_slice(), |row| Self::row_to_node(row))?; + let nodes = stmt.query_map(params_refs.as_slice(), Self::row_to_node)?; let mut result = Vec::new(); for node in nodes { @@ -2404,12 +2412,11 @@ impl Storage { /// Generate missing embeddings #[cfg(all(feature = "embeddings", feature = "vector-search"))] fn generate_missing_embeddings(&self) -> Result { - if !self.embedding_service.is_ready() { - if let Err(e) = self.embedding_service.init() { + if !self.embedding_service.is_ready() + && let Err(e) = self.embedding_service.init() { tracing::warn!("Could not initialize embedding model: {}", e); return Ok(0); } - } let nodes: Vec<(String, String)> = { let reader = self.reader.lock() @@ -2615,7 +2622,7 @@ impl Storage { "SELECT * FROM intentions WHERE id = ?1" )?; - stmt.query_row(params![id], |row| Self::row_to_intention(row)) + stmt.query_row(params![id], Self::row_to_intention) .optional() .map_err(StorageError::from) } @@ -2628,7 +2635,7 @@ impl Storage { "SELECT * FROM intentions WHERE status = 'active' ORDER BY priority DESC, created_at ASC" )?; - let rows = stmt.query_map([], |row| Self::row_to_intention(row))?; + let rows = stmt.query_map([], Self::row_to_intention)?; let mut result = Vec::new(); for row in rows { result.push(row?); @@ -2644,7 +2651,7 @@ impl Storage { "SELECT * FROM intentions WHERE status = ?1 ORDER BY priority DESC, created_at ASC" )?; - let rows = stmt.query_map(params![status], |row| Self::row_to_intention(row))?; + let rows = stmt.query_map(params![status], Self::row_to_intention)?; let mut result = Vec::new(); for row in rows { result.push(row?); @@ -2683,7 +2690,7 @@ impl Storage { "SELECT * FROM intentions WHERE status = 'active' AND deadline IS NOT NULL AND deadline < ?1 ORDER BY deadline ASC" )?; - let rows = stmt.query_map(params![now], |row| Self::row_to_intention(row))?; + let rows = stmt.query_map(params![now], Self::row_to_intention)?; let mut result = Vec::new(); for row in rows { result.push(row?); @@ -2775,7 +2782,7 @@ impl Storage { "SELECT * FROM insights ORDER BY generated_at DESC LIMIT ?1" )?; - let rows = stmt.query_map(params![limit], |row| Self::row_to_insight(row))?; + let rows = stmt.query_map(params![limit], Self::row_to_insight)?; let mut result = Vec::new(); for row in rows { result.push(row?); @@ -2791,7 +2798,7 @@ impl Storage { "SELECT * FROM insights WHERE feedback IS NULL ORDER BY novelty_score DESC" )?; - let rows = stmt.query_map([], |row| Self::row_to_insight(row))?; + let rows = stmt.query_map([], Self::row_to_insight)?; let mut result = Vec::new(); for row in rows { result.push(row?); @@ -2874,7 +2881,7 @@ impl Storage { "SELECT * FROM memory_connections WHERE source_id = ?1 OR target_id = ?1 ORDER BY strength DESC" )?; - let rows = stmt.query_map(params![memory_id], |row| Self::row_to_connection(row))?; + let rows = stmt.query_map(params![memory_id], Self::row_to_connection)?; let mut result = Vec::new(); for row in rows { result.push(row?); @@ -2890,7 +2897,7 @@ impl Storage { "SELECT * FROM memory_connections ORDER BY strength DESC" )?; - let rows = stmt.query_map([], |row| Self::row_to_connection(row))?; + let rows = stmt.query_map([], Self::row_to_connection)?; let mut result = Vec::new(); for row in rows { result.push(row?); @@ -2988,7 +2995,7 @@ impl Storage { "SELECT * FROM memory_states WHERE memory_id = ?1" )?; - stmt.query_row(params![memory_id], |row| Self::row_to_memory_state(row)) + stmt.query_row(params![memory_id], Self::row_to_memory_state) .optional() .map_err(StorageError::from) } @@ -3241,14 +3248,13 @@ impl Storage { let name = entry.file_name(); let name_str = name.to_string_lossy(); // Parse vestige-YYYYMMDD-HHMMSS.db - if let Some(ts_part) = name_str.strip_prefix("vestige-").and_then(|s| s.strip_suffix(".db")) { - if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(ts_part, "%Y%m%d-%H%M%S") { + if let Some(ts_part) = name_str.strip_prefix("vestige-").and_then(|s| s.strip_suffix(".db")) + && let Ok(naive) = chrono::NaiveDateTime::parse_from_str(ts_part, "%Y%m%d-%H%M%S") { let dt = naive.and_utc(); if latest.as_ref().is_none_or(|l| dt > *l) { latest = Some(dt); } } - } } } @@ -3406,12 +3412,37 @@ impl Storage { /// Auto-GC memories below threshold (used by retention target system) pub fn gc_below_retention(&self, threshold: f64, min_age_days: i64) -> Result { let cutoff = (Utc::now() - Duration::days(min_age_days)).to_rfc3339(); + + // Collect IDs first for vector index cleanup + #[cfg(all(feature = "embeddings", feature = "vector-search"))] + let doomed_ids: Vec = { + let reader = self.reader.lock() + .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; + let mut stmt = reader.prepare( + "SELECT id FROM knowledge_nodes WHERE retention_strength < ?1 AND created_at < ?2", + )?; + stmt.query_map(params![threshold, cutoff], |row| row.get(0))? + .filter_map(|r| r.ok()) + .collect() + }; + let writer = self.writer.lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; let deleted = writer.execute( "DELETE FROM knowledge_nodes WHERE retention_strength < ?1 AND created_at < ?2", params![threshold, cutoff], )? as i64; + drop(writer); + + // Clean up vector index + #[cfg(all(feature = "embeddings", feature = "vector-search"))] + if deleted > 0 + && let Ok(mut index) = self.vector_index.lock() { + for id in &doomed_ids { + let _ = index.remove(id); + } + } + Ok(deleted) } @@ -3489,7 +3520,7 @@ impl Storage { let mut stmt = reader.prepare( "SELECT * FROM knowledge_nodes WHERE waking_tag = TRUE ORDER BY waking_tag_at DESC LIMIT ?1" )?; - let nodes = stmt.query_map(params![limit], |row| Self::row_to_node(row))?; + let nodes = stmt.query_map(params![limit], Self::row_to_node)?; let mut result = Vec::new(); for node in nodes { result.push(node?); diff --git a/crates/vestige-mcp/Cargo.toml b/crates/vestige-mcp/Cargo.toml index c452fcc..510e6cd 100644 --- a/crates/vestige-mcp/Cargo.toml +++ b/crates/vestige-mcp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vestige-mcp" -version = "2.0.0" +version = "2.0.1" 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"] @@ -32,7 +32,7 @@ path = "src/bin/cli.rs" # ============================================================================ # Includes: FSRS-6, spreading activation, synaptic tagging, hippocampal indexing, # memory states, context memory, importance signals, dreams, and more -vestige-core = { version = "2.0.0", path = "../vestige-core" } +vestige-core = { version = "2.0.1", path = "../vestige-core" } # ============================================================================ # MCP Server Dependencies @@ -61,9 +61,6 @@ tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } # Platform directories directories = "6" -# Official Anthropic MCP Rust SDK -rmcp = "0.14" - # CLI clap = { version = "4", features = ["derive"] } colored = "3" @@ -71,7 +68,7 @@ colored = "3" # SQLite (for backup WAL checkpoint) rusqlite = { version = "0.38", features = ["bundled"] } -# Dashboard (v1.2) - hyper/tower already in Cargo.lock via rmcp/reqwest +# Dashboard (v2.0) - HTTP server + WebSocket + embedded SvelteKit axum = { version = "0.8", default-features = false, features = ["json", "query", "tokio", "http1", "ws"] } tower = { version = "0.5", features = ["limit"] } tower-http = { version = "0.6", features = ["cors", "set-header"] } diff --git a/crates/vestige-mcp/src/dashboard/handlers.rs b/crates/vestige-mcp/src/dashboard/handlers.rs index 532230a..0d1005a 100644 --- a/crates/vestige-mcp/src/dashboard/handlers.rs +++ b/crates/vestige-mcp/src/dashboard/handlers.rs @@ -829,9 +829,10 @@ pub async fn trigger_consolidation( pub async fn retention_distribution( State(state): State, ) -> Result, StatusCode> { + // Cap at 1000 to prevent excessive memory usage on large databases let nodes = state .storage - .get_all_nodes(10000, 0) + .get_all_nodes(1000, 0) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; // Build distribution buckets diff --git a/crates/vestige-mcp/src/dashboard/mod.rs b/crates/vestige-mcp/src/dashboard/mod.rs index 3c84e21..d236041 100644 --- a/crates/vestige-mcp/src/dashboard/mod.rs +++ b/crates/vestige-mcp/src/dashboard/mod.rs @@ -48,22 +48,23 @@ pub fn build_router_with_event_tx( fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) { - let origins = vec![ + #[allow(unused_mut)] + let mut origins = vec![ format!("http://127.0.0.1:{}", port) .parse::() .expect("valid origin"), format!("http://localhost:{}", port) .parse::() .expect("valid origin"), - // SvelteKit dev server - "http://localhost:5173" - .parse::() - .expect("valid origin"), - "http://127.0.0.1:5173" - .parse::() - .expect("valid origin"), ]; + // SvelteKit dev server — only in debug builds + #[cfg(debug_assertions)] + { + origins.push("http://localhost:5173".parse::().expect("valid origin")); + origins.push("http://127.0.0.1:5173".parse::().expect("valid origin")); + } + let cors = CorsLayer::new() .allow_origin(AllowOrigin::list(origins)) .allow_methods([ @@ -77,11 +78,39 @@ fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) { axum::http::header::AUTHORIZATION, ]); + // Security: restrict WebSocket connections to localhost only (prevents cross-site WS hijacking) + let csp_value = format!( + "default-src 'self'; \ + script-src 'self' 'unsafe-inline'; \ + style-src 'self' 'unsafe-inline'; \ + img-src 'self' blob: data:; \ + connect-src 'self' ws://127.0.0.1:{port} ws://localhost:{port}; \ + font-src 'self' data:; \ + frame-ancestors 'none'; \ + base-uri 'self'; \ + form-action 'self';" + ); let csp = SetResponseHeaderLayer::overriding( axum::http::header::CONTENT_SECURITY_POLICY, - axum::http::HeaderValue::from_static( - "default-src 'self' 'unsafe-inline' 'unsafe-eval' blob: data: ws: wss:", - ), + axum::http::HeaderValue::from_str(&csp_value).expect("valid CSP header"), + ); + + // Additional security headers + let x_frame_options = SetResponseHeaderLayer::overriding( + axum::http::header::X_FRAME_OPTIONS, + axum::http::HeaderValue::from_static("DENY"), + ); + let x_content_type_options = SetResponseHeaderLayer::overriding( + axum::http::header::X_CONTENT_TYPE_OPTIONS, + axum::http::HeaderValue::from_static("nosniff"), + ); + let referrer_policy = SetResponseHeaderLayer::overriding( + axum::http::HeaderName::from_static("referrer-policy"), + axum::http::HeaderValue::from_static("strict-origin-when-cross-origin"), + ); + let permissions_policy = SetResponseHeaderLayer::overriding( + axum::http::HeaderName::from_static("permissions-policy"), + axum::http::HeaderValue::from_static("camera=(), microphone=(), geolocation=()"), ); let router = Router::new() @@ -121,7 +150,11 @@ fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) { ServiceBuilder::new() .concurrency_limit(50) .layer(cors) - .layer(csp), + .layer(csp) + .layer(x_frame_options) + .layer(x_content_type_options) + .layer(referrer_policy) + .layer(permissions_policy), ) .with_state(state.clone()); diff --git a/crates/vestige-mcp/src/dashboard/websocket.rs b/crates/vestige-mcp/src/dashboard/websocket.rs index 6d6af80..88d428c 100644 --- a/crates/vestige-mcp/src/dashboard/websocket.rs +++ b/crates/vestige-mcp/src/dashboard/websocket.rs @@ -5,6 +5,7 @@ use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade}; use axum::extract::State; +use axum::http::{HeaderMap, StatusCode}; use axum::response::IntoResponse; use chrono::Utc; use futures_util::{SinkExt, StreamExt}; @@ -15,11 +16,33 @@ use super::events::VestigeEvent; use super::state::AppState; /// WebSocket upgrade handler — GET /ws +/// Validates Origin header to prevent cross-site WebSocket hijacking. pub async fn ws_handler( + headers: HeaderMap, ws: WebSocketUpgrade, State(state): State, ) -> impl IntoResponse { - ws.on_upgrade(move |socket| handle_socket(socket, state)) + // Validate Origin header (browsers always send it for WebSocket upgrades). + // Non-browser clients (curl, wscat) won't have Origin — allowed since localhost-only. + match headers.get("origin").and_then(|v| v.to_str().ok()) { + Some(origin) => { + let allowed = origin.starts_with("http://127.0.0.1:") + || origin.starts_with("http://localhost:"); + #[cfg(debug_assertions)] + let allowed = allowed || origin == "http://localhost:5173" || origin == "http://127.0.0.1:5173"; + if !allowed { + warn!("Rejected WebSocket connection from origin: {}", origin); + return StatusCode::FORBIDDEN.into_response(); + } + } + None => { + debug!("WebSocket connection without Origin header (non-browser client)"); + } + } + ws.max_frame_size(64 * 1024) + .max_message_size(256 * 1024) + .on_upgrade(move |socket| handle_socket(socket, state)) + .into_response() } async fn handle_socket(socket: WebSocket, state: AppState) { diff --git a/crates/vestige-mcp/src/server.rs b/crates/vestige-mcp/src/server.rs index 17e92d7..95a0b88 100644 --- a/crates/vestige-mcp/src/server.rs +++ b/crates/vestige-mcp/src/server.rs @@ -850,14 +850,14 @@ impl McpServer { match tool_name { // -- smart_ingest: memory created/updated -- "smart_ingest" | "ingest" | "session_checkpoint" => { - // Single mode: result has "action" (created/updated/superseded/reinforced) - if let Some(action) = result.get("action").and_then(|a| a.as_str()) { + // Single mode: result has "decision" (create/update/supersede/reinforce/merge/replace/add_context) + if let Some(decision) = result.get("decision").and_then(|a| a.as_str()) { let id = result.get("nodeId").or(result.get("id")) .and_then(|v| v.as_str()).unwrap_or("").to_string(); let preview = result.get("contentPreview").or(result.get("content")) .and_then(|v| v.as_str()).unwrap_or("").to_string(); - match action { - "created" => { + match decision { + "create" => { let node_type = result.get("nodeType") .and_then(|v| v.as_str()).unwrap_or("fact").to_string(); let tags = result.get("tags") @@ -868,9 +868,9 @@ impl McpServer { id, content_preview: preview, node_type, tags, timestamp: now, }); } - "updated" | "superseded" | "reinforced" => { + "update" | "supersede" | "reinforce" | "merge" | "replace" | "add_context" => { self.emit(VestigeEvent::MemoryUpdated { - id, content_preview: preview, field: action.to_string(), timestamp: now, + id, content_preview: preview, field: decision.to_string(), timestamp: now, }); } _ => {} @@ -879,20 +879,20 @@ impl McpServer { // Batch mode: result has "results" array if let Some(results) = result.get("results").and_then(|r| r.as_array()) { for item in results { - let action = item.get("action").and_then(|a| a.as_str()).unwrap_or(""); + let decision = item.get("decision").and_then(|a| a.as_str()).unwrap_or(""); let id = item.get("nodeId").or(item.get("id")) .and_then(|v| v.as_str()).unwrap_or("").to_string(); let preview = item.get("contentPreview") .and_then(|v| v.as_str()).unwrap_or("").to_string(); - if action == "created" { + if decision == "create" { self.emit(VestigeEvent::MemoryCreated { id, content_preview: preview, node_type: "fact".to_string(), tags: vec![], timestamp: now, }); - } else if !action.is_empty() { + } else if !decision.is_empty() { self.emit(VestigeEvent::MemoryUpdated { id, content_preview: preview, - field: action.to_string(), timestamp: now, + field: decision.to_string(), timestamp: now, }); } } @@ -1000,7 +1000,7 @@ impl McpServer { let preview = args.as_ref() .and_then(|a| a.get("content")) .and_then(|v| v.as_str()) - .map(|s| if s.len() > 100 { format!("{}...", &s[..100]) } else { s.to_string() }) + .map(|s| if s.len() > 100 { format!("{}...", &s[..s.floor_char_boundary(100)]) } else { s.to_string() }) .unwrap_or_default(); let composite = result.get("compositeScore") .or(result.get("composite_score")) diff --git a/crates/vestige-mcp/src/tools/changelog.rs b/crates/vestige-mcp/src/tools/changelog.rs index c098b5d..e98adef 100644 --- a/crates/vestige-mcp/src/tools/changelog.rs +++ b/crates/vestige-mcp/src/tools/changelog.rs @@ -72,10 +72,10 @@ pub async fn execute( if let Some(ref memory_id) = args.memory_id { // Per-memory mode: state transitions for a specific memory - execute_per_memory(&storage, memory_id, limit) + execute_per_memory(storage, memory_id, limit) } else { // System-wide mode: consolidations + recent transitions - execute_system_wide(&storage, limit) + execute_system_wide(storage, limit) } } diff --git a/crates/vestige-mcp/src/tools/codebase_unified.rs b/crates/vestige-mcp/src/tools/codebase_unified.rs index 4a7a846..61c7536 100644 --- a/crates/vestige-mcp/src/tools/codebase_unified.rs +++ b/crates/vestige-mcp/src/tools/codebase_unified.rs @@ -204,7 +204,7 @@ async fn execute_remember_decision( // Build content with structured format (ADR-like) let mut content = format!( "# Decision: {}\n\n## Context\n\n{}\n\n## Decision\n\n{}", - &decision[..decision.len().min(50)], + &decision[..decision.floor_char_boundary(50)], rationale, decision ); diff --git a/crates/vestige-mcp/src/tools/dedup.rs b/crates/vestige-mcp/src/tools/dedup.rs index ea3cacf..c982091 100644 --- a/crates/vestige-mcp/src/tools/dedup.rs +++ b/crates/vestige-mcp/src/tools/dedup.rs @@ -219,7 +219,7 @@ pub async fn execute( .map(|n| { let c = n.content.replace('\n', " "); if c.len() > 120 { - format!("{}...", &c[..120]) + format!("{}...", &c[..c.floor_char_boundary(120)]) } else { c } diff --git a/crates/vestige-mcp/src/tools/ingest.rs b/crates/vestige-mcp/src/tools/ingest.rs index 872185f..402e24f 100644 --- a/crates/vestige-mcp/src/tools/ingest.rs +++ b/crates/vestige-mcp/src/tools/ingest.rs @@ -96,7 +96,7 @@ pub async fn execute( if intent_result.confidence > 0.5 { let intent_tag = format!("intent:{:?}", intent_result.primary_intent); let intent_tag = if intent_tag.len() > 50 { - format!("{}...", &intent_tag[..47]) + format!("{}...", &intent_tag[..intent_tag.floor_char_boundary(47)]) } else { intent_tag }; diff --git a/crates/vestige-mcp/src/tools/intention_unified.rs b/crates/vestige-mcp/src/tools/intention_unified.rs index 0cf6eb1..3a22d93 100644 --- a/crates/vestige-mcp/src/tools/intention_unified.rs +++ b/crates/vestige-mcp/src/tools/intention_unified.rs @@ -297,7 +297,7 @@ async fn execute_set( if intent_result.confidence > 0.5 { let intent_tag = format!("intent:{:?}", intent_result.primary_intent); let intent_tag = if intent_tag.len() > 50 { - format!("{}...", &intent_tag[..47]) + format!("{}...", &intent_tag[..intent_tag.floor_char_boundary(47)]) } else { intent_tag }; diff --git a/crates/vestige-mcp/src/tools/maintenance.rs b/crates/vestige-mcp/src/tools/maintenance.rs index 9b5926e..9764dce 100644 --- a/crates/vestige-mcp/src/tools/maintenance.rs +++ b/crates/vestige-mcp/src/tools/maintenance.rs @@ -249,7 +249,7 @@ pub async fn execute_system_status( let last_dream = storage.get_last_dream().ok().flatten(); let saves_since_last_dream = match &last_dream { Some(dt) => storage.count_memories_since(*dt).unwrap_or(0), - None => stats.total_nodes as i64, + None => stats.total_nodes, }; let last_backup = Storage::get_last_backup_timestamp(); diff --git a/crates/vestige-mcp/src/tools/mod.rs b/crates/vestige-mcp/src/tools/mod.rs index c2251d9..99357cc 100644 --- a/crates/vestige-mcp/src/tools/mod.rs +++ b/crates/vestige-mcp/src/tools/mod.rs @@ -37,8 +37,10 @@ pub mod session_context; pub mod health; pub mod graph; -// Deprecated tools - kept for internal backwards compatibility -// These modules are intentionally unused in the public API +// Deprecated/internal tools — not advertised in the public MCP tools/list, +// but some functions are actively dispatched for backwards compatibility +// and internal cognitive operations. #[allow(dead_code)] suppresses warnings +// for the unused schema/struct items within these modules. #[allow(dead_code)] pub mod checkpoint; #[allow(dead_code)] diff --git a/crates/vestige-mcp/src/tools/session_context.rs b/crates/vestige-mcp/src/tools/session_context.rs index 623c8a4..ada68aa 100644 --- a/crates/vestige-mcp/src/tools/session_context.rs +++ b/crates/vestige-mcp/src/tools/session_context.rs @@ -218,7 +218,7 @@ pub async fn execute( let last_dream = storage.get_last_dream().ok().flatten(); let saves_since_last_dream = match &last_dream { Some(dt) => storage.count_memories_since(*dt).unwrap_or(0), - None => stats.total_nodes as i64, + None => stats.total_nodes, }; let last_backup = Storage::get_last_backup_timestamp(); let now = Utc::now(); @@ -333,8 +333,8 @@ pub async fn execute( // ==================================================================== // 5. Codebase patterns/decisions (if codebase specified) // ==================================================================== - if let Some(ref ctx) = args.context { - if let Some(ref codebase) = ctx.codebase { + if let Some(ref ctx) = args.context + && let Some(ref codebase) = ctx.codebase { let codebase_tag = format!("codebase:{}", codebase); let mut cb_lines: Vec = Vec::new(); @@ -368,7 +368,6 @@ pub async fn execute( context_parts.push(format!("**Codebase ({}):**\n{}", codebase, cb_lines.join("\n"))); } } - } // ==================================================================== // 6. Assemble final response @@ -404,11 +403,10 @@ fn check_intention_triggered( match trigger.trigger_type.as_deref() { Some("time") => { - if let Some(ref at) = trigger.at { - if let Ok(trigger_time) = DateTime::parse_from_rfc3339(at) { + if let Some(ref at) = trigger.at + && let Ok(trigger_time) = DateTime::parse_from_rfc3339(at) { return trigger_time.with_timezone(&Utc) <= now; } - } if let Some(mins) = trigger.in_minutes { let trigger_time = intention.created_at + Duration::minutes(mins); return trigger_time <= now; @@ -418,29 +416,25 @@ fn check_intention_triggered( Some("context") => { // Check codebase match if let (Some(trigger_cb), Some(current_cb)) = (&trigger.codebase, &ctx.codebase) - { - if current_cb + && current_cb .to_lowercase() .contains(&trigger_cb.to_lowercase()) { return true; } - } // Check file pattern match - if let (Some(pattern), Some(file)) = (&trigger.file_pattern, &ctx.file) { - if file.contains(pattern.as_str()) { + if let (Some(pattern), Some(file)) = (&trigger.file_pattern, &ctx.file) + && file.contains(pattern.as_str()) { return true; } - } // Check topic match - if let (Some(topic), Some(topics)) = (&trigger.topic, &ctx.topics) { - if topics + if let (Some(topic), Some(topics)) = (&trigger.topic, &ctx.topics) + && topics .iter() .any(|t| t.to_lowercase().contains(&topic.to_lowercase())) { return true; } - } false } _ => false, diff --git a/crates/vestige-mcp/src/tools/smart_ingest.rs b/crates/vestige-mcp/src/tools/smart_ingest.rs index 9521f24..900c99f 100644 --- a/crates/vestige-mcp/src/tools/smart_ingest.rs +++ b/crates/vestige-mcp/src/tools/smart_ingest.rs @@ -165,7 +165,7 @@ pub async fn execute( let intent_tag = format!("intent:{:?}", intent_result.primary_intent); // Truncate long intent tags let intent_tag = if intent_tag.len() > 50 { - format!("{}...", &intent_tag[..47]) + format!("{}...", &intent_tag[..intent_tag.floor_char_boundary(47)]) } else { intent_tag }; @@ -338,7 +338,7 @@ async fn execute_batch( if intent_result.confidence > 0.5 { let intent_tag = format!("intent:{:?}", intent_result.primary_intent); let intent_tag = if intent_tag.len() > 50 { - format!("{}...", &intent_tag[..47]) + format!("{}...", &intent_tag[..intent_tag.floor_char_boundary(47)]) } else { intent_tag }; diff --git a/docs/CLAUDE-SETUP.md b/docs/CLAUDE-SETUP.md index 6d0d15b..6d00538 100644 --- a/docs/CLAUDE-SETUP.md +++ b/docs/CLAUDE-SETUP.md @@ -71,7 +71,7 @@ Use `codebase` → `remember_pattern`: | "Don't forget" | `smart_ingest` with high priority | | "I always..." / "I never..." | Save as preference | | "I prefer..." / "I like..." | Save as preference | -| "This is important" | `smart_ingest` + `promote_memory` | +| "This is important" | `smart_ingest` + `memory(action="promote")` | | "Remind me..." | Create `intention` | | "Next time..." | Create `intention` with context trigger | @@ -148,11 +148,11 @@ smart_ingest( At the end of significant conversations: 1. Reflect: "Did anything change about how I understand myself?" 2. If yes, update identity memories with `smart_ingest` -3. Prune outdated self-concepts with `demote_memory` +3. Prune outdated self-concepts with `memory(action="demote")` ### Memory Hygiene -- Use `promote_memory` when a memory proves valuable -- Use `demote_memory` when a memory led you astray +- Use `memory(action="promote")` when a memory proves valuable +- Use `memory(action="demote")` when a memory led you astray ``` --- @@ -199,7 +199,7 @@ You have persistent memory via Vestige. Use it intelligently: - Notice a pattern? `codebase(action="remember_pattern")` - Made a decision? `codebase(action="remember_decision")` with rationale - I mention a preference? `smart_ingest` it -- Something important? `importance()` to strengthen recent memories +- Something important? `importance_score` to check if worth saving - Need to follow up? `intention(action="set")` ### Session End @@ -208,8 +208,8 @@ You have persistent memory via Vestige. Use it intelligently: - Anything change about our working relationship? Update identity memories ### Memory Hygiene -- When a memory helps: `promote_memory` -- When a memory misleads: `demote_memory` +- When a memory helps: `memory(action="promote")` +- When a memory misleads: `memory(action="demote")` - Weekly: `vestige health` to check system status ``` diff --git a/package.json b/package.json index bdd7fe1..8c66e67 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "vestige", - "version": "1.6.0", + "version": "2.0.1", "private": true, "description": "Cognitive memory for AI - MCP server with FSRS-6 spaced repetition", "author": "Sam Valladares", - "license": "MIT OR Apache-2.0", + "license": "AGPL-3.0-only", "repository": { "type": "git", "url": "https://github.com/samvallad33/vestige" diff --git a/packages/vestige-init/package.json b/packages/vestige-init/package.json index a3f4b2e..a7a8cee 100644 --- a/packages/vestige-init/package.json +++ b/packages/vestige-init/package.json @@ -1,6 +1,6 @@ { "name": "@vestige/init", - "version": "2.0.0", + "version": "2.0.1", "description": "Give your AI a brain in 10 seconds — zero-config Vestige v2.0 installer with 3D dashboard", "bin": { "vestige-init": "bin/init.js" diff --git a/packages/vestige-mcp-npm/README.md b/packages/vestige-mcp-npm/README.md index 2523cd4..f0d6ee7 100644 --- a/packages/vestige-mcp-npm/README.md +++ b/packages/vestige-mcp-npm/README.md @@ -1,4 +1,4 @@ -# @vestige/mcp +# vestige-mcp-server Vestige MCP Server - A synthetic hippocampus for AI assistants. @@ -120,10 +120,10 @@ Fix the MCP connection first, then the model will download automatically. | Platform | Architecture | |----------|--------------| -| macOS | ARM64 (Apple Silicon) | +| macOS | ARM64 (Apple Silicon), x86_64 (Intel) | | Linux | x86_64 | | Windows | x86_64 | ## License -MIT +AGPL-3.0-only diff --git a/packages/vestige-mcp-npm/bin/vestige-mcp.js b/packages/vestige-mcp-npm/bin/vestige-mcp.js index 9165dad..6135e77 100755 --- a/packages/vestige-mcp-npm/bin/vestige-mcp.js +++ b/packages/vestige-mcp-npm/bin/vestige-mcp.js @@ -13,7 +13,7 @@ if (!fs.existsSync(binaryPath)) { console.error('Error: vestige-mcp binary not found.'); console.error(`Expected at: ${binaryPath}`); console.error(''); - console.error('Try reinstalling: npm install -g @vestige/mcp'); + console.error('Try reinstalling: npm install -g vestige-mcp-server'); process.exit(1); } diff --git a/packages/vestige-mcp-npm/bin/vestige.js b/packages/vestige-mcp-npm/bin/vestige.js index 443f4a0..dbf02d7 100755 --- a/packages/vestige-mcp-npm/bin/vestige.js +++ b/packages/vestige-mcp-npm/bin/vestige.js @@ -13,7 +13,7 @@ if (!fs.existsSync(binaryPath)) { console.error('Error: vestige CLI binary not found.'); console.error(`Expected at: ${binaryPath}`); console.error(''); - console.error('Try reinstalling: npm install -g @vestige/mcp'); + console.error('Try reinstalling: npm install -g vestige-mcp-server'); process.exit(1); } diff --git a/packages/vestige-mcp-npm/package.json b/packages/vestige-mcp-npm/package.json index 09e77e7..e50754d 100644 --- a/packages/vestige-mcp-npm/package.json +++ b/packages/vestige-mcp-npm/package.json @@ -1,6 +1,6 @@ { "name": "vestige-mcp-server", - "version": "2.0.0", + "version": "2.0.1", "description": "Vestige MCP Server — Cognitive memory for AI with FSRS-6, 3D dashboard, and 29 brain modules", "bin": { "vestige-mcp": "bin/vestige-mcp.js", diff --git a/packages/vestige-mcp-npm/scripts/postinstall.js b/packages/vestige-mcp-npm/scripts/postinstall.js index cd5302a..c6e11ec 100644 --- a/packages/vestige-mcp-npm/scripts/postinstall.js +++ b/packages/vestige-mcp-npm/scripts/postinstall.js @@ -7,7 +7,7 @@ const os = require('os'); const { execSync } = require('child_process'); const VERSION = require('../package.json').version; -const BINARY_VERSION = '1.1.3'; // GitHub release version for binaries +const BINARY_VERSION = '2.0.1'; // GitHub release version for binaries const PLATFORM = os.platform(); const ARCH = os.arch(); diff --git a/tests/e2e/Cargo.toml b/tests/e2e/Cargo.toml index 8459c17..c8afd65 100644 --- a/tests/e2e/Cargo.toml +++ b/tests/e2e/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" publish = false [dependencies] -vestige-core = { path = "../../crates/vestige-core", features = ["full"] } +vestige-core = { path = "../../crates/vestige-core", features = ["embeddings", "vector-search"] } chrono = { version = "0.4", features = ["serde"] } uuid = { version = "1", features = ["v4", "serde"] } tempfile = "3"