diff --git a/CHANGELOG.md b/CHANGELOG.md index 42d81408..51245723 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,513 +1,127 @@ # Changelog -All notable changes to MeMesh will be documented in this file. +All notable changes to MeMesh are documented here. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [4.0.0] — 2026-04-20 -## [Unreleased] - -## [2.11.0] - 2026-03-09 - -### Added - -- **`memesh-view` CLI command** — generates self-contained HTML dashboard with D3.js force-directed knowledge graph, searchable entity table, and statistics - -### Breaking Changes - -- **Minimal core rewrite** — stripped from 50+ source files to 5, 26 dependencies to 3 -- **3 MCP tools only**: `remember`, `recall`, `forget` (removed buddy-do, buddy-help, memesh-hook-tool-use, memesh-generate-tests, memesh-metrics) -- **2 hooks only**: session-start, post-commit (removed pre-tool-use, post-tool-use, stop, subagent-stop) - -### Removed - -- Vector search (ONNX embeddings, sqlite-vec, EmbeddingService) -- Daemon/proxy server modes (standalone only) -- CLI features (commander, inquirer, interactive prompts) -- HTTP server (express) -- All UI formatting (chalk, boxen, ora, cli-spinners, cli-table3, asciichart) -- Logging framework (winston) -- 23 production dependencies - -### Fixed - -- vitest pool from `threads` to `forks` to prevent SIGSEGV with better-sqlite3 native module - -### Architecture - -- **Database**: Direct better-sqlite3 with FTS5 full-text search -- **Server**: Standalone MCP via StdioServerTransport -- **Validation**: Zod schemas for all tool inputs -- **Backward compatible**: Existing DB data (entities, observations, relations, tags) preserved and queryable - -## [2.10.2] - 2026-03-09 - -### Changed - -- Clean up changelog entries - -## [2.10.1] - 2026-03-09 - -### Fixed - -- **health-check**: Support npm global install and plugin marketplace install modes (previously only worked from dev directory) - -### Changed - -- Streamline repository for professional open source standards -- Simplify documentation and build configuration - -## [2.10.0] - 2026-03-08 - -### Added - -- **Streamlit Visual Explorer** — interactive web UI for exploring the knowledge graph - - **Dashboard page**: entity/relation/observation/tag counts, entity type distribution (pie chart), top tags (bar chart), entity growth over time (line chart), recent entities table - - **KG Explorer page**: interactive graph visualization with color-coded entity types and relation edges, FTS5 full-text search, filtering by type/tags/date range, adjustable node count - - Uses `streamlit-agraph` (vis.js) for graph rendering with physics-based layout - - Automatic database path resolution (`~/.memesh/` and legacy `~/.claude-code-buddy/`) -- **Auto-relation inference** — entities created via `create-entities` now automatically infer relations - - Intra-batch relations between newly created entities sharing topic keywords - - New-to-existing relations linking new entities with relevant existing ones - - Semantic relation types: `similar_to`, `solves`, `caused_by`, `enabled_by`, `depends_on`, `follows_pattern` -- **Relation backfill script** (`streamlit/backfill_relations.py`) — 3-layer strategy for existing entities - - Layer 1: Topic/project clustering via name prefix matching - - Layer 2: Cross-type semantic relations within topic clusters - - Layer 3: Tag-based similarity (entities sharing 2+ tags) - - Supports `--dry-run` and `--db-path` flags, idempotent via INSERT OR IGNORE -- **KG-style SVG logo** — replaced emoji badge with clean vector knowledge graph icon - -### Fixed - -- FTS5 false unavailability on Streamlit hot-reload (removed stale module-level cache) -- JSON injection in backfill relation metadata (f-string → `json.dumps`) -- Keyword false positives in auto-relation inference (minimum 4-char filter) -- Removed dead code (`insert_relations`, unused `KNOWN_PREFIXES` constant) - -## [2.9.3] - 2026-03-08 - -### Added - -- **ProactiveRecaller** — automatically surfaces relevant memories based on context triggers - - `session-start`: injects top 5 memories (>0.5 similarity) into hook output using project name + recent commits - - `test-failure`: recalls top 3 memories (>0.6 similarity) matching test name + error message - - `error-detection`: recalls top 3 memories (>0.6 similarity) matching error type + first line - - Session-start: integrated into `scripts/hooks/session-start.js` via FTS5 query - - Test/error: integrated via `post-tool-use.js` → `proactive-recall.json` → `HookToolHandler` -- **ContentHasher** — SHA-256 hash-based embedding deduplication - - Skips ONNX inference when entity content (name + observations) unchanged - - `embedding_hashes` side table (vec0 virtual tables don't support ALTER TABLE) - - Cleaned up on entity deletion -- **Batch embedding** — `createEntitiesBatch()` uses `encodeBatch()` instead of N individual `encode()` calls - - Combined with hash dedup for maximum efficiency - -### Fixed - -- Update all repository URLs from `claude-code-buddy` to `memesh-plugin` -- Sync `marketplace.json` version to match package version -- Update dependencies to resolve security vulnerabilities (express-rate-limit, ip-address) - -## [2.9.2] - 2026-03-08 - -### Architecture Refactoring - -- **VectorSearchAdapter** (Strategy pattern) — decouples vector search from sqlite-vec - - `SqliteVecAdapter`: sqlite-vec implementation (pinned to v0.1.3 stable) - - `InMemoryVectorAdapter`: pure TypeScript implementation for testing without native deps - - KnowledgeGraph now uses injected adapter instance instead of static VectorExtension -- **ToolHandlers decomposition** — split 922-line monolith into 3 focused sub-handlers - - `MemoryToolHandler`: entity/relation/recall/mistake operations - - `SystemToolHandler`: skills, uninstall, test generation - - `HookToolHandler`: hook tool-use tracking - - ToolHandlers.ts reduced to ~130-line facade -- **KGSearchEngine** — extracted FTS5, semantic, and hybrid search from KnowledgeGraph -- **MemorySearchEngine** — extracted search/filter/rank/dedup from UnifiedMemoryStore -- **GitCommandParser** — extracted git command detection from HookIntegration -- **StdinBufferManager** — extracted stdin buffering from server-bootstrap.ts - -### Performance - -- **Embedding LRU cache** — 500-entry cache eliminates redundant ONNX inference -- **Batch transactions** — `createEntitiesBatch()` wraps N inserts in single transaction -- **ONNX preloading** — daemon mode preloads model in background (eliminates 10-20s cold start) -- **encodeBatch parallelization** — texts encoded in parallel chunks of 10 - -### Fixed - -- Global `unhandledRejection` and `uncaughtException` handlers in server-bootstrap -- Embedding failure log level upgraded from DEBUG to WARN -- Socket cleanup in `StdioProxyClient.connectToDaemon()` error path -- Duplicate `CONTROL_CHAR_PATTERN` constant unified -- Duplicate `uncaughtException` handler prevented in daemon mode -- StdinBufferManager catch block now logs errors instead of silent swallow - -### Removed - -- 6 unused npm dependencies (ink, ink-spinner, multer, cors, form-data, log-update) -- 4 dead utility modules (money.ts, toonify-adapter.ts, toonify.ts, tracing/middleware.ts) -- Unused AgentRegistry injection from ToolHandlers - -### Documentation - -- Added project CLAUDE.md with mandatory documentation-code synchronization rules -- Updated ARCHITECTURE.md to reflect all architectural changes -- Fixed embedding model references (bge-small-en-v1.5 → all-MiniLM-L6-v2) - -## [2.9.1] - 2026-03-01 - -### Security - -- Updated plugin dependencies to resolve Dependabot alerts -- Removed orphaned dist files and scripts from previously removed features - -## [2.9.0] - 2026-02-25 +MeMesh transforms from memory database to **cognitive middleware** — memory that auto-injects, auto-captures, auto-cleans, and auto-improves. ### Added - -- **Claude Code Hooks System** - 7 hooks for session lifecycle automation - - `pre-tool-use`: Smart routing (model selection per task type), planning enforcement (SDD+BDD template), dry-run gate (untested code warning), code review reminder - - `post-tool-use`: Pattern detection (EDIT_WITHOUT_READ, GIT_WORKFLOW, FRONTEND_WORK), anomaly detection, session state tracking - - `post-commit`: Saves commit metadata to knowledge graph - - `subagent-stop`: Captures code review findings to knowledge graph - - `session-start`: Cache-first memory recall with SQLite fallback, CLAUDE.md reload - - `stop`: Session archiving, key point extraction, pattern analysis - - `hook-utils`: Shared utilities (sqliteQuery, sqliteBatch, sqliteBatchEntity, readStdin) - - 15 automated tests via custom test harness -- **`memesh-metrics` MCP Tool** - Exposes session, routing, and memory metrics - - Session: modified files, tested files, code review status - - Routing: active model rules, planning enforcement, audit log - - Memory: knowledge graph size and status -- **Semantic Search** - KnowledgeGraph now supports vector similarity search via sqlite-vec - - `semanticSearch()`: Vector search with FTS5 keyword fallback - - `hybridSearch()`: Combines keyword and vector results, deduplicates by entity name - - Async embedding generation via `Xenova/all-MiniLM-L6-v2` - - Keyword fallback uses 0.5 similarity (not 1.0) for honest scoring -- **Smart Task Analysis in `buddy-do`** - Detects task type (bug-fix, feature, refactor, test, performance, security), extracts metadata (goal, reason, expected outcome), queries related context from knowledge graph -- **Bundled Skill: `comprehensive-code-review`** - Included in `scripts/skills/` -- **Cloud-Only Fallback Mode** - MCP server can now run without local SQLite when better-sqlite3 is unavailable but MEMESH_API_KEY is configured (#73, #74) - - Three-mode architecture: Standard (SQLite) / Cloud-Only (API key) / Error (neither) - - Graceful degradation for Claude Desktop Cowork environments - - Added 13 comprehensive tests for cloud-only mode (4 unit + 9 integration) -- **Claude Desktop Cowork Documentation** - Guide at `docs/COWORK_SUPPORT.md` (#75) -- **BuddyHandlers Test Coverage** - 9 unit tests covering all buddy commands (#19) -- **Enhanced .gitignore** - Multi-platform patterns for macOS, Windows, Linux, IDEs (#21) +- **Recall Effectiveness Tracking** — `recall_hits`/`recall_misses` columns track whether injected memories are actually used by the AI. Session-start hook records injected entity IDs; Stop hook checks transcript for usage and updates hit/miss counts. `/v1/analytics` returns overall hit rate, top effective, and most ignored memories. +- **Continuous Recall (PreToolUse hook)** — New `pre-edit-recall.js` hook triggers on Edit/Write. Queries MeMesh for entities matching the file being edited (tag-based + FTS5 search). Throttled to max 1 recall per file per session. 5 hooks total now. +- **BYOK Embedding** — Multi-provider embedding support: OpenAI `text-embedding-3-small` (1536-dim), Ollama embedding models (768-dim), ONNX fallback (384-dim). Anthropic has no embedding API — correctly falls back to ONNX. Auto dimension migration: stores dim in metadata, drops/recreates `entities_vec` on provider change. +- **Auto-Tagging with LLM** — When `remember()` is called without tags and LLM is configured, generates 2-5 tags (project:, topic:, tech:, severity:, scope:) via LLM. Fire-and-forget: doesn't block the sync remember call. +- **Noise Filter** — `compressWeeklyNoise()` groups auto-tracked entities (session_keypoint, commit, session-insight) older than 7 days by ISO week, creates weekly summary entities, archives originals. Threshold: 20+ per week. Never touches decisions, patterns, lessons, or intentional knowledge. Throttled to once per 24h. +- **Memory Impact Score** — Laplace-smoothed `(recall_hits+1)/(recall_hits+recall_misses+2)` as 6th scoring factor (10% weight). Entities with high recall effectiveness rise in search results; ignored entities fade. +- **RecallEffectiveness dashboard component** — Stats row (effectiveness %, hits, misses, tracked) + bar charts for top/bottom entities. i18n across all 11 locales. +- Skills rewritten to CLI-first with hooks documentation and auto-detect flow (MCP → CLI → npx fallback) ### Changed +- Scoring weights rebalanced: searchRelevance 0.30 (was 0.35), frequency 0.15 (was 0.20), new impact 0.10 +- `Capabilities.embeddings` correctly reports `onnx` when provider is Anthropic (was incorrectly reporting `anthropic`) +- Circular dependency between db.ts and embedder.ts resolved — `getEmbeddingDimension()` moved to config.ts +- 445 tests across 29 test files (up from 408/26) +- 5 hooks (up from 4): added PreToolUse for continuous recall +- Dashboard: 124KB (up from 107KB, new RecallEffectiveness component + i18n) -- **Server Bootstrap** - Structured error handling with error classification (permission, disk, socket) and actionable recovery hints -- **StdioProxyClient** - Buffer overflow handling, proper listener cleanup on connection failure -- **Dependencies Update** - Updated 15/17 packages to latest versions (#68) - - @types/node, @typescript-eslint/*, ajv, dotenv, glob, ink, inquirer, onnxruntime-node, typedoc, and more - - Note: eslint 10.0.0 blocked (typescript-eslint incompatibility) - -### Fixed - -- **`getEntity()` Exact Match** - Now uses direct SQL `WHERE name = ?` instead of FTS5 fuzzy search that could return wrong entity -- **`post-commit` Hook Exit Code** - Changed from `exit(1)` to `exit(0)` to never block Claude Code on hook errors -- **Entity Name Collision** - `subagent-stop` entity names now include timestamp to prevent UNIQUE constraint failures on multiple reviews per day -- **`sqliteQuery` Error Handling** - Returns `null` on error instead of empty string, allowing callers to distinguish errors from empty results -- **`sqliteBatch` Orphan Prevention** - Propagates errors; `sqliteBatchEntity` cleans up orphaned entities when batch fails -- **Error Logging** - `initVectorSearch` and `deleteEntity` catch blocks now log error context instead of silently discarding -- **Metrics Tool** - Uses PathResolver for DB path (supports custom data directories), `os.homedir()` fallback, bounded audit log read (256KB) -- **`UnifiedMemoryStore.update()` Race Condition** - Returns `false` on concurrent delete instead of creating phantom data -- **Session File Growth** - `post-tool-use` toolCalls array capped at 1000 entries -- **Windows Compatibility** - `pre-tool-use` uses `fileURLToPath()` instead of `URL.pathname` -- **`readStdin` Performance** - Fast-path for already-closed stdin avoids 3-second timeout -- **Hybrid Search Scoring** - Keyword fallback uses `similarity: 0.5` instead of `1.0` for honest scoring in merge -- **Cloud-Only Mode Error Handling** - Fixed `handleHookToolUse` to check for cloud-only mode before accessing memory systems - -### Technical - -- **Server Architecture** - Modified `ServerInitializer` to support three initialization modes -- **Type Safety** - Changed all optional memory systems from `null` to `undefined` for TypeScript consistency -- **Test Coverage** - 1817 unit tests + 15 hook tests (100% passing) -- **Code Quality** - Three rounds of comprehensive code review, all CRITICAL and MAJOR issues resolved -- **Refactored `subagent-stop`** - Uses `sqliteBatchEntity` (3 spawns) instead of individual `sqliteQuery` calls (N+2 spawns) - -**Issues Resolved**: #73 (cloud-only mode), #74 (Phase 2), #75 (docs), #68 (deps), #19 (tests), #21 (.gitignore) - -## [2.8.10] - 2026-02-14 - -### Documentation - -- Added comprehensive development guide at `docs/DEVELOPMENT.md` covering prerequisites, setup, development workflow, testing strategy, MCP server debugging, common tasks, troubleshooting, and best practices -- Added "Real-World Examples" section to `docs/USER_GUIDE.md` with three multi-day project scenarios demonstrating cross-session memory and context preservation -- Set up TypeDoc for auto-generated API documentation with GitHub Actions deployment to GitHub Pages -- Added `typedoc.json` configuration to generate API docs to `api-docs/` directory -- Created `.github/workflows/deploy-docs.yml` for automatic API documentation deployment -- Updated `README.md` with links to new Development Guide and API Reference -- Updated `.gitignore` to exclude auto-generated `api-docs/` directory - -### Fixed - -- **Project Memory Isolation**: Fixed `buddy-remember` to isolate memories by project, preventing cross-project memory mixing - - Added `allProjects` parameter to `buddy-remember` tool (default: `false`, searches only current project + global memories) - - Modified `ProjectMemoryManager.search()` to filter by `scope:project` and `scope:global` tags - - Updated `keywordSearch()`, `semanticSearch()`, and `hybridSearch()` to support project filtering - - Memories are now tagged with `scope:project` (via `AutoTagger`) when stored with `projectPath` context - - Use `buddy-remember "query" allProjects=true` to search across all projects when needed - -**Issues Resolved**: #70, #69, #17 - -## [2.8.9] - 2026-02-12 - -### Documentation - -- Fixed outdated version numbers across all docs (2.8.0/2.8.3 → 2.8.8) -- Replaced remaining "smart routing" and "intelligent task routing" references with accurate descriptions -- Fixed MCP config path in ARCHITECTURE.md (`~/.config/claude/` → `~/.claude/mcp_settings.json`) -- Prioritized `npm install -g @pcircle/memesh` as recommended installation method in all guides -- Updated repo metadata (GitHub description, topics, keywords) -- Fixed outdated paths, dead links, and wrong package names across docs -- Rebuilt CHANGELOG with complete v2.0.0–v2.8.8 history - -### Fixed - -- Fixed `release.sh` `head -n -1` incompatibility on macOS - -## [2.8.8] - 2026-02-12 - -### Documentation - -- Rewrote README with user-first design — reorganized around user journey (Install → Verify → Use → Troubleshoot) -- Added prerequisites section, inline troubleshooting, removed jargon -- Removed vibe coder branding, improved issue reporting links - -### Fixed - -- Resolved remaining GitHub code scanning alerts -- Removed unused imports - -## [2.8.7] - 2026-02-12 - -### Fixed - -- Resolved all 18 GitHub code scanning alerts (insecure temp files, TOCTOU races, unused code) -- Removed unused `afterEach` import in login.test.ts - -### Repository - -- Dismissed 3 medium alerts as intentional (cloud sync, credential storage) -- Resolved 2 secret scanning alerts (test dummy values in deleted files) -- Cleaned up 3 stale branches (develop, feature/memesh-login, fix/device-auth-tier1-security) - -## [2.8.6] - 2026-02-12 - -### Fixed - -- **Hooks DB Path** - Resolved hooks silently failing due to hardcoded legacy path - - Hooks now use PathResolver logic: checks `~/.memesh/` first, falls back to `~/.claude-code-buddy/` - - Session-start, post-tool-use, and stop hooks now correctly access the active knowledge graph -- **MCP Connection** - Fixed invalid marketplace source type preventing Claude Code from connecting - - Changed source type from invalid `'local'` to correct `'directory'` in all installation scripts - - Updated TypeScript type definition to include all valid source types - -### Security - -- Resolved GitHub code scanning alerts (insecure temp files, TOCTOU race conditions, unused code) - -## [2.8.5] - 2026-02-12 - -### Fixed - -- **Plugin Installation via npm install** - Complete installation flow with backward compatibility - - Fixed marketplace registration not happening during npm install (only happened during build) - - Users installing via `npm install -g @pcircle/memesh` now get a fully functional plugin - - Auto-detects install mode (global vs local dev) - - Auto-repairs legacy v2.8.4/v2.8.3 installations on first run - - Comprehensive plugin configuration: - - Registers marketplace in `~/.claude/plugins/known_marketplaces.json` - - Creates symlink to `~/.claude/plugins/marketplaces/pcircle-ai` - - Enables plugin in `~/.claude/settings.json` - - Configures MCP server in `~/.claude/mcp_settings.json` - - Fixed pre-deployment check treating plugin as standalone MCP server - -### Technical - -- Implemented TDD with 20 tests (10 unit + 9 integration, 100% passing) -- Created `scripts/postinstall-lib.ts` with core installation functions -- Fixed ESM compatibility issues (replaced `__dirname` with proper ESM patterns) - -## [2.8.4] - 2026-02-10 - -### Added - -- **Device Auth Login** - `memesh login` / `memesh logout` CLI commands with device auth flow -- Secure stdin input for manual API key entry - -## [2.8.3] - 2026-02-09 - -### Fixed - -- **Version Reporting** - MCP server now correctly reports version from package.json instead of hardcoded "2.6.6" - - Replaced import assertion syntax with `fs.readFileSync` for cross-environment compatibility - -## [2.8.2] - 2026-02-08 +## [3.2.1] — 2026-04-19 ### Added - -- **WCAG AA Compliance** - Color contrast verification following WCAG 2.1 Level AA -- **Screen Reader Support** - JSON event emission via `MEMESH_SCREEN_READER=1` environment variable -- Accessibility documentation at `docs/ACCESSIBILITY.md` -- Contrast verification script: `npm run verify:contrast` - -## [2.8.1] - 2026-02-08 +- **Precision Engineer Design System** — Satoshi + Geist Mono fonts, cyan accent `#00D6B4`, compact 4px spacing, `DESIGN.md` as single source of truth +- **Analytics Insights Dashboard** — Memory Health Score (0-100) with 4 weighted factors, 30-day memory timeline (canvas sparkline), value metrics (recalls, lessons learned/applied), knowledge coverage with percentage bars, cleanup suggestions with one-click archive +- **Interactive Knowledge Graph** — type filter checkboxes, search with highlight and auto-center, ego graph mode, recency heatmap, orphan detection, physics cooling +- **Feedback Widget** — bug/feature/question selector with system info toggle, pre-fills GitHub issues +- New `GET /v1/analytics` backend endpoint +- i18n: ~50 new keys across all 11 locales ### Fixed - -- **Build Artifacts Cleanup** - Removed legacy secret-types files from build output - - Cleaned up `dist/memory/types/secret-types.*` files deprecated in v2.8.0 - - No functional changes - purely build artifact cleanup - -## [2.8.0] - 2026-02-08 - -### ⚠️ Breaking Changes - -- **MCP Tool Naming Unification** - All non-core tools now use `memesh-*` prefix - - `buddy-record-mistake` → `memesh-record-mistake` - - `create-entities` → `memesh-create-entities` - - `hook-tool-use` → `memesh-hook-tool-use` - - `generate-tests` → `memesh-generate-tests` - - **Migration**: Old tool names were removed in v2.11.0. - -### Added - -- **Vector Semantic Search** - Find memories by meaning, not just keywords - - `buddy-remember` supports `mode`: `semantic`, `keyword`, `hybrid` (default) - - `minSimilarity` parameter for quality filtering (0-1 threshold) - - Uses all-MiniLM-L6-v2 ONNX model (384 dimensions, runs 100% locally) - - Backfill script for existing entities: `npm run backfill-embeddings` -- **Alias System** - Backward compatibility for renamed tools with deprecation warnings - -### Removed - -- **A2A Local Collaboration** - Simplified to local-first architecture - - Removed 35 A2A-related files (daemon, socket server, distributed task queue) - - Focus on single-agent local memory management - -### Changed - -- **Tool Count**: 18 → **8 tools** (3 buddy commands + 4 memesh tools + 1 cloud sync) - -### Technical - -- New `src/embeddings/` module with ModelManager, EmbeddingService, VectorExtension -- Added sqlite-vec, onnxruntime-node, @xenova/transformers dependencies -- ToolRouter with alias resolution and deprecation warnings - -## [2.7.0] - 2026-02-04 - -### Added -- Daemon socket cleanup on exit/crash - prevents stale socket issues -- Exception handlers (uncaughtException, unhandledRejection) for graceful daemon shutdown +- SQLite datetime comparison fix (proper `datetime()` function instead of text comparison) ### Changed -- **Memory retention periods**: Session 7→30 days, Project 30→90 days -- Auto-memory hooks improvements - -### Fixed -- Stale daemon socket causing MCP connection failures in new sessions - -## [2.6.6] - 2026-02-03 +- Zero `as any` type casts in dashboard +- 408 tests passing across 26 test files -### Fixed -- GitHub Actions npm publish workflow - replaced invalid GitHub API method with logging - -## [2.6.5] - 2026-02-03 +## [3.2.0] — 2026-04-18 ### Added -- Enhanced post-install messaging with quick-start guide -- Unified getting-started guide (docs/GETTING_STARTED.md) -- Comprehensive PathResolver tests (47 tests, 100% coverage) -- Professional error formatting with category badges +- **Neural Embeddings** — Xenova/all-MiniLM-L6-v2 (384-dim, ~30MB, local, no API key needed) +- **Hybrid search** — FTS5 keyword + vector similarity, merged and re-ranked +- Fire-and-forget async embedding on `remember()` — zero latency impact +- Graceful fallback to FTS5 when @xenova/transformers unavailable +- **Dashboard 2.0** — 7 tabs (up from 5): new Graph tab (canvas force-directed, no D3) and Lessons tab (structured lesson cards with severity colors) ### Fixed -- **Critical**: Fixed 4 hardcoded `~/.claude-code-buddy/` paths to use PathResolver -- Fixed 14 failing errorHandler tests to match new API structure - -## [2.6.0] - 2026-01-31 +- **Overwrite import** — now actually replaces old observations (was appending due to reactivation bug) +- **Namespace export** — filter applied at SQL query level (was post-filtering after LIMIT, causing truncation) ### Changed -- Code quality improvements -- Documentation updates - -## [2.5.3] - 2026-01-31 - -### Fixed -- Bug fixes and stability improvements - -## [2.5.2] - 2026-01-31 - -### Fixed -- Bug fixes and stability improvements - -## [2.5.1] - 2026-01-31 +- 402 tests across 25 test files +- 14 core modules (+ embedder.ts) +- 76KB dashboard single-file HTML +- 1 `as any` remaining (down from 20 in v3.1.0) -### Fixed -- Bug fixes and stability improvements - -## [2.5.0] - 2026-01-30 - -### Added -- Process management tools -- Internationalization improvements +## [3.1.1] — 2026-04-17 ### Changed -- Code comments converted to English - -## [2.4.2] - 2026-01-30 - -### Fixed -- Build configuration issues - -## [2.4.1] - 2026-01-30 - -### Fixed -- MCP resources distribution +- **Module Extraction** — `operations.ts` split from 501 to 236 lines; new `consolidator.ts` and `serializer.ts` +- **N+1 query fix** — `getEntitiesByIds()` batch hydration (4 queries instead of 400+ for limit=100) +- **Type Safety** — `as any` casts: 20 to 1 (95% elimination); new typed interfaces for DB rows and LLM responses +- **Input Validation** — shared Zod schemas (`schemas.ts`) as single source of truth; max lengths enforced +- API key masked in `/v1/config` capabilities response +- `updateConfig()` deep-merges LLM config (preserves apiKey on partial updates) +- Express body limit: 1MB +- 396 tests across 24 test files -## [2.4.0] - 2026-01-30 +## [3.1.0] — 2026-04-17 ### Added -- Enhanced testing system -- Hook integration improvements - -## [2.3.1] - 2026-01-30 +- **Self-Improving Memory** — LLM-powered failure analysis in Stop hook automatically extracts root cause, fix, and prevention into structured `lesson_learned` entities +- **Proactive warnings** — session-start hook surfaces known lessons for the current project +- **`learn` tool** — 7th MCP tool for explicitly recording lessons across all 3 transports +- **Upsert dedup** — same error pattern across sessions updates existing lessons instead of creating duplicates +- New modules: `failure-analyzer.ts`, `lesson-engine.ts` ### Fixed -- MCP server lifecycle -- Verification script updates - -## [2.3.0] - 2026-01-30 - -### Added -- NPM package support +- API key in `/v1/config` capabilities response is now masked +- `updateConfig()` deep-merges LLM config to preserve API key on partial updates ### Changed -- Installation improvements - -## [2.2.1] - 2026-01-30 +- 348 tests across 20 test files +- 7 MCP tools, 11 core modules, 3 transports, 4 hooks -### Fixed -- MCP stdio communication -- Build process improvements - -## [2.2.0] - 2026-01-20 +## [3.0.1] — 2026-04-17 ### Added -- Evidence-based guardrails -- Quality gates +- **Built-in Skills** — `/memesh` (proactive memory management) and `/memesh-review` (cleanup recommendations) +- **Dashboard Rebuild** — Preact + Vite architecture, dark theme, 5 tabs +- Content quality improvements: filter system tags from Analytics, pagination in Browse, meaningful memory previews +- Marketing-grade README redesign with dashboard screenshots -### Changed -- Improved E2E test reliability - -## [2.0.0] - 2026-01-01 +## [3.0.0] — 2026-04-17 ### Added -- Initial MCP server implementation -- Core memory management -- Knowledge graph storage +- **Universal AI Memory Layer** — complete rewrite +- **6 MCP Tools** — remember, recall, forget, consolidate, export, import +- **3 Transports** — CLI + HTTP REST API + MCP +- **Smart Recall** — multi-factor scoring + LLM query expansion (97% R@5) +- **Knowledge Evolution** — soft-archive, supersedes, reactivation (never deletes) +- **Session Auto-Capture** — 4 hooks capture knowledge automatically +- **Interactive Dashboard** — Preact + Vite, 5 tabs, dark theme +- 289 tests across 17 test files + +## v2.x Releases + +- **2.16.0** — Interactive Dashboard +- **2.15.0** — Smart Recall +- **2.14.0** — Session Auto-Capture +- **2.13.0** — Core Refactor +- **2.11.0** — Minimal core rewrite (50+ files to 5, 26 deps to 3) +- **2.10.x** — Streamlit Visual Explorer, auto-relation inference +- **2.9.x** — Proactive recall, vector search, architecture refactoring +- **2.8.x** — Device auth, semantic search, hooks system, accessibility +- **2.7.0** — Daemon socket cleanup, memory retention improvements +- **2.6.x** — PathResolver, error formatting, npm publish fixes +- **2.0.0–2.5.x** — Initial MCP server, knowledge graph, process management --- - -For detailed changes, see the [commit history](https://github.com/PCIRCLE-AI/memesh-llm-memory/commits/main). +_Note: The GitHub repository is [PCIRCLE-AI/memesh-llm-memory](https://github.com/PCIRCLE-AI/memesh-llm-memory). The npm package is [@pcircle/memesh](https://www.npmjs.com/package/@pcircle/memesh)._ diff --git a/README.de.md b/README.de.md index f3732d4b..8bd3d2b9 100644 --- a/README.de.md +++ b/README.de.md @@ -11,7 +11,6 @@ MIT Node MCP - PyPI

@@ -62,6 +61,10 @@ memesh MeMesh Analytics — verstehe das Wissen deiner KI

+

+ MeMesh Graph — interaktiver Wissensgraph mit Typfiltern und Ego-Modus +

+ --- ## Für Wen ist Das? @@ -122,7 +125,7 @@ Tools in jeden API-Aufruf einfügen | **Einrichtung** | `npm i -g` — fertig | Neo4j + VectorDB + API-Schlüssel | Neo4j + Konfiguration | | **Speicherung** | Einzelne SQLite-Datei | Neo4j + Qdrant | Neo4j | | **Offline nutzbar** | Ja, immer | Nein | Nein | -| **Dashboard** | Integriert (5 Tabs) | Keins | Keins | +| **Dashboard** | Integriert (7 Tabs + Analytik) | Keins | Keins | | **Abhängigkeiten** | 6 | 20+ | 10+ | | **Preis** | Dauerhaft kostenlos | Kostenlose Stufe / Kostenpflichtig | Kostenlose Stufe / Kostenpflichtig | @@ -136,15 +139,31 @@ Du musst nicht alles manuell merken. MeMesh hat **4 Hooks**, die Wissen erfassen | Wann | Was MeMesh tut | |------|------------------| -| **Zu Beginn jeder Session** | Lädt deine relevantesten Erinnerungen (nach Scoring-Algorithmus gerankt) | +| **Zu Beginn jeder Session** | Lädt deine relevantesten Erinnerungen + proaktive Warnungen aus früheren Lektionen | | **Nach jedem `git commit`** | Erfasst was du geändert hast, mit Diff-Statistiken | -| **Wenn Claude beendet** | Erfasst bearbeitete Dateien, behobene Fehler und getroffene Entscheidungen | +| **Wenn Claude beendet** | Erfasst bearbeitete Dateien, behobene Fehler und generiert automatisch strukturierte Lektionen aus Fehlern | | **Vor der Kontextkomprimierung** | Sichert Wissen, bevor es durch Kontextgrenzen verloren geht | > **Jederzeit deaktivierbar:** `export MEMESH_AUTO_CAPTURE=false` --- +## Dashboard + +7 Tabs, 11 Sprachen, null externe Abhängigkeiten. Zugriff über `http://localhost:3737/dashboard` wenn der Server läuft. + +| Tab | Was du siehst | +|-----|---------------| +| **Search** | Volltextsuche + Vektor-Ähnlichkeitssuche über alle Erinnerungen | +| **Browse** | Paginierte Liste aller Entitäten mit Archivierung/Wiederherstellung | +| **Analytics** | Memory Health Score (0-100), 30-Tage-Timeline, Wert-Metriken, Wissensabdeckung, Aufräum-Vorschläge, deine Arbeitsmuster | +| **Graph** | Interaktiver kräftebasierter Wissensgraph mit Typfiltern, Suche, Ego-Modus, Aktualitäts-Heatmap | +| **Lessons** | Strukturierte Lektionen aus vergangenen Fehlern (Fehler, Ursache, Fix, Prävention) | +| **Manage** | Entitäten archivieren und wiederherstellen | +| **Settings** | LLM-Anbieter-Konfiguration, Sprachauswahl | + +--- + ## Intelligente Funktionen **🧠 Smart Search** — Suche nach „login security" und finde Erinnerungen über „OAuth PKCE". MeMesh erweitert Suchanfragen mit verwandten Begriffen über das konfigurierte LLM. @@ -183,7 +202,7 @@ memesh # öffnet Dashboard → Einstellungs-Tab --- -## Alle 6 Gedächtnis-Tools +## Alle 8 Gedächtnis-Tools | Tool | Funktion | |------|-------------| @@ -193,6 +212,8 @@ memesh # öffnet Dashboard → Einstellungs-Tab | `consolidate` | LLM-gestützte Komprimierung ausführlicher Erinnerungen | | `export` | Teilt Erinnerungen als JSON zwischen Projekten oder Teammitgliedern | | `import` | Importiert Erinnerungen mit Zusammenführungsstrategien (überspringen / überschreiben / anhängen) | +| `learn` | Strukturierte Lektionen aus Fehlern aufzeichnen (Fehler, Ursache, Fix, Prävention) | +| `user_patterns` | Analysiere deine Arbeitsmuster — Zeitplan, Tools, Stärken, Lernbereiche | --- @@ -201,7 +222,7 @@ memesh # öffnet Dashboard → Einstellungs-Tab ``` ┌─────────────────┐ │ Core Engine │ - │ (6 operations) │ + │ (8 operations) │ └────────┬────────┘ ┌─────────────────┼─────────────────┐ │ │ │ @@ -222,7 +243,7 @@ Der Kern ist framework-unabhängig. Dieselbe Logik läuft vom Terminal, HTTP ode ```bash git clone https://github.com/PCIRCLE-AI/memesh-llm-memory cd memesh-llm-memory && npm install && npm run build -npm test -- --run # 289 tests +npm test -- --run # 413 tests ``` Dashboard: `cd dashboard && npm install && npm run dev` diff --git a/README.es.md b/README.es.md index ca6598c9..c8b13bb4 100644 --- a/README.es.md +++ b/README.es.md @@ -11,7 +11,6 @@ MIT Node MCP - PyPI

@@ -62,6 +61,10 @@ memesh MeMesh Analytics — comprende el conocimiento de tu IA

+

+ MeMesh Graph — grafo de conocimiento interactivo con filtros de tipo y modo ego +

+ --- ## ¿Para Quién Es? @@ -122,7 +125,7 @@ Pega las herramientas en cualquier llamada a la API | **Configuración** | `npm i -g` — listo | Neo4j + VectorDB + claves API | Neo4j + config | | **Almacenamiento** | Archivo SQLite único | Neo4j + Qdrant | Neo4j | | **Funciona sin conexión** | Sí, siempre | No | No | -| **Panel** | Integrado (5 pestañas) | Ninguno | Ninguno | +| **Panel** | Integrado (7 pestañas + analytics) | Ninguno | Ninguno | | **Dependencias** | 6 | 20+ | 10+ | | **Precio** | Gratis para siempre | Plan gratuito / De pago | Plan gratuito / De pago | @@ -136,15 +139,31 @@ No necesitas recordar todo manualmente. MeMesh tiene **4 hooks** que capturan co | Cuándo | Qué hace MeMesh | |------|------------------| -| **Al inicio de cada sesión** | Carga tus recuerdos más relevantes (clasificados por algoritmo de puntuación) | +| **Al inicio de cada sesión** | Carga tus recuerdos más relevantes + advertencias proactivas de lecciones pasadas | | **Tras cada `git commit`** | Registra lo que cambiaste, con estadísticas del diff | -| **Cuando Claude se detiene** | Captura archivos editados, errores corregidos y decisiones tomadas | +| **Cuando Claude se detiene** | Captura archivos editados, errores corregidos y genera automáticamente lecciones estructuradas a partir de fallos | | **Antes de la compactación de contexto** | Guarda el conocimiento antes de que se pierda por los límites del contexto | > **Desactívalo cuando quieras:** `export MEMESH_AUTO_CAPTURE=false` --- +## Panel de Control + +7 pestañas, 11 idiomas, cero dependencias externas. Accede en `http://localhost:3737/dashboard` cuando el servidor esté activo. + +| Pestaña | Qué ves | +|---------|---------| +| **Search** | Búsqueda de texto completo + similitud vectorial en todas las memorias | +| **Browse** | Lista paginada de todas las entidades con archivado/restauración | +| **Analytics** | Puntuación de Salud de Memoria (0-100), timeline de 30 días, métricas de valor, cobertura de conocimiento, sugerencias de limpieza, tus patrones de trabajo | +| **Graph** | Grafo de conocimiento interactivo dirigido por fuerzas con filtros de tipo, búsqueda, modo ego, mapa de calor de recencia | +| **Lessons** | Lecciones estructuradas de fallos pasados (error, causa raíz, corrección, prevención) | +| **Manage** | Archivar y restaurar entidades | +| **Settings** | Configuración del proveedor LLM, selector de idioma | + +--- + ## Funcionalidades Inteligentes **🧠 Búsqueda Inteligente** — Busca "login security" y encuentra recuerdos sobre "OAuth PKCE". MeMesh expande las consultas con términos relacionados usando el LLM configurado. @@ -183,7 +202,7 @@ memesh # abre el panel → pestaña Configuración --- -## Las 6 Herramientas de Memoria +## Las 8 Herramientas de Memoria | Herramienta | Qué hace | |------|-------------| @@ -193,6 +212,8 @@ memesh # abre el panel → pestaña Configuración | `consolidate` | Compresión de memorias extensas asistida por LLM | | `export` | Comparte memorias como JSON entre proyectos o miembros del equipo | | `import` | Importa memorias con estrategias de fusión (omitir / sobrescribir / añadir) | +| `learn` | Registra lecciones estructuradas a partir de errores (error, causa raíz, corrección, prevención) | +| `user_patterns` | Analiza tus patrones de trabajo — horario, herramientas, fortalezas, áreas de aprendizaje | --- @@ -201,7 +222,7 @@ memesh # abre el panel → pestaña Configuración ``` ┌─────────────────┐ │ Core Engine │ - │ (6 operations) │ + │ (8 operations) │ └────────┬────────┘ ┌─────────────────┼─────────────────┐ │ │ │ @@ -222,7 +243,7 @@ El núcleo es independiente del framework. La misma lógica se ejecuta desde el ```bash git clone https://github.com/PCIRCLE-AI/memesh-llm-memory cd memesh-llm-memory && npm install && npm run build -npm test -- --run # 289 tests +npm test -- --run # 413 tests ``` Panel: `cd dashboard && npm install && npm run dev` diff --git a/README.fr.md b/README.fr.md index 81d8574e..0ccb868e 100644 --- a/README.fr.md +++ b/README.fr.md @@ -11,7 +11,6 @@ MIT Node MCP - PyPI

@@ -62,6 +61,10 @@ memesh MeMesh Analytics — visualisez les connaissances de votre IA

+

+ MeMesh Graph — graphe de connaissances interactif avec filtres de type et mode ego +

+ --- ## Pour Qui Est-ce Fait ? @@ -122,7 +125,7 @@ Collez les outils dans n'importe quel appel API | **Configuration** | `npm i -g` — terminé | Neo4j + VectorDB + clés API | Neo4j + config | | **Stockage** | Fichier SQLite unique | Neo4j + Qdrant | Neo4j | | **Fonctionne hors ligne** | Oui, toujours | Non | Non | -| **Tableau de bord** | Intégré (5 onglets) | Aucun | Aucun | +| **Tableau de bord** | Intégré (7 onglets + analytiques) | Aucun | Aucun | | **Dépendances** | 6 | 20+ | 10+ | | **Prix** | Gratuit à vie | Offre gratuite / Payant | Offre gratuite / Payant | @@ -136,15 +139,31 @@ Inutile de tout mémoriser manuellement. MeMesh dispose de **4 hooks** qui captu | Quand | Ce que fait MeMesh | |------|------------------| -| **Au démarrage de chaque session** | Charge vos souvenirs les plus pertinents (classés par algorithme de scoring) | +| **Au démarrage de chaque session** | Charge vos souvenirs les plus pertinents + avertissements proactifs des leçons passées | | **Après chaque `git commit`** | Enregistre ce que vous avez modifié, avec les statistiques de diff | -| **Quand Claude s'arrête** | Capture les fichiers édités, les erreurs corrigées et les décisions prises | +| **Quand Claude s'arrête** | Capture les fichiers édités, les erreurs corrigées et génère automatiquement des leçons structurées à partir des échecs | | **Avant la compaction du contexte** | Sauvegarde les connaissances avant qu'elles se perdent dans les limites du contexte | > **Désactivez à tout moment :** `export MEMESH_AUTO_CAPTURE=false` --- +## Tableau de Bord + +7 onglets, 11 langues, zéro dépendance externe. Accessible à `http://localhost:3737/dashboard` lorsque le serveur tourne. + +| Onglet | Ce que vous voyez | +|--------|-------------------| +| **Search** | Recherche plein texte + similarité vectorielle sur tous les souvenirs | +| **Browse** | Liste paginée de toutes les entités avec archivage/restauration | +| **Analytics** | Score de Santé Mémoire (0-100), timeline 30 jours, métriques de valeur, couverture des connaissances, suggestions de nettoyage, vos habitudes de travail | +| **Graph** | Graphe de connaissances interactif dirigé par forces avec filtres de type, recherche, mode ego, carte thermique de récence | +| **Lessons** | Leçons structurées tirées des échecs passés (erreur, cause racine, correction, prévention) | +| **Manage** | Archiver et restaurer des entités | +| **Settings** | Configuration du fournisseur LLM, sélecteur de langue | + +--- + ## Fonctionnalités Intelligentes **🧠 Recherche Intelligente** — Cherchez « login security » et trouvez des souvenirs sur « OAuth PKCE ». MeMesh élargit les requêtes avec des termes connexes via le LLM configuré. @@ -183,7 +202,7 @@ memesh # ouvre le tableau de bord → onglet Paramètres --- -## Les 6 Outils Mémoire +## Les 8 Outils Mémoire | Outil | Ce qu'il fait | |------|-------------| @@ -193,6 +212,8 @@ memesh # ouvre le tableau de bord → onglet Paramètres | `consolidate` | Compression de souvenirs verbeux assistée par LLM | | `export` | Partage les souvenirs en JSON entre projets ou membres d'équipe | | `import` | Importe des souvenirs avec des stratégies de fusion (ignorer / écraser / ajouter) | +| `learn` | Enregistre des leçons structurées à partir des erreurs (erreur, cause racine, correction, prévention) | +| `user_patterns` | Analyse vos habitudes de travail — planning, outils, points forts, axes d'apprentissage | --- @@ -201,7 +222,7 @@ memesh # ouvre le tableau de bord → onglet Paramètres ``` ┌─────────────────┐ │ Core Engine │ - │ (6 operations) │ + │ (8 operations) │ └────────┬────────┘ ┌─────────────────┼─────────────────┐ │ │ │ @@ -222,7 +243,7 @@ Le cœur est indépendant du framework. La même logique s'exécute depuis le te ```bash git clone https://github.com/PCIRCLE-AI/memesh-llm-memory cd memesh-llm-memory && npm install && npm run build -npm test -- --run # 289 tests +npm test -- --run # 413 tests ``` Tableau de bord : `cd dashboard && npm install && npm run dev` diff --git a/README.ja.md b/README.ja.md index add8aaca..3130f3e0 100644 --- a/README.ja.md +++ b/README.ja.md @@ -11,7 +11,6 @@ MIT Node MCP - PyPI

@@ -62,6 +61,10 @@ memesh MeMesh Analytics — AI の知識を可視化

+

+ MeMesh Graph — タイプフィルターとエゴモード付きインタラクティブ知識グラフ +

+ --- ## 誰のためのツールか? @@ -122,7 +125,7 @@ memesh export-schema \ | **設定方法** | `npm i-g` — 完了 | Neo4j + VectorDB + API キー | Neo4j + 設定 | | **ストレージ** | SQLite ファイル 1 つ | Neo4j + Qdrant | Neo4j | | **オフライン利用** | 常時対応 | 非対応 | 非対応 | -| **ダッシュボード** | 組み込み(5 タブ) | なし | なし | +| **ダッシュボード** | 組み込み(7 タブ + アナリティクス) | なし | なし | | **依存関係** | 6 | 20+ | 10+ | | **価格** | 永久無料 | 無料枠 / 有料 | 無料枠 / 有料 | @@ -136,15 +139,31 @@ memesh export-schema \ | タイミング | MeMesh が行うこと | |------|------------------| -| **セッション開始時** | スコアリングアルゴリズムで最も関連性の高いメモリを読み込む | +| **セッション開始時** | 最も関連性の高いメモリを読み込み + 過去の教訓からのプロアクティブな警告 | | **`git commit` 後** | 変更内容と差分統計を記録する | -| **Claude 終了時** | 編集したファイル・修正したエラー・下した決断をキャプチャする | +| **Claude 終了時** | 編集したファイル・修正したエラーをキャプチャし、失敗から構造化された教訓を自動生成 | | **コンテキスト圧縮前** | コンテキスト上限で失われる前に知識を保存する | > **いつでも無効化:** `export MEMESH_AUTO_CAPTURE=false` --- +## ダッシュボード + +7 タブ、11 言語、外部依存ゼロ。サーバー起動中は `http://localhost:3737/dashboard` でアクセス。 + +| タブ | 内容 | +|------|------| +| **Search** | すべてのメモリに対する全文検索 + ベクトル類似度検索 | +| **Browse** | すべてのエンティティのページネーション一覧(アーカイブ/復元対応) | +| **Analytics** | メモリヘルススコア(0-100)、30日タイムライン、価値指標、知識カバレッジ、クリーンアップ提案、作業パターン | +| **Graph** | タイプフィルター、検索、エゴモード、最新性ヒートマップ付きインタラクティブ力学グラフ | +| **Lessons** | 過去の失敗から生成された構造化レッスン(エラー、根本原因、修正方法、予防策) | +| **Manage** | エンティティのアーカイブと復元 | +| **Settings** | LLM プロバイダー設定、言語セレクター | + +--- + ## スマート機能 **🧠 スマート検索** — 「login security」で「OAuth PKCE」に関するメモリが見つかります。設定した LLM を使ってクエリを関連語に展開します。 @@ -183,7 +202,7 @@ memesh # ダッシュボードを開く → 設定タブ --- -## 全 6 つのメモリツール +## 全 8 つのメモリツール | ツール | 機能 | |------|-------------| @@ -193,6 +212,8 @@ memesh # ダッシュボードを開く → 設定タブ | `consolidate` | LLM を使って冗長なメモリを圧縮 | | `export` | メモリを JSON でプロジェクトやチームメンバーと共有 | | `import` | マージ戦略(スキップ / 上書き / 追加)を選んでメモリをインポート | +| `learn` | ミスから構造化されたレッスンを記録(エラー、根本原因、修正方法、予防策) | +| `user_patterns` | 作業パターンを分析 — スケジュール、ツール、強み、学習分野 | --- @@ -201,7 +222,7 @@ memesh # ダッシュボードを開く → 設定タブ ``` ┌─────────────────┐ │ Core Engine │ - │ (6 operations) │ + │ (8 operations) │ └────────┬────────┘ ┌─────────────────┼─────────────────┐ │ │ │ @@ -222,7 +243,7 @@ memesh # ダッシュボードを開く → 設定タブ ```bash git clone https://github.com/PCIRCLE-AI/memesh-llm-memory cd memesh-llm-memory && npm install && npm run build -npm test -- --run # 289 tests +npm test -- --run # 413 tests ``` ダッシュボード:`cd dashboard && npm install && npm run dev` diff --git a/README.ko.md b/README.ko.md index 22e81bfc..aa6e02d6 100644 --- a/README.ko.md +++ b/README.ko.md @@ -11,7 +11,6 @@ MIT Node MCP - PyPI

@@ -62,6 +61,10 @@ memesh MeMesh Analytics — AI의 지식을 한눈에 파악

+

+ MeMesh Graph — 타입 필터와 에고 모드가 있는 인터랙티브 지식 그래프 +

+ --- ## 누구를 위한 도구인가? @@ -122,7 +125,7 @@ memesh export-schema \ | **설정 방법** | `npm i -g` — 완료 | Neo4j + VectorDB + API 키 | Neo4j + 설정 | | **저장소** | SQLite 파일 하나 | Neo4j + Qdrant | Neo4j | | **오프라인 사용** | 항상 가능 | 불가 | 불가 | -| **대시보드** | 내장 (5개 탭) | 없음 | 없음 | +| **대시보드** | 내장 (7개 탭 + 분석) | 없음 | 없음 | | **의존성** | 6개 | 20개 이상 | 10개 이상 | | **가격** | 영구 무료 | 무료 티어 / 유료 | 무료 티어 / 유료 | @@ -136,15 +139,31 @@ memesh export-schema \ | 언제 | MeMesh가 하는 일 | |------|------------------| -| **모든 세션 시작 시** | 스코어링 알고리즘으로 가장 관련성 높은 메모리를 로드 | +| **모든 세션 시작 시** | 가장 관련성 높은 메모리를 로드 + 과거 교훈으로부터의 사전 경고 | | **모든 `git commit` 후** | 변경 내용과 diff 통계를 기록 | -| **Claude 종료 시** | 편집한 파일, 수정한 오류, 내린 결정을 포착 | +| **Claude 종료 시** | 편집한 파일, 수정한 오류를 포착하고 실패로부터 구조화된 교훈을 자동 생성 | | **컨텍스트 압축 전** | 컨텍스트 한계로 사라지기 전에 지식을 저장 | > **언제든 비활성화:** `export MEMESH_AUTO_CAPTURE=false` --- +## 대시보드 + +7개 탭, 11개 언어, 외부 의존성 제로. 서버 실행 중 `http://localhost:3737/dashboard`에서 접근. + +| 탭 | 내용 | +|----|------| +| **Search** | 모든 메모리에 대한 전문 검색 + 벡터 유사도 검색 | +| **Browse** | 모든 엔티티의 페이지네이션 목록 (아카이브/복원 지원) | +| **Analytics** | 메모리 건강 점수 (0-100), 30일 타임라인, 가치 지표, 지식 커버리지, 정리 제안, 작업 패턴 | +| **Graph** | 타입 필터, 검색, 에고 모드, 최신성 히트맵이 있는 인터랙티브 포스 그래프 | +| **Lessons** | 과거 실패로부터 생성된 구조화된 교훈 (오류, 근본 원인, 수정, 예방) | +| **Manage** | 엔티티 아카이브 및 복원 | +| **Settings** | LLM 제공자 설정, 언어 선택 | + +--- + ## 스마트 기능 **🧠 스마트 검색** — "login security"를 검색하면 "OAuth PKCE"에 관한 메모리를 찾아냅니다. 설정한 LLM을 이용해 쿼리를 관련 용어로 확장합니다. @@ -183,7 +202,7 @@ memesh # 대시보드 열기 → 설정 탭 --- -## 전체 6가지 메모리 도구 +## 전체 8가지 메모리 도구 | 도구 | 기능 | |------|-------------| @@ -193,6 +212,8 @@ memesh # 대시보드 열기 → 설정 탭 | `consolidate` | LLM 기반 장황한 메모리 압축 | | `export` | 메모리를 JSON으로 프로젝트 또는 팀원과 공유 | | `import` | 병합 전략(건너뛰기 / 덮어쓰기 / 추가)을 선택해 메모리 가져오기 | +| `learn` | 실수로부터 구조화된 교훈 기록 (오류, 근본 원인, 수정, 예방) | +| `user_patterns` | 작업 패턴 분석 — 일정, 도구, 강점, 학습 영역 | --- @@ -201,7 +222,7 @@ memesh # 대시보드 열기 → 설정 탭 ``` ┌─────────────────┐ │ Core Engine │ - │ (6 operations) │ + │ (8 operations) │ └────────┬────────┘ ┌─────────────────┼─────────────────┐ │ │ │ @@ -222,7 +243,7 @@ memesh # 대시보드 열기 → 설정 탭 ```bash git clone https://github.com/PCIRCLE-AI/memesh-llm-memory cd memesh-llm-memory && npm install && npm run build -npm test -- --run # 289 tests +npm test -- --run # 413 tests ``` 대시보드: `cd dashboard && npm install && npm run dev` diff --git a/README.md b/README.md index f3d211c0..99c0b656 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ MIT Node MCP - PyPI

@@ -59,7 +58,11 @@ memesh

- MeMesh Analytics — understand your AI's knowledge + MeMesh Analytics — health score, timeline, patterns, knowledge coverage +

+ +

+ MeMesh Graph — interactive knowledge graph with type filters and ego mode

--- @@ -90,13 +93,12 @@ MCP protocol (auto-configured) -**Python / LangChain** -```python -from memesh import MeMesh -m = MeMesh() -m.recall("auth") +**Any HTTP Client** +```bash +curl localhost:3737/v1/recall \ + -d '{"query":"auth"}' ``` -`pip install memesh` +`memesh serve` (REST API) @@ -122,7 +124,7 @@ Paste tools into any API call | **Setup** | `npm i -g` — done | Neo4j + VectorDB + API keys | Neo4j + config | | **Storage** | Single SQLite file | Neo4j + Qdrant | Neo4j | | **Works offline** | Yes, always | No | No | -| **Dashboard** | Built-in (5 tabs) | None | None | +| **Dashboard** | Built-in (7 tabs + analytics) | None | None | | **Dependencies** | 6 | 20+ | 10+ | | **Price** | Free forever | Free tier / Paid | Free tier / Paid | @@ -136,15 +138,31 @@ You don't need to manually remember everything. MeMesh has **4 hooks** that capt | When | What MeMesh does | |------|------------------| -| **Every session start** | Loads your most relevant memories (ranked by scoring algorithm) | +| **Every session start** | Loads your most relevant memories + proactive warnings from past lessons | | **After every `git commit`** | Records what you changed, with diff stats | -| **When Claude stops** | Captures files edited, errors fixed, and decisions made | +| **When Claude stops** | Captures files edited, errors fixed, and auto-generates structured lessons from failures | | **Before context compaction** | Saves knowledge before it's lost to context limits | > **Opt out anytime:** `export MEMESH_AUTO_CAPTURE=false` --- +## Dashboard + +7 tabs, 11 languages, zero external dependencies. Access at `http://localhost:3737/dashboard` when the server is running. + +| Tab | What you see | +|-----|-------------| +| **Search** | Full-text + vector similarity search across all memories | +| **Browse** | Paginated list of all entities with archive/restore | +| **Analytics** | Memory Health Score (0-100), 30-day timeline, value metrics, knowledge coverage, cleanup suggestions, your work patterns | +| **Graph** | Interactive force-directed knowledge graph with type filters, search, ego mode, recency heatmap | +| **Lessons** | Structured lessons from past failures (error, root cause, fix, prevention) | +| **Manage** | Archive and restore entities | +| **Settings** | LLM provider config, language selector | + +--- + ## Smart Features **🧠 Smart Search** — Search "login security" and find memories about "OAuth PKCE". MeMesh expands queries with related terms using your configured LLM. @@ -159,6 +177,19 @@ You don't need to manually remember everything. MeMesh has **4 hooks** that capt --- +## Real-World Usage + +> "MeMesh remembered that we chose PKCE over implicit flow three weeks ago. When I asked Claude about auth again, it already knew — no re-explaining needed." +> — **Solo developer, building a SaaS** + +> "We export our team's memory every Friday and import it Monday. Everyone's Claude starts the week knowing what the team learned last week." +> — **3-person startup, shared knowledge base** + +> "The dashboard showed me that 90% of my memories were auto-generated session logs. I started using `remember` deliberately for architecture decisions. Game changer." +> — **Developer who discovered the Analytics tab** + +--- + ## Unlock Smart Mode (Optional) MeMesh works fully offline out of the box. Add an LLM API key to unlock smarter search: @@ -183,7 +214,7 @@ memesh # opens dashboard → Settings tab --- -## All 6 Memory Tools +## All 8 Memory Tools | Tool | What it does | |------|-------------| @@ -193,6 +224,8 @@ memesh # opens dashboard → Settings tab | `consolidate` | LLM-powered compression of verbose memories | | `export` | Share memories as JSON between projects or team members | | `import` | Import memories with merge strategies (skip / overwrite / append) | +| `learn` | Record structured lessons from mistakes (error, root cause, fix, prevention) | +| `user_patterns` | Analyze your work patterns — schedule, tools, strengths, learning areas | --- @@ -201,7 +234,7 @@ memesh # opens dashboard → Settings tab ``` ┌─────────────────┐ │ Core Engine │ - │ (6 operations) │ + │ (8 operations) │ └────────┬────────┘ ┌─────────────────┼─────────────────┐ │ │ │ @@ -222,7 +255,7 @@ Core is framework-agnostic. Same logic runs from terminal, HTTP, or MCP. ```bash git clone https://github.com/PCIRCLE-AI/memesh-llm-memory cd memesh-llm-memory && npm install && npm run build -npm test -- --run # 289 tests +npm test -- --run # 413 tests ``` Dashboard: `cd dashboard && npm install && npm run dev` diff --git a/README.pt.md b/README.pt.md index c8a90008..cf7ff585 100644 --- a/README.pt.md +++ b/README.pt.md @@ -11,7 +11,6 @@ MIT Node MCP - PyPI

@@ -62,6 +61,10 @@ memesh MeMesh Analytics — entenda o conhecimento do seu AI

+

+ MeMesh Graph — grafo de conhecimento interativo com filtros de tipo e modo ego +

+ --- ## Para Quem É Isso? @@ -122,7 +125,7 @@ Cole as ferramentas em qualquer chamada de API | **Configuração** | `npm i -g` — pronto | Neo4j + VectorDB + chaves de API | Neo4j + config | | **Armazenamento** | Arquivo SQLite único | Neo4j + Qdrant | Neo4j | | **Funciona offline** | Sim, sempre | Não | Não | -| **Painel** | Integrado (5 abas) | Nenhum | Nenhum | +| **Painel** | Integrado (7 abas + analytics) | Nenhum | Nenhum | | **Dependências** | 6 | 20+ | 10+ | | **Preço** | Grátis para sempre | Plano gratuito / Pago | Plano gratuito / Pago | @@ -136,15 +139,31 @@ Você não precisa lembrar de tudo manualmente. O MeMesh possui **4 hooks** que | Quando | O que o MeMesh faz | |------|------------------| -| **Início de cada sessão** | Carrega suas memórias mais relevantes (classificadas por algoritmo de pontuação) | +| **Início de cada sessão** | Carrega suas memórias mais relevantes + avisos proativos de lições passadas | | **Após cada `git commit`** | Registra o que você alterou, com estatísticas do diff | -| **Quando o Claude para** | Captura arquivos editados, erros corrigidos e decisões tomadas | +| **Quando o Claude para** | Captura arquivos editados, erros corrigidos e gera automaticamente lições estruturadas a partir de falhas | | **Antes da compactação de contexto** | Salva o conhecimento antes que se perca por limite de contexto | > **Desative quando quiser:** `export MEMESH_AUTO_CAPTURE=false` --- +## Painel + +7 abas, 11 idiomas, zero dependências externas. Acesse em `http://localhost:3737/dashboard` quando o servidor estiver rodando. + +| Aba | O que você vê | +|-----|---------------| +| **Search** | Busca full-text + similaridade vetorial em todas as memórias | +| **Browse** | Lista paginada de todas as entidades com arquivamento/restauração | +| **Analytics** | Pontuação de Saúde da Memória (0-100), timeline de 30 dias, métricas de valor, cobertura de conhecimento, sugestões de limpeza, seus padrões de trabalho | +| **Graph** | Grafo de conhecimento interativo dirigido por força com filtros de tipo, busca, modo ego, heatmap de recência | +| **Lessons** | Lições estruturadas de falhas passadas (erro, causa raiz, correção, prevenção) | +| **Manage** | Arquivar e restaurar entidades | +| **Settings** | Configuração do provedor LLM, seletor de idioma | + +--- + ## Funcionalidades Inteligentes **🧠 Busca Inteligente** — Pesquise "login security" e encontre memórias sobre "OAuth PKCE". O MeMesh expande consultas com termos relacionados usando o LLM configurado. @@ -183,7 +202,7 @@ memesh # abre o painel → aba Configurações --- -## Todas as 6 Ferramentas de Memória +## Todas as 8 Ferramentas de Memória | Ferramenta | O que faz | |------|-------------| @@ -193,6 +212,8 @@ memesh # abre o painel → aba Configurações | `consolidate` | Compressão de memórias longas com auxílio de LLM | | `export` | Compartilha memórias como JSON entre projetos ou membros da equipe | | `import` | Importa memórias com estratégias de mesclagem (pular / sobrescrever / anexar) | +| `learn` | Registra lições estruturadas a partir de erros (erro, causa raiz, correção, prevenção) | +| `user_patterns` | Analisa seus padrões de trabalho — agenda, ferramentas, pontos fortes, áreas de aprendizado | --- @@ -201,7 +222,7 @@ memesh # abre o painel → aba Configurações ``` ┌─────────────────┐ │ Core Engine │ - │ (6 operations) │ + │ (8 operations) │ └────────┬────────┘ ┌─────────────────┼─────────────────┐ │ │ │ @@ -222,7 +243,7 @@ O núcleo é independente de framework. A mesma lógica é executada a partir do ```bash git clone https://github.com/PCIRCLE-AI/memesh-llm-memory cd memesh-llm-memory && npm install && npm run build -npm test -- --run # 289 tests +npm test -- --run # 413 tests ``` Painel: `cd dashboard && npm install && npm run dev` diff --git a/README.th.md b/README.th.md index 97509e6b..d9916efd 100644 --- a/README.th.md +++ b/README.th.md @@ -11,7 +11,6 @@ MIT Node MCP - PyPI

@@ -62,6 +61,10 @@ memesh MeMesh Analytics — ทำความเข้าใจความรู้ของ AI คุณ

+

+ MeMesh Graph — กราฟความรู้แบบโต้ตอบพร้อมฟิลเตอร์ประเภทและโหมด ego +

+ --- ## เหมาะสำหรับใคร? @@ -122,7 +125,7 @@ memesh export-schema \ | **การตั้งค่า** | `npm i -g` — เสร็จ | Neo4j + VectorDB + API key | Neo4j + config | | **การจัดเก็บ** | ไฟล์ SQLite เดียว | Neo4j + Qdrant | Neo4j | | **ใช้ออฟไลน์ได้** | ได้ เสมอ | ไม่ได้ | ไม่ได้ | -| **Dashboard** | ในตัว (5 แท็บ) | ไม่มี | ไม่มี | +| **Dashboard** | ในตัว (7 แท็บ + analytics) | ไม่มี | ไม่มี | | **Dependencies** | 6 | 20+ | 10+ | | **ราคา** | ฟรีตลอดชีพ | แผนฟรี / เสียเงิน | แผนฟรี / เสียเงิน | @@ -136,15 +139,31 @@ memesh export-schema \ | เมื่อไหร่ | MeMesh ทำอะไร | |------|------------------| -| **ทุกครั้งที่เริ่ม session** | โหลดความจำที่เกี่ยวข้องมากที่สุดของคุณ (จัดอันดับด้วย scoring algorithm) | +| **ทุกครั้งที่เริ่ม session** | โหลดความจำที่เกี่ยวข้องมากที่สุด + การแจ้งเตือนเชิงรุกจากบทเรียนที่ผ่านมา | | **หลังทุก `git commit`** | บันทึกสิ่งที่คุณเปลี่ยน พร้อมสถิติ diff | -| **เมื่อ Claude หยุดทำงาน** | จับไฟล์ที่แก้ไข บักที่แก้ไขแล้ว และการตัดสินใจที่เกิดขึ้น | +| **เมื่อ Claude หยุดทำงาน** | จับไฟล์ที่แก้ไข บักที่แก้ไขแล้ว และสร้างบทเรียนที่มีโครงสร้างจากความล้มเหลวโดยอัตโนมัติ | | **ก่อนการบีบอัด context** | บันทึกความรู้ก่อนที่จะหายไปเพราะข้อจำกัดของ context | > **ปิดใช้งานได้ตลอดเวลา:** `export MEMESH_AUTO_CAPTURE=false` --- +## Dashboard + +7 แท็บ, 11 ภาษา, ไม่มี dependency ภายนอก เข้าถึงที่ `http://localhost:3737/dashboard` เมื่อเซิร์ฟเวอร์ทำงาน + +| แท็บ | สิ่งที่คุณเห็น | +|------|----------------| +| **Search** | ค้นหาเต็มรูปแบบ + ความคล้ายคลึงเชิงเวกเตอร์ในทุกความจำ | +| **Browse** | รายการแบบแบ่งหน้าของ entity ทั้งหมดพร้อมการเก็บถาวร/กู้คืน | +| **Analytics** | คะแนนสุขภาพหน่วยความจำ (0-100), ไทม์ไลน์ 30 วัน, ตัวชี้วัดมูลค่า, ความครอบคลุมของความรู้, คำแนะนำการทำความสะอาด, pattern การทำงาน | +| **Graph** | กราฟความรู้แบบโต้ตอบ force-directed พร้อมฟิลเตอร์ประเภท, ค้นหา, โหมด ego, heatmap ความใหม่ | +| **Lessons** | บทเรียนที่มีโครงสร้างจากความล้มเหลวในอดีต (ข้อผิดพลาด, สาเหตุหลัก, การแก้ไข, การป้องกัน) | +| **Manage** | เก็บถาวรและกู้คืน entity | +| **Settings** | ตั้งค่าผู้ให้บริการ LLM, เลือกภาษา | + +--- + ## ฟีเจอร์อัจฉริยะ **🧠 Smart Search** — ค้น "login security" แล้วพบความจำเกี่ยวกับ "OAuth PKCE" MeMesh ขยายคำค้นด้วยคำที่เกี่ยวข้องผ่าน LLM ที่ตั้งค่าไว้ @@ -183,7 +202,7 @@ memesh # เปิด dashboard → แท็บ Settings --- -## เครื่องมือหน่วยความจำทั้ง 6 อย่าง +## เครื่องมือหน่วยความจำทั้ง 8 อย่าง | เครื่องมือ | หน้าที่ | |------|-------------| @@ -193,6 +212,8 @@ memesh # เปิด dashboard → แท็บ Settings | `consolidate` | บีบอัดความจำที่ยืดเยื้อด้วย LLM | | `export` | แชร์ความจำในรูปแบบ JSON ระหว่างโปรเจกต์หรือสมาชิกทีม | | `import` | นำเข้าความจำด้วยกลยุทธ์การรวม (ข้าม / เขียนทับ / ต่อท้าย) | +| `learn` | บันทึกบทเรียนที่มีโครงสร้างจากข้อผิดพลาด (ข้อผิดพลาด, สาเหตุหลัก, การแก้ไข, การป้องกัน) | +| `user_patterns` | วิเคราะห์ pattern การทำงาน — ตารางเวลา, เครื่องมือ, จุดแข็ง, สิ่งที่ต้องเรียนรู้ | --- @@ -201,7 +222,7 @@ memesh # เปิด dashboard → แท็บ Settings ``` ┌─────────────────┐ │ Core Engine │ - │ (6 operations) │ + │ (8 operations) │ └────────┬────────┘ ┌─────────────────┼─────────────────┐ │ │ │ @@ -222,7 +243,7 @@ Core ไม่ขึ้นกับ framework ตรรกะเดียวก ```bash git clone https://github.com/PCIRCLE-AI/memesh-llm-memory cd memesh-llm-memory && npm install && npm run build -npm test -- --run # 289 tests +npm test -- --run # 413 tests ``` Dashboard: `cd dashboard && npm install && npm run dev` diff --git a/README.vi.md b/README.vi.md index 1d6c04af..a55671cf 100644 --- a/README.vi.md +++ b/README.vi.md @@ -11,7 +11,6 @@ MIT Node MCP - PyPI

@@ -62,6 +61,10 @@ memesh MeMesh Analytics — hiểu rõ tri thức của AI bạn

+

+ MeMesh Graph — đồ thị tri thức tương tác với bộ lọc loại và chế độ ego +

+ --- ## Dành Cho Ai? @@ -122,7 +125,7 @@ Dán tools vào bất kỳ API call nào | **Cấu hình** | `npm i -g` — xong | Neo4j + VectorDB + API key | Neo4j + config | | **Lưu trữ** | Một file SQLite | Neo4j + Qdrant | Neo4j | | **Hoạt động offline** | Có, luôn luôn | Không | Không | -| **Dashboard** | Tích hợp sẵn (5 tab) | Không có | Không có | +| **Dashboard** | Tích hợp sẵn (7 tab + phân tích) | Không có | Không có | | **Phụ thuộc** | 6 | 20+ | 10+ | | **Giá** | Miễn phí mãi mãi | Gói miễn phí / Trả phí | Gói miễn phí / Trả phí | @@ -136,15 +139,31 @@ Bạn không cần phải tự ghi nhớ mọi thứ. MeMesh có **4 hook** tự | Khi nào | MeMesh làm gì | |------|------------------| -| **Mỗi khi bắt đầu phiên** | Tải các ký ức liên quan nhất (xếp hạng theo thuật toán scoring) | +| **Mỗi khi bắt đầu phiên** | Tải các ký ức liên quan nhất + cảnh báo chủ động từ bài học trước đó | | **Sau mỗi `git commit`** | Ghi lại những gì bạn thay đổi, kèm thống kê diff | -| **Khi Claude kết thúc** | Thu thập file đã sửa, lỗi đã fix và quyết định đã đưa ra | +| **Khi Claude kết thúc** | Thu thập file đã sửa, lỗi đã fix và tự động tạo bài học có cấu trúc từ các lỗi | | **Trước khi nén context** | Lưu kiến thức trước khi mất do giới hạn context | > **Tắt bất cứ lúc nào:** `export MEMESH_AUTO_CAPTURE=false` --- +## Dashboard + +7 tab, 11 ngôn ngữ, không phụ thuộc bên ngoài. Truy cập tại `http://localhost:3737/dashboard` khi server đang chạy. + +| Tab | Nội dung | +|-----|----------| +| **Search** | Tìm kiếm toàn văn + tương tự vector trên tất cả ký ức | +| **Browse** | Danh sách phân trang của tất cả thực thể với lưu trữ/khôi phục | +| **Analytics** | Điểm Sức khỏe Bộ nhớ (0-100), timeline 30 ngày, chỉ số giá trị, phạm vi kiến thức, đề xuất dọn dẹp, pattern làm việc | +| **Graph** | Đồ thị tri thức tương tác dạng lực với bộ lọc loại, tìm kiếm, chế độ ego, heatmap mức độ mới | +| **Lessons** | Bài học có cấu trúc từ các lỗi trước đó (lỗi, nguyên nhân gốc, cách sửa, phòng ngừa) | +| **Manage** | Lưu trữ và khôi phục thực thể | +| **Settings** | Cấu hình nhà cung cấp LLM, chọn ngôn ngữ | + +--- + ## Tính Năng Thông Minh **🧠 Tìm kiếm thông minh** — Tìm "login security" là ra ký ức về "OAuth PKCE". MeMesh mở rộng truy vấn bằng các thuật ngữ liên quan qua LLM đã cấu hình. @@ -183,7 +202,7 @@ memesh # mở dashboard → tab Cài đặt --- -## Tất Cả 6 Công Cụ Bộ Nhớ +## Tất Cả 8 Công Cụ Bộ Nhớ | Công cụ | Chức năng | |------|-------------| @@ -193,6 +212,8 @@ memesh # mở dashboard → tab Cài đặt | `consolidate` | Nén ký ức dài dòng bằng LLM | | `export` | Chia sẻ ký ức dạng JSON giữa dự án hoặc thành viên nhóm | | `import` | Nhập ký ức với chiến lược gộp (bỏ qua / ghi đè / nối thêm) | +| `learn` | Ghi lại bài học có cấu trúc từ sai lầm (lỗi, nguyên nhân gốc, cách sửa, phòng ngừa) | +| `user_patterns` | Phân tích pattern làm việc — lịch trình, công cụ, điểm mạnh, lĩnh vực cần học | --- @@ -201,7 +222,7 @@ memesh # mở dashboard → tab Cài đặt ``` ┌─────────────────┐ │ Core Engine │ - │ (6 operations) │ + │ (8 operations) │ └────────┬────────┘ ┌─────────────────┼─────────────────┐ │ │ │ @@ -222,7 +243,7 @@ Core độc lập với framework. Logic giống nhau chạy từ terminal, HTTP ```bash git clone https://github.com/PCIRCLE-AI/memesh-llm-memory cd memesh-llm-memory && npm install && npm run build -npm test -- --run # 289 tests +npm test -- --run # 413 tests ``` Dashboard: `cd dashboard && npm install && npm run dev` diff --git a/README.zh-CN.md b/README.zh-CN.md index 0bdd4b0b..49757984 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -11,7 +11,6 @@ MIT Node MCP - PyPI

@@ -62,6 +61,10 @@ memesh MeMesh Analytics — 深入了解 AI 的知识

+

+ MeMesh Graph — 交互式知识图谱,支持类型筛选与自我模式 +

+ --- ## 这是为谁设计的? @@ -122,7 +125,7 @@ memesh export-schema \ | **配置方式** | `npm i -g` — 完成 | Neo4j + VectorDB + API 密钥 | Neo4j + 配置 | | **存储方式** | 单一 SQLite 文件 | Neo4j + Qdrant | Neo4j | | **离线使用** | 支持,始终如此 | 不支持 | 不支持 | -| **仪表板** | 内置(5 个标签页) | 无 | 无 | +| **仪表板** | 内置(7 个标签页 + 分析) | 无 | 无 | | **依赖包** | 6 | 20+ | 10+ | | **价格** | 永久免费 | 免费方案/付费 | 免费方案/付费 | @@ -136,15 +139,31 @@ memesh export-schema \ | 时机 | MeMesh 做了什么 | |------|------------------| -| **每次会话开始** | 依评分算法加载最相关的记忆 | +| **每次会话开始** | 加载最相关的记忆 + 来自过去教训的主动警告 | | **每次 `git commit` 后** | 记录你的变更内容与差异统计 | -| **Claude 结束时** | 捕获已编辑的文件、已修复的错误及做出的决策 | +| **Claude 结束时** | 捕获已编辑的文件、已修复的错误,并自动从失败中生成结构化教训 | | **上下文压缩前** | 在知识因上下文限制消失前保存起来 | > **随时退出:** `export MEMESH_AUTO_CAPTURE=false` --- +## 仪表板 + +7 个标签页、11 种语言、零外部依赖。服务器运行后在 `http://localhost:3737/dashboard` 访问。 + +| 标签页 | 内容 | +|--------|------| +| **Search** | 跨所有记忆的全文搜索 + 向量相似度搜索 | +| **Browse** | 所有实体的分页列表,支持归档/恢复 | +| **Analytics** | 记忆健康分数(0-100)、30 天时间轴、价值指标、知识覆盖率、清理建议、你的工作模式 | +| **Graph** | 交互式力导向知识图谱,支持类型筛选、搜索、自我模式、时间热力图 | +| **Lessons** | 从过去失败中生成的结构化教训(错误、根因、修复方法、预防措施) | +| **Manage** | 归档与恢复实体 | +| **Settings** | LLM 提供者配置、语言选择 | + +--- + ## 智能功能 **🧠 智能搜索** — 搜索「login security」就能找到关于「OAuth PKCE」的记忆。MeMesh 使用你配置的 LLM 将查询扩展为相关词汇。 @@ -183,7 +202,7 @@ memesh # 打开仪表板 → 设置标签页 --- -## 全部 6 个记忆工具 +## 全部 8 个记忆工具 | 工具 | 功能说明 | |------|-------------| @@ -193,6 +212,8 @@ memesh # 打开仪表板 → 设置标签页 | `consolidate` | LLM 驱动的冗长记忆压缩 | | `export` | 将记忆以 JSON 格式分享给其他项目或团队成员 | | `import` | 导入记忆,支持合并策略(跳过 / 覆盖 / 追加) | +| `learn` | 记录来自错误的结构化教训(错误、根因、修复方法、预防措施) | +| `user_patterns` | 分析你的工作模式 — 时间安排、工具、优势、学习方向 | --- @@ -201,7 +222,7 @@ memesh # 打开仪表板 → 设置标签页 ``` ┌─────────────────┐ │ Core Engine │ - │ (6 operations) │ + │ (8 operations) │ └────────┬────────┘ ┌─────────────────┼─────────────────┐ │ │ │ @@ -222,7 +243,7 @@ memesh # 打开仪表板 → 设置标签页 ```bash git clone https://github.com/PCIRCLE-AI/memesh-llm-memory cd memesh-llm-memory && npm install && npm run build -npm test -- --run # 289 tests +npm test -- --run # 413 tests ``` 仪表板:`cd dashboard && npm install && npm run dev` diff --git a/README.zh-TW.md b/README.zh-TW.md index d40efa11..863ed0a4 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -11,7 +11,6 @@ MIT Node MCP - PyPI

@@ -62,6 +61,10 @@ memesh MeMesh Analytics — 深入了解 AI 的知識

+

+ MeMesh Graph — 互動式知識圖譜,支援類型篩選與自我模式 +

+ --- ## 這是為誰設計的? @@ -122,7 +125,7 @@ memesh export-schema \ | **設定方式** | `npm i -g` — 完成 | Neo4j + VectorDB + API 金鑰 | Neo4j + 設定 | | **儲存方式** | 單一 SQLite 檔案 | Neo4j + Qdrant | Neo4j | | **離線使用** | 支援,始終如此 | 不支援 | 不支援 | -| **儀表板** | 內建(5 個頁籤) | 無 | 無 | +| **儀表板** | 內建(7 個頁籤 + 分析) | 無 | 無 | | **依賴套件** | 6 | 20+ | 10+ | | **價格** | 永久免費 | 免費方案/付費 | 免費方案/付費 | @@ -136,15 +139,31 @@ memesh export-schema \ | 時機 | MeMesh 做了什麼 | |------|------------------| -| **每次工作階段開始** | 依評分演算法載入最相關的記憶 | +| **每次工作階段開始** | 載入最相關的記憶 + 來自過去教訓的主動警告 | | **每次 `git commit` 後** | 記錄你的變更內容與差異統計 | -| **Claude 結束時** | 擷取已編輯的檔案、已修復的錯誤及做出的決策 | +| **Claude 結束時** | 擷取已編輯的檔案、已修復的錯誤,並自動從失敗中產生結構化教訓 | | **上下文壓縮前** | 在知識因上下文限制消失前儲存起來 | > **隨時退出:** `export MEMESH_AUTO_CAPTURE=false` --- +## 儀表板 + +7 個頁籤、11 種語言、零外部依賴。伺服器啟動後在 `http://localhost:3737/dashboard` 存取。 + +| 頁籤 | 內容 | +|------|------| +| **Search** | 跨所有記憶的全文搜尋 + 向量相似度搜尋 | +| **Browse** | 所有實體的分頁列表,支援封存/還原 | +| **Analytics** | 記憶健康分數(0-100)、30 天時間軸、價值指標、知識覆蓋率、清理建議、你的工作模式 | +| **Graph** | 互動式力導向知識圖譜,支援類型篩選、搜尋、自我模式、時間熱力圖 | +| **Lessons** | 從過去失敗中產生的結構化教訓(錯誤、根因、修復方法、預防措施) | +| **Manage** | 封存與還原實體 | +| **Settings** | LLM 提供者設定、語言選擇 | + +--- + ## 智慧功能 **🧠 智慧搜尋** — 搜尋「login security」就能找到關於「OAuth PKCE」的記憶。MeMesh 使用你設定的 LLM 將查詢擴展為相關詞彙。 @@ -183,7 +202,7 @@ memesh # 開啟儀表板 → 設定頁籤 --- -## 全部 6 個記憶工具 +## 全部 8 個記憶工具 | 工具 | 功能說明 | |------|-------------| @@ -193,6 +212,8 @@ memesh # 開啟儀表板 → 設定頁籤 | `consolidate` | LLM 驅動的冗長記憶壓縮 | | `export` | 將記憶以 JSON 格式分享給其他專案或團隊成員 | | `import` | 匯入記憶,支援合併策略(跳過 / 覆寫 / 附加) | +| `learn` | 記錄來自錯誤的結構化教訓(錯誤、根因、修復方法、預防措施) | +| `user_patterns` | 分析你的工作模式 — 時程、工具、強項、學習方向 | --- @@ -201,7 +222,7 @@ memesh # 開啟儀表板 → 設定頁籤 ``` ┌─────────────────┐ │ Core Engine │ - │ (6 operations) │ + │ (8 operations) │ └────────┬────────┘ ┌─────────────────┼─────────────────┐ │ │ │ @@ -222,7 +243,7 @@ memesh # 開啟儀表板 → 設定頁籤 ```bash git clone https://github.com/PCIRCLE-AI/memesh-llm-memory cd memesh-llm-memory && npm install && npm run build -npm test -- --run # 289 tests +npm test -- --run # 413 tests ``` 儀表板:`cd dashboard && npm install && npm run dev` diff --git a/dashboard/src/components/AnalyticsTab.tsx b/dashboard/src/components/AnalyticsTab.tsx index 28d42c9a..8d771069 100644 --- a/dashboard/src/components/AnalyticsTab.tsx +++ b/dashboard/src/components/AnalyticsTab.tsx @@ -3,6 +3,7 @@ import { api, type StatsData, type AnalyticsData, type PatternsData } from '../l import { HealthScore } from './HealthScore'; import { MemoryTimeline } from './MemoryTimeline'; import { ValueMetrics } from './ValueMetrics'; +import { RecallEffectiveness } from './RecallEffectiveness'; import { CleanupSuggestions } from './CleanupSuggestions'; import { UserPatterns } from './UserPatterns'; import { t } from '../lib/i18n'; @@ -65,7 +66,14 @@ export function AnalyticsTab() { )} - {/* Row 5: Cleanup Suggestions */} + {/* Row 5: Recall Effectiveness */} + {analytics?.recallEffectiveness && ( +
+ +
+ )} + + {/* Row 6: Cleanup Suggestions */} {analytics && (
+
+
+
= 50 ? 'var(--accent)' : 'var(--warning, #f59e0b)' }}>{pct}%
+
{t('recall.effectiveness')}
+
+
+
{totalHits}
+
{t('recall.hits')}
+
+
+
{totalMisses}
+
{t('recall.misses')}
+
+
+
{trackedEntities}
+
{t('recall.tracked')}
+
+
+ + {total > 0 && ( +
+
{t('recall.hitRate')}
+ + {/* Overall bar */} +
+
+
= 50 ? 'rgba(0, 214, 180, 0.7)' : 'rgba(245, 158, 11, 0.7)', + transition: 'width 600ms ease-out', + }} + /> +
+
+ {totalHits} {t('recall.used')} / {total} {t('recall.injected')} +
+
+ + {/* Top effective */} + {topEffective.length > 0 && ( +
+
{t('recall.mostEffective')}
+ {topEffective.slice(0, 3).map((e) => ( + + ))} +
+ )} + + {/* Most ignored */} + {mostIgnored.length > 0 && mostIgnored[0].hitRate < 0.5 && ( +
+
{t('recall.leastEffective')}
+ {mostIgnored.slice(0, 3).map((e) => ( + + ))} +
+ )} +
+ )} + + {total === 0 && ( +
+
+ {t('recall.noData')} +
+
+ )} +
+ ); +} + +function EntityBar({ entry, color }: { entry: EffectivenessEntry; color: string }) { + const pct = Math.round(entry.hitRate * 100); + return ( +
+
+ + {entry.name} + + + {pct}% ({entry.hits}h/{entry.misses}m) + +
+
+
+
+
+ ); +} diff --git a/dashboard/src/lib/api.ts b/dashboard/src/lib/api.ts index b45d582a..70eeafff 100644 --- a/dashboard/src/lib/api.ts +++ b/dashboard/src/lib/api.ts @@ -83,6 +83,14 @@ export interface AnalyticsData { lessonCount: number; typeDistribution: Array<{ type: string; count: number }>; }; + recallEffectiveness: { + overallHitRate: number; + totalHits: number; + totalMisses: number; + trackedEntities: number; + topEffective: Array<{ name: string; type: string; hits: number; misses: number; hitRate: number }>; + mostIgnored: Array<{ name: string; type: string; hits: number; misses: number; hitRate: number }>; + } | null; cleanup: { staleEntities: Array<{ id: number; name: string; type: string; confidence: number; days_unused: number; diff --git a/dashboard/src/lib/i18n.ts b/dashboard/src/lib/i18n.ts index d4c4fd49..4e6c68f3 100644 --- a/dashboard/src/lib/i18n.ts +++ b/dashboard/src/lib/i18n.ts @@ -115,6 +115,16 @@ const translations: Record> = { 'patterns.focus': 'Focus Areas', 'patterns.strengths': 'Strengths', 'patterns.learning': 'Learning Areas', + 'recall.effectiveness': 'Effectiveness', + 'recall.hits': 'Hits', + 'recall.misses': 'Misses', + 'recall.tracked': 'Tracked', + 'recall.hitRate': 'Recall Hit Rate', + 'recall.used': 'used', + 'recall.injected': 'injected', + 'recall.mostEffective': 'Most Effective', + 'recall.leastEffective': 'Least Effective', + 'recall.noData': 'No recall data yet. Effectiveness tracking starts after your next session.', 'common.error': 'Error', 'common.loading': 'Loading…', }, @@ -232,6 +242,16 @@ const translations: Record> = { 'patterns.focus': '關注領域', 'patterns.strengths': '強項', 'patterns.learning': '學習領域', + 'recall.effectiveness': '有效性', + 'recall.hits': '命中', + 'recall.misses': '未命中', + 'recall.tracked': '追蹤中', + 'recall.hitRate': '記憶命中率', + 'recall.used': '已使用', + 'recall.injected': '已注入', + 'recall.mostEffective': '最有效', + 'recall.leastEffective': '最低效', + 'recall.noData': '尚無召回數據。下次會話後開始追蹤有效性。', 'common.error': '錯誤', 'common.loading': '載入中…', }, @@ -349,6 +369,16 @@ const translations: Record> = { 'patterns.focus': '关注领域', 'patterns.strengths': '强项', 'patterns.learning': '学习领域', + 'recall.effectiveness': '有效性', + 'recall.hits': '命中', + 'recall.misses': '未命中', + 'recall.tracked': '追踪中', + 'recall.hitRate': '记忆命中率', + 'recall.used': '已使用', + 'recall.injected': '已注入', + 'recall.mostEffective': '最有效', + 'recall.leastEffective': '最低效', + 'recall.noData': '暂无召回数据。下次会话后开始追踪有效性。', 'common.error': '错误', 'common.loading': '加载中…', }, @@ -465,6 +495,16 @@ const translations: Record> = { 'patterns.focus': 'フォーカスエリア', 'patterns.strengths': '強み', 'patterns.learning': '学習エリア', + 'recall.effectiveness': '有効性', + 'recall.hits': 'ヒット', + 'recall.misses': 'ミス', + 'recall.tracked': '追跡中', + 'recall.hitRate': 'リコール命中率', + 'recall.used': '使用済み', + 'recall.injected': '注入済み', + 'recall.mostEffective': '最も効果的', + 'recall.leastEffective': '最も非効果的', + 'recall.noData': 'リコールデータはまだありません。次のセッション後に追跡が開始されます。', 'common.error': 'エラー', 'common.loading': '読み込み中…', }, @@ -581,6 +621,16 @@ const translations: Record> = { 'patterns.focus': '집중 영역', 'patterns.strengths': '강점', 'patterns.learning': '학습 영역', + 'recall.effectiveness': '효과', + 'recall.hits': '적중', + 'recall.misses': '미적중', + 'recall.tracked': '추적 중', + 'recall.hitRate': '리콜 적중률', + 'recall.used': '사용됨', + 'recall.injected': '주입됨', + 'recall.mostEffective': '가장 효과적', + 'recall.leastEffective': '가장 비효과적', + 'recall.noData': '아직 리콜 데이터가 없습니다. 다음 세션 후 추적이 시작됩니다.', 'common.error': '오류', 'common.loading': '로딩 중…', }, @@ -697,6 +747,16 @@ const translations: Record> = { 'patterns.focus': 'Áreas de Foco', 'patterns.strengths': 'Pontos Fortes', 'patterns.learning': 'Áreas de Aprendizado', + 'recall.effectiveness': 'Eficácia', + 'recall.hits': 'Acertos', + 'recall.misses': 'Erros', + 'recall.tracked': 'Rastreados', + 'recall.hitRate': 'Taxa de Acerto', + 'recall.used': 'usadas', + 'recall.injected': 'injetadas', + 'recall.mostEffective': 'Mais Eficazes', + 'recall.leastEffective': 'Menos Eficazes', + 'recall.noData': 'Ainda sem dados de recall. O rastreamento começa após a próxima sessão.', 'common.error': 'Erro', 'common.loading': 'Carregando…', }, @@ -813,6 +873,16 @@ const translations: Record> = { 'patterns.focus': 'Domaines de focus', 'patterns.strengths': 'Points forts', 'patterns.learning': 'Domaines d\'apprentissage', + 'recall.effectiveness': 'Efficacité', + 'recall.hits': 'Succès', + 'recall.misses': 'Échecs', + 'recall.tracked': 'Suivis', + 'recall.hitRate': 'Taux de succès', + 'recall.used': 'utilisées', + 'recall.injected': 'injectées', + 'recall.mostEffective': 'Plus efficaces', + 'recall.leastEffective': 'Moins efficaces', + 'recall.noData': 'Pas encore de données. Le suivi commence après la prochaine session.', 'common.error': 'Erreur', 'common.loading': 'Chargement…', }, @@ -929,6 +999,16 @@ const translations: Record> = { 'patterns.focus': 'Schwerpunkte', 'patterns.strengths': 'Stärken', 'patterns.learning': 'Lernbereiche', + 'recall.effectiveness': 'Effektivität', + 'recall.hits': 'Treffer', + 'recall.misses': 'Fehltreffer', + 'recall.tracked': 'Verfolgt', + 'recall.hitRate': 'Trefferquote', + 'recall.used': 'verwendet', + 'recall.injected': 'injiziert', + 'recall.mostEffective': 'Am effektivsten', + 'recall.leastEffective': 'Am wenigsten effektiv', + 'recall.noData': 'Noch keine Abrufdaten. Die Verfolgung beginnt nach der nächsten Sitzung.', 'common.error': 'Fehler', 'common.loading': 'Laden…', }, @@ -1045,6 +1125,16 @@ const translations: Record> = { 'patterns.focus': 'Lĩnh vực tập trung', 'patterns.strengths': 'Điểm mạnh', 'patterns.learning': 'Lĩnh vực học tập', + 'recall.effectiveness': 'Hiệu quả', + 'recall.hits': 'Trúng', + 'recall.misses': 'Trượt', + 'recall.tracked': 'Theo dõi', + 'recall.hitRate': 'Tỷ lệ trúng', + 'recall.used': 'đã dùng', + 'recall.injected': 'đã tiêm', + 'recall.mostEffective': 'Hiệu quả nhất', + 'recall.leastEffective': 'Kém hiệu quả nhất', + 'recall.noData': 'Chưa có dữ liệu. Theo dõi sẽ bắt đầu sau phiên tiếp theo.', 'common.error': 'Lỗi', 'common.loading': 'Đang tải…', }, @@ -1161,6 +1251,16 @@ const translations: Record> = { 'patterns.focus': 'Áreas de enfoque', 'patterns.strengths': 'Fortalezas', 'patterns.learning': 'Áreas de aprendizaje', + 'recall.effectiveness': 'Eficacia', + 'recall.hits': 'Aciertos', + 'recall.misses': 'Fallos', + 'recall.tracked': 'Rastreados', + 'recall.hitRate': 'Tasa de acierto', + 'recall.used': 'usadas', + 'recall.injected': 'inyectadas', + 'recall.mostEffective': 'Más eficaces', + 'recall.leastEffective': 'Menos eficaces', + 'recall.noData': 'Aún sin datos. El seguimiento comienza después de la próxima sesión.', 'common.error': 'Error', 'common.loading': 'Cargando…', }, @@ -1277,6 +1377,16 @@ const translations: Record> = { 'patterns.focus': 'พื้นที่โฟกัส', 'patterns.strengths': 'จุดแข็ง', 'patterns.learning': 'พื้นที่การเรียนรู้', + 'recall.effectiveness': 'ประสิทธิผล', + 'recall.hits': 'ตรง', + 'recall.misses': 'พลาด', + 'recall.tracked': 'ติดตาม', + 'recall.hitRate': 'อัตราการเรียกคืน', + 'recall.used': 'ใช้แล้ว', + 'recall.injected': 'ฉีดแล้ว', + 'recall.mostEffective': 'มีประสิทธิภาพมากที่สุด', + 'recall.leastEffective': 'มีประสิทธิภาพน้อยที่สุด', + 'recall.noData': 'ยังไม่มีข้อมูล การติดตามจะเริ่มหลังเซสชันถัดไป', 'common.error': 'ข้อผิดพลาด', 'common.loading': 'กำลังโหลด…', }, diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b3b660f7..aefc6108 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -61,6 +61,7 @@ src/ │ ├── failure-analyzer.ts # LLM-powered failure analysis → StructuredLesson │ ├── lesson-engine.ts # Structured lesson creation, upsert, project query │ ├── embedder.ts # Neural embeddings (Xenova/all-MiniLM-L6-v2, 384-dim) +│ ├── auto-tagger.ts # LLM-powered auto-tag generation (fire-and-forget) │ ├── patterns.ts # User work patterns computation (shared by MCP + HTTP) │ └── version-check.ts # npm registry version check ├── db.ts # SQLite + FTS5 + sqlite-vec + migrations @@ -261,20 +262,27 @@ Foreign key cascades: deleting an entity automatically deletes its observations, Hooks are defined in `hooks/hooks.json` and executed by Claude Code at specific lifecycle events. -### Hook Scripts (4 hooks) +### Hook Scripts (5 hooks) | Hook | Event | Purpose | |------|-------|---------| -| session-start.js | SessionStart | Auto-recall project memories | +| pre-edit-recall.js | PreToolUse (Edit/Write) | Continuous recall: inject relevant memories when editing files | +| session-start.js | SessionStart | Auto-recall + record injected IDs + noise compression | | post-commit.js | PostToolUse (Bash) | Record git commits with diff stats | -| session-summary.js | Stop | Auto-capture session knowledge | +| session-summary.js | Stop | Auto-capture session knowledge + recall effectiveness tracking | | pre-compact.js | PreCompact | Save knowledge before compaction | +### Pre-Edit Recall (`scripts/hooks/pre-edit-recall.js`) + +- **Trigger**: `PreToolUse` event on `Edit` and `Write` tools +- **Matcher**: `Edit|Write` +- **Behavior**: Reads the file path from tool input, queries MeMesh for entities tagged with the file name or matching via FTS5 search. Returns relevant memories as additional context. Throttled to max 1 recall per file per session via temp file (`~/.memesh/session-recalled-files.json`). Timeout: 5 seconds. + ### Session Start (`scripts/hooks/session-start.js`) - **Trigger**: `SessionStart` event (every new Claude Code session) - **Matcher**: `*` (all sessions) -- **Behavior**: Opens the database, queries recent entities tagged with the current project, outputs a summary for Claude to use as context. Also shows proactive warnings for known `lesson_learned` entities matching the current project +- **Behavior**: Opens the database, queries recent entities tagged with the current project, outputs a summary for Claude to use as context. Also shows proactive warnings for known `lesson_learned` entities matching the current project. Records injected entity IDs to `~/.memesh/last-session-injected.json` for recall effectiveness tracking. After output, runs `compressWeeklyNoise()` (throttled to once per 24h) to archive old auto-tracked noise into weekly summaries ### Post Commit (`scripts/hooks/post-commit.js`) @@ -286,7 +294,7 @@ Hooks are defined in `hooks/hooks.json` and executed by Claude Code at specific - **Trigger**: `Stop` event (when Claude finishes responding) - **Matcher**: `*` (all sessions) -- **Behavior**: Extracts session knowledge (files edited, errors fixed, decisions made) and stores it as entities in the knowledge graph. When LLM is configured (Level 1), additionally runs failure analysis to create structured `lesson_learned` entities from session errors. Opt-out via `MEMESH_AUTO_CAPTURE=false` +- **Behavior**: Extracts session knowledge (files edited, errors fixed, decisions made) and stores it as entities in the knowledge graph. When LLM is configured (Level 1), additionally runs failure analysis to create structured `lesson_learned` entities from session errors. Also reads `~/.memesh/last-session-injected.json` to track recall effectiveness — updates `recall_hits` (entity name found in transcript) or `recall_misses` (not found). Opt-out via `MEMESH_AUTO_CAPTURE=false` ### Pre-Compact (`scripts/hooks/pre-compact.js`) diff --git a/docs/demo/index.html b/docs/demo/index.html new file mode 100644 index 00000000..0aecf51e --- /dev/null +++ b/docs/demo/index.html @@ -0,0 +1,309 @@ + + + + + +MeMesh Dashboard Demo + + + + + + + + + + + +
+
+

MeMesh LLM Memory

+ Universal AI Memory Layer +
+
+ Connected + v3.2.1 · 1,247 memories +
+
+ + + + + +
+ + +
+
1,247
Total Memories
+
5,891
Knowledge Facts
+
842
Connections
+
146
Topics
+
+ + +
+
Memory Health
+
+
+
78
+ Good +
+
+
+
Activity65%
+
+
+
+
Quality85%
+
+
+
+
Freshness70%
+
+
+
+
Self-Improvement100%
+
+
+
+
+
+ + +
+
12,450
Total Recalls
+
23
Lessons Learned
+
8
Lessons Applied
+
+ + +
+
Knowledge Coverage
+
decision120 (35%)
+
pattern85 (25%)
+
lesson_learned45 (13%)
+
concept35 (10%)
+
feature30 (9%)
+
bug_fix28 (8%)
+
+ + +
+
+ + Your memory is clean. No stale or duplicate entries found. +
+
+ + +
+
Your Patterns
+ + +
+ +
+
+ Peak hours: 10:00, 14:00, 16:00 · Busiest days: Tue, Thu +
+
+ + +
+ +
+ Bash (342) + Read (289) + Edit (215) + Grep (178) + Write (134) +
+
+ + +
+ +
+
22m
Avg Session
+
1.2
Commits / Session
+
+
+ + +
+ +
+ typescript (45) + api-design (38) + testing (32) + refactoring (28) + security (21) + performance (18) + ci-cd (15) + documentation (12) +
+
+
+ +
+ + + + + + + diff --git a/docs/images/dashboard-analytics.png b/docs/images/dashboard-analytics.png index 76d37f17..5c88d83b 100644 Binary files a/docs/images/dashboard-analytics.png and b/docs/images/dashboard-analytics.png differ diff --git a/docs/images/dashboard-graph.png b/docs/images/dashboard-graph.png new file mode 100644 index 00000000..c58c9f28 Binary files /dev/null and b/docs/images/dashboard-graph.png differ diff --git a/docs/images/dashboard-search.png b/docs/images/dashboard-search.png index 0727298b..afcb2320 100644 Binary files a/docs/images/dashboard-search.png and b/docs/images/dashboard-search.png differ diff --git a/docs/plans/2026-04-19-v4-ai-native-improvements.md b/docs/plans/2026-04-19-v4-ai-native-improvements.md new file mode 100644 index 00000000..3464ac4c --- /dev/null +++ b/docs/plans/2026-04-19-v4-ai-native-improvements.md @@ -0,0 +1,186 @@ +# MeMesh v4.0 — AI-Native Memory Improvements + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Context:** +- Repository: memesh-llm-memory +- Branch: develop +- Created: 2026-04-19 + +**Goal:** Transform MeMesh from a "memory database" into "cognitive middleware" — memory that auto-injects, auto-captures, auto-improves. + +**Architecture:** 6 features, each independent. Can ship incrementally. + +**Completeness Target:** 8/10 + +--- + +## Feature 1: Recall Effectiveness Tracking + +**Why:** MeMesh can't prove it's useful. No data on whether recalled memories actually help the AI. + +**What:** Track when a recalled memory is "used" (appeared in AI's response or influenced a tool call) vs "ignored". + +**Implementation:** +- Add `recall_hits` and `recall_misses` columns to entities table (migration in db.ts) +- When SessionStart hook injects memories, record which entities were injected +- When Stop hook captures session summary, check if injected entities' keywords appear in the session output +- If yes → increment recall_hits. If no → increment recall_misses. +- Add hit_rate to /v1/analytics response +- Dashboard: show "Memory Effectiveness: N%" in Analytics + +**Files:** +- Modify: `src/db.ts` (migration) +- Modify: `scripts/hooks/session-start.js` (record injected entities) +- Modify: `scripts/hooks/session-summary.js` (check usage, update hits/misses) +- Modify: `src/transports/http/server.ts` (/v1/analytics add hit_rate) +- Modify: `dashboard/src/components/AnalyticsTab.tsx` (show effectiveness) + +**Tests:** 3 new tests for hit/miss tracking logic + +--- + +## Feature 2: Continuous Recall via PreToolUse Hook + +**Why:** Current recall is one-shot at session start. AI forgets about relevant memories mid-conversation. + +**What:** Add a PreToolUse hook on Edit/Write that checks if the file being edited has relevant memories. If yes, inject them as context. + +**Implementation:** +- New hook: `scripts/hooks/pre-edit-recall.js` +- Triggers on Edit and Write tool use +- Reads the file path from stdin JSON (`tool_input.file_path`) +- Queries MeMesh for entities tagged with that file path or related keywords +- Returns `hookSpecificOutput.additionalContext` with relevant memories +- Throttle: max 1 recall per file per session (cache in memory) + +**Files:** +- Create: `scripts/hooks/pre-edit-recall.js` +- Modify: `hooks/hooks.json` (add PreToolUse matcher for Edit|Write) +- Modify: `scripts/smoke-packed-artifact.mjs` (add new hook to required files) + +**Tests:** 2 tests for the hook logic + +--- + +## Feature 3: BYOK Embedding + +**Why:** all-MiniLM-L6-v2 is old, English-biased, 384-dim. Users with BYOK can get better embeddings. + +**What:** When LLM config has an API key, use the provider's embedding API instead of local ONNX. + +**Implementation:** +- Modify `src/core/embedder.ts`: + - Add `embedWithProvider(text, config)` function + - If config.llm.provider === 'anthropic': use Voyager-3-lite (1024-dim) + - If config.llm.provider === 'openai': use text-embedding-3-small (1536-dim) + - Fallback: existing @xenova/transformers (384-dim) +- Handle different vector dimensions: sqlite-vec supports variable dimensions +- Migration: if switching providers, re-embed all entities (background task) + +**Files:** +- Modify: `src/core/embedder.ts` +- Modify: `src/core/config.ts` (embedding provider selection) +- Modify: `src/db.ts` (handle variable vector dimensions) + +**Tests:** 4 tests (provider selection, fallback, dimension handling) + +--- + +## Feature 4: Auto-Tagging with LLM + +**Why:** Users forget to tag. Untagged memories are harder to find and analyze. + +**What:** When `remember()` is called without tags, use LLM to generate relevant tags from the entity name + observations. + +**Implementation:** +- Add `autoTag(name, type, observations, existingTags): Promise` to `src/core/operations.ts` +- Uses LLM to generate 2-5 tags (project, topic, technology categories) +- Only runs if: LLM is configured AND entity has 0 user-provided tags +- Prompt: "Given this memory entity, suggest 2-5 tags. Categories: project:X, topic:X, tech:X, severity:X" +- Fire-and-forget: don't block remember() on LLM response + +**Files:** +- Modify: `src/core/operations.ts` (add autoTag call in remember) +- Create: `src/core/auto-tagger.ts` (LLM prompt + parsing) + +**Tests:** 3 tests (tag generation, no-LLM fallback, existing tags skip) + +--- + +## Feature 5: Noise Filter (Auto-compress session_keypoint/commit) + +**Why:** 90% of entities are auto-tracked noise. Signal-to-noise ratio is terrible. + +**What:** Auto-compress old session_keypoint and commit entities into weekly summaries. + +**Implementation:** +- New function: `compressWeeklyNoise(db, weekDate)` in `src/core/lifecycle.ts` +- Runs on session-start hook (throttled to once per day) +- Groups session_keypoint entities older than 7 days by ISO week +- Creates one summary entity per week: "Week of 2026-04-14: 45 sessions, 12 commits, main focus: dashboard redesign" +- Archives the individual session_keypoint entities +- Preserves: decisions, patterns, lessons, bug_fixes (never compressed) +- Threshold: only compress if > 20 session_keypoints exist for that week + +**Files:** +- Modify: `src/core/lifecycle.ts` (add compressWeeklyNoise) +- Modify: `scripts/hooks/session-start.js` (call compression on startup) +- Modify: `src/core/operations.ts` (export compressWeeklyNoise) + +**Tests:** 4 tests (compression logic, preservation of important types, threshold, weekly grouping) + +--- + +## Feature 6: Memory Impact Score (Learning-to-Rank) + +**Why:** Current scoring is static formula. No learning from actual usage. + +**What:** Add a `impact_score` that adjusts based on recall effectiveness data from Feature 1. + +**Implementation:** +- After Feature 1 ships (depends on recall tracking data) +- New scoring factor in `src/core/scoring.ts`: `impactWeight` +- Formula: `impact = recall_hits / (recall_hits + recall_misses)` (smoothed with Laplace) +- Integrate into existing multi-factor scoring: add impact as 6th factor (10% weight, reduce others proportionally) +- Entities with high hit rate rise in search results. Ignored entities fade. + +**Files:** +- Modify: `src/core/scoring.ts` (add impact factor) +- Modify: `src/core/types.ts` (add impact fields to ScoringResult) + +**Tests:** 3 tests (impact calculation, smoothing, integration with existing scoring) + +--- + +## Execution Order + +``` +Feature 1 (recall tracking) — foundation, no dependencies + ↓ +Feature 5 (noise filter) — independent, high impact on data quality + ↓ +Feature 4 (auto-tagging) — independent, improves data quality + ↓ +Feature 2 (continuous recall) — independent, high UX impact + ↓ +Feature 3 (BYOK embedding) — independent, quality improvement + ↓ +Feature 6 (impact score) — depends on Feature 1 data +``` + +Features 1-5 can be done in any order. Feature 6 needs Feature 1's data first. + +--- + +## CEO/PM Items (Status) + +| Item | Status | +|------|--------| +| CHANGELOG.md | DONE | +| PyPI badge removed | DONE | +| User stories in README | DONE | +| 10 translated READMEs updated | DONE | +| Static demo page | DONE | +| Skills updated (8 tools, proactive) | DONE | +| Repo name note | DONE (in CHANGELOG footer) | diff --git a/docs/plans/2026-04-20-v4-adversarial-findings.md b/docs/plans/2026-04-20-v4-adversarial-findings.md new file mode 100644 index 00000000..85704cd2 --- /dev/null +++ b/docs/plans/2026-04-20-v4-adversarial-findings.md @@ -0,0 +1,81 @@ +# v4.0.0 Adversarial Review Findings + +> Cross-model review (Claude adversarial + Codex). Fix all before release. + +## MUST FIX (HIGH, both models agree) + +### 1. Noise filter cutoff missing in 2nd query +**File:** `src/core/lifecycle.ts:152-158` +**Problem:** First query filters by `created_at < cutoff` but second query fetching entity IDs does NOT include the cutoff. Recent entities in the same week bucket get archived prematurely. +**Fix:** Add `AND e.created_at < ?` to the second query with same cutoff parameter. + +### 2. Pre-edit recall throttle never cleared between sessions +**File:** `scripts/hooks/pre-edit-recall.js:14` +**Problem:** `session-recalled-files.json` persists across sessions. After first session, commonly edited files are already "seen" and won't get recalled. +**Fix:** Clear the throttle file in `session-start.js`, or add session timestamp and check staleness. + +### 3. API key fallback sends wrong key to wrong provider +**File:** `src/core/auto-tagger.ts:77` +**Problem:** `config.apiKey || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY` — if provider is 'openai' but ANTHROPIC_API_KEY is set first, the Anthropic key gets sent to OpenAI. +**Fix:** Use provider-specific env var lookup. + +### 4. NULL hitRate in analytics per-entity queries +**File:** `src/transports/http/server.ts:458-461` +**Problem:** Per-entity queries don't use COALESCE — entities from before migration have NULL values producing NULL hitRate. +**Fix:** Use `COALESCE(recall_hits, 0)` in per-entity queries. + +### 5. FTS5 injection in pre-edit-recall hook +**File:** `scripts/hooks/pre-edit-recall.js:88-97` +**Problem:** File name passed directly to FTS5 MATCH without sanitization. Files named `"OR *"` cause unexpected behavior. +**Fix:** Wrap in double quotes like knowledge-graph.ts does. + +## SHOULD FIX (MEDIUM) + +### 6. ONNX pipeline load failure is permanent +**File:** `src/core/embedder.ts:169-189` +**Problem:** On failure, `onnxPipelineLoading` remains a rejected promise. All subsequent calls fail forever. +**Fix:** Reset `onnxPipelineLoading = null` on failure. + +### 7. WAL pragma on read-only connection +**File:** `scripts/hooks/pre-edit-recall.js:53` +**Problem:** Setting WAL on readonly DB is a no-op or error on some platforms. +**Fix:** Remove the pragma call. + +### 8. Summary entities not indexed in FTS5 +**File:** `src/core/lifecycle.ts:173-192` +**Problem:** `compressWeeklyNoise` inserts directly, bypassing FTS5 index. +**Fix:** Insert into `entities_fts` after creating summary entity. + +### 9. ISO week %W vs %V +**File:** `src/core/lifecycle.ts:137` +**Problem:** `%W` is not ISO week. Comment says ISO but implementation isn't. +**Fix:** Use correct format or update comment. + +### 10. .passthrough() on config endpoint +**File:** `src/transports/http/server.ts:239` +**Problem:** Allows arbitrary keys written to config.json. +**Fix:** Remove `.passthrough()`. + +### 11. Noise compression skips updating existing summary +**File:** `src/core/lifecycle.ts:170` +**Problem:** If `weekly-summary-{week}` already exists, new entities crossing cutoff get archived but summary isn't updated. +**Fix:** Append observations to existing summary. + +### 12. BYOK dimension drop — add warning +**File:** `src/db.ts:134` +**Problem:** Silent DROP TABLE on dimension change. +**Fix:** Log warning to stderr before dropping. + +## KNOWN LIMITATIONS (INVESTIGATE, defer to v4.1) + +### 13. Recall name matching (transcript poisoning) +Session-start injects entity names into transcript, then stop hook searches transcript for those names. False positives guaranteed. Needs architectural rethink — match only against user/assistant messages, not hook output. + +### 14. Race condition on last-session-injected.json +Multiple sessions overwrite each other. Fix: session-ID-based filenames. Low practical impact (rare concurrent sessions). + +### 15. Ollama model vs embedding model +Shared `config.model` used for both chat and embedding. Fix: separate `embeddingModel` config field. + +### 16. Fire-and-forget DB writes race against shutdown +`autoTagAndApply` and `embedAndStore` may write after `closeDatabase()`. Fundamental design choice. diff --git a/hooks/hooks.json b/hooks/hooks.json index 50d5b184..b2c8540f 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -1,5 +1,17 @@ { "hooks": { + "PreToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/hooks/pre-edit-recall.js", + "timeout": 5 + } + ] + } + ], "SessionStart": [ { "matcher": "*", diff --git a/package.json b/package.json index ee58940a..3d39be6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pcircle/memesh", - "version": "3.2.1", + "version": "4.0.0", "description": "MeMesh — The lightest universal AI memory layer. One SQLite file, any LLM, zero cloud.", "main": "dist/index.js", "type": "module", diff --git a/plugin.json b/plugin.json index f1d67b77..dab841c0 100644 --- a/plugin.json +++ b/plugin.json @@ -4,7 +4,7 @@ "author": { "name": "PCIRCLE AI" }, - "version": "3.2.1", + "version": "4.0.0", "homepage": "https://github.com/PCIRCLE-AI/memesh-llm-memory", "repository": "https://github.com/PCIRCLE-AI/memesh-llm-memory", "license": "MIT", diff --git a/scripts/hooks/pre-edit-recall.js b/scripts/hooks/pre-edit-recall.js new file mode 100644 index 00000000..72b72325 --- /dev/null +++ b/scripts/hooks/pre-edit-recall.js @@ -0,0 +1,163 @@ +#!/usr/bin/env node + +// Continuous Recall — PreToolUse hook for Edit/Write +// When editing a file, checks if MeMesh has relevant memories +// and injects them as context. Throttled: max 1 recall per file per session. + +import { createRequire } from 'module'; +import { homedir } from 'os'; +import { join, basename, dirname } from 'path'; +import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; + +const require = createRequire(import.meta.url); + +const THROTTLE_FILE = join(homedir(), '.memesh', 'session-recalled-files.json'); +const MAX_RESULTS = 3; + +let input = ''; +process.stdin.setEncoding('utf8'); +process.stdin.on('data', (chunk) => { input += chunk; }); +process.stdin.on('end', () => { + try { + const data = JSON.parse(input); + const toolInput = data.tool_input || {}; + const filePath = toolInput.file_path || toolInput.path || ''; + + // Only process if we have a file path + if (!filePath || typeof filePath !== 'string') { + return pass(); + } + + // Throttle: skip if we already recalled for this file + const fileKey = filePath.toLowerCase(); + let seenFiles = []; + try { + if (existsSync(THROTTLE_FILE)) { + const raw = JSON.parse(readFileSync(THROTTLE_FILE, 'utf8')); + seenFiles = Array.isArray(raw) ? raw : []; + } + } catch { + seenFiles = []; + } + + if (seenFiles.includes(fileKey)) { + return pass(); + } + + // Find database + const dbPath = process.env.MEMESH_DB_PATH || join(homedir(), '.memesh', 'knowledge-graph.db'); + if (!existsSync(dbPath)) return pass(); + + const Database = require('better-sqlite3'); + const db = new Database(dbPath, { readonly: true }); + try { + + // Check if entities table exists + const tableCheck = db.prepare( + "SELECT name FROM sqlite_master WHERE type='table' AND name='entities'" + ).get(); + if (!tableCheck) return pass(); + + const hasStatus = db.prepare("PRAGMA table_info(entities)").all() + .some(c => c.name === 'status'); + const statusFilter = hasStatus ? "AND e.status = 'active'" : ''; + + // Search strategies: + // 1. Entities tagged with the file's basename (e.g., "file:auth.ts") + // 2. Entities with name matching the file's basename (without extension) + // 3. FTS5 search on the basename (without extension) + const fileName = basename(filePath); + const fileNameNoExt = fileName.replace(/\.[^.]+$/, ''); + const dirName = basename(dirname(filePath)); + + const results = []; + + // Strategy 1: Tag-based search (file:name or mentions of the file) + const tagResults = db.prepare(` + SELECT DISTINCT e.id, e.name, e.type + FROM entities e + JOIN tags t ON t.entity_id = e.id + WHERE (t.tag = ? OR t.tag = ?) + ${statusFilter} + LIMIT ? + `).all(`file:${fileName}`, `file:${fileNameNoExt}`, MAX_RESULTS); + results.push(...tagResults); + + // Strategy 2: FTS5 search on file name (if not enough results) + if (results.length < MAX_RESULTS && fileNameNoExt.length >= 4) { + try { + const ftsResults = db.prepare(` + SELECT DISTINCT e.id, e.name, e.type + FROM entities e + JOIN entities_fts fts ON fts.rowid = e.id + WHERE entities_fts MATCH ? + ${statusFilter} + LIMIT ? + `).all('"' + fileNameNoExt.replace(/"/g, '""') + '"', MAX_RESULTS - results.length); + // Deduplicate + for (const r of ftsResults) { + if (!results.some(existing => existing.id === r.id)) { + results.push(r); + } + } + } catch { + // FTS query failed — skip silently + } + } + + if (results.length === 0) { + // Record as seen even with no results (avoid re-querying) + recordSeen(seenFiles, fileKey); + return pass(); + } + + // Fetch first observation for each result + const getObs = db.prepare( + 'SELECT content FROM observations WHERE entity_id = ? ORDER BY id ASC LIMIT 1' + ); + + const lines = [`Relevant memories for ${fileName}:`]; + for (const r of results) { + const obs = getObs.get(r.id); + const snippet = obs ? obs.content.slice(0, 120) : ''; + lines.push(snippet + ? `• ${r.name} (${r.type}): ${snippet}` + : `• ${r.name} (${r.type})` + ); + } + + // Record as seen + recordSeen(seenFiles, fileKey); + + console.log(JSON.stringify({ + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: lines.join('\n'), + }, + })); + } finally { + db.close(); + } + } catch { + // Never crash Claude Code + pass(); + } +}); + +function pass() { + // Empty output = no additional context + process.exit(0); +} + +function recordSeen(seenFiles, fileKey) { + try { + seenFiles.push(fileKey); + // Cap at 100 to prevent unbounded growth + if (seenFiles.length > 100) seenFiles = seenFiles.slice(-50); + const memeshDir = join(homedir(), '.memesh'); + if (!existsSync(memeshDir)) mkdirSync(memeshDir, { recursive: true }); + writeFileSync(THROTTLE_FILE, JSON.stringify(seenFiles), 'utf8'); + } catch { + // Non-critical + } +} diff --git a/scripts/hooks/session-start.js b/scripts/hooks/session-start.js index 8e7b756d..8f817e8b 100755 --- a/scripts/hooks/session-start.js +++ b/scripts/hooks/session-start.js @@ -3,18 +3,30 @@ import { createRequire } from 'module'; import { homedir } from 'os'; import { join, basename } from 'path'; -import { existsSync } from 'fs'; +import { existsSync, writeFileSync, mkdirSync, unlinkSync } from 'fs'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; const require = createRequire(import.meta.url); let input = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', (chunk) => { input += chunk; }); -process.stdin.on('end', () => { +process.stdin.on('end', async () => { try { const data = JSON.parse(input); const projectName = basename(data.cwd || process.cwd()); + // Clear pre-edit recall throttle from previous session + try { + const throttlePath = join(homedir(), '.memesh', 'session-recalled-files.json'); + if (existsSync(throttlePath)) { + unlinkSync(throttlePath); + } + } catch { + // Non-critical + } + // Find database const dbPath = process.env.MEMESH_DB_PATH || join(homedir(), '.memesh', 'knowledge-graph.db'); if (!existsSync(dbPath)) { @@ -165,6 +177,27 @@ process.stdin.on('end', () => { // Lesson query failed — don't break session start } + // --- Record injected entity IDs for recall effectiveness tracking --- + try { + const allInjected = [...projectEntities, ...recentEntities]; + if (allInjected.length > 0) { + const memeshDir = join(homedir(), '.memesh'); + if (!existsSync(memeshDir)) mkdirSync(memeshDir, { recursive: true }); + writeFileSync( + join(memeshDir, 'last-session-injected.json'), + JSON.stringify({ + injectedAt: new Date().toISOString(), + project: projectName, + entityIds: allInjected.map(e => e.id), + entityNames: allInjected.map(e => e.name), + }), + 'utf8' + ); + } + } catch { + // Non-critical — don't break session start + } + const hookOutput = { suppressOutput: true, hookSpecificOutput: { @@ -176,6 +209,23 @@ process.stdin.on('end', () => { } finally { db.close(); } + // ── Noise compression (after readonly DB is closed) ────────────── + // Opens a separate read-write connection via the core module. + // Throttled to once per 24h inside compressWeeklyNoise(). + try { + const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT + || dirname(dirname(fileURLToPath(import.meta.url))); + const dbMod = await import(join(pluginRoot, 'dist/db.js')); + const lifecycleMod = await import(join(pluginRoot, 'dist/core/lifecycle.js')); + dbMod.openDatabase(); + try { + lifecycleMod.compressWeeklyNoise(dbMod.getDatabase()); + } finally { + dbMod.closeDatabase(); + } + } catch { + // Non-critical — noise compression failed, will retry next session + } } catch (err) { // Hooks must never crash Claude Code — but report honestly console.log(JSON.stringify({ systemMessage: `MeMesh: Session start failed (${err?.message || 'unknown error'}). Memories not loaded.` })); diff --git a/scripts/hooks/session-summary.js b/scripts/hooks/session-summary.js index d1fab640..5001a9f5 100755 --- a/scripts/hooks/session-summary.js +++ b/scripts/hooks/session-summary.js @@ -7,7 +7,7 @@ import { createRequire } from 'module'; import { homedir } from 'os'; import { join, basename, dirname } from 'path'; -import { existsSync, mkdirSync, readFileSync } from 'fs'; +import { existsSync, mkdirSync, readFileSync, unlinkSync } from 'fs'; import { fileURLToPath } from 'url'; const require = createRequire(import.meta.url); @@ -238,6 +238,49 @@ process.stdin.on('end', async () => { [...baseTags, 'type:heavy-session'] ); } + + // ── Recall effectiveness tracking ──────────────────────────────── + // Read which entities were injected at session start, check if + // their names appear in the transcript, update hits/misses. + try { + const injectedPath = join(homedir(), '.memesh', 'last-session-injected.json'); + if (existsSync(injectedPath)) { + const injectedData = JSON.parse(readFileSync(injectedPath, 'utf8')); + const { entityIds, entityNames } = injectedData; + + if (entityIds && entityIds.length > 0) { + // Check if recall_hits column exists (v4.0+ migration) + const colCheck = db.prepare("PRAGMA table_info(entities)").all(); + if (colCheck.some(c => c.name === 'recall_hits')) { + // Build a lowercase transcript text for matching + const transcriptText = readFileSync(transcriptPath, 'utf8').toLowerCase(); + + const updateHit = db.prepare( + 'UPDATE entities SET recall_hits = COALESCE(recall_hits, 0) + 1 WHERE id = ?' + ); + const updateMiss = db.prepare( + 'UPDATE entities SET recall_misses = COALESCE(recall_misses, 0) + 1 WHERE id = ?' + ); + + for (let i = 0; i < entityIds.length; i++) { + const name = (entityNames[i] || '').toLowerCase(); + // Skip very short names to avoid false positives + if (name.length < 4) continue; + if (transcriptText.includes(name)) { + updateHit.run(entityIds[i]); + } else { + updateMiss.run(entityIds[i]); + } + } + } + } + + // Clean up temp file + try { unlinkSync(injectedPath); } catch {} + } + } catch { + // Non-critical — don't break session summary + } } finally { db.close(); } diff --git a/scripts/smoke-packed-artifact.mjs b/scripts/smoke-packed-artifact.mjs index a87022e5..639d73e0 100644 --- a/scripts/smoke-packed-artifact.mjs +++ b/scripts/smoke-packed-artifact.mjs @@ -82,11 +82,12 @@ const requiredFiles = [ // Dist — dashboard assets 'dist/cli/view.js', 'dist/cli/assets/d3.v7.min.js', - // Hooks (4) + // Hooks (5) 'scripts/hooks/session-start.js', 'scripts/hooks/post-commit.js', 'scripts/hooks/session-summary.js', 'scripts/hooks/pre-compact.js', + 'scripts/hooks/pre-edit-recall.js', // Skills (2) 'skills/memesh/SKILL.md', 'skills/memesh-review/SKILL.md', diff --git a/skills/memesh-review/SKILL.md b/skills/memesh-review/SKILL.md index bc7fadce..1ab5ff65 100644 --- a/skills/memesh-review/SKILL.md +++ b/skills/memesh-review/SKILL.md @@ -1,6 +1,6 @@ --- name: memesh-review -description: Review and clean up the MeMesh memory database. Find stale, contradicting, or redundant memories and suggest cleanup actions. Use when asked to "review memories", "clean up knowledge", or "what's in my memory". +description: Review and optimize the MeMesh memory database. Analyzes health score, finds stale/conflicting/redundant memories, shows work patterns, and suggests cleanup actions. Use when asked to "review memories", "check memory health", "clean up knowledge", or "what's in my memory". user-invocable: true --- @@ -8,47 +8,90 @@ user-invocable: true Review the memory database and provide actionable cleanup recommendations. +## How to Access + +Use CLI (works everywhere) or MCP tools (if available). See the `memesh` skill for auto-detect instructions. + ## Process -1. **Recall recent memories** — use `recall` with no query to see what's stored -2. **Check for staleness** — identify memories not accessed in 30+ days -3. **Check for conflicts** — look for `contradicts` relations or opposing observations -4. **Check for verbosity** — entities with 5+ observations that could be consolidated -5. **Report findings** with specific actions +### Step 1: Gather data + +```bash +# Get system health +memesh status + +# Get all recent memories (structured output for analysis) +memesh recall --limit 50 --json -## Steps +# Get memories by type for quality analysis +memesh recall --tag "type:decision" --json +memesh recall --tag "type:lesson_learned" --json +memesh recall --tag "type:session_keypoint" --json +``` -### Step 1: Load overview +If MCP `user_patterns` tool is available, also run it for work pattern analysis: ```json -recall: {"limit": 50} +user_patterns: {} ``` ### Step 2: Analyze and report -Present findings in this format: +From the recalled data, compute and present: -``` -## Memory Review +```markdown +## Memory Health Report + +### Overview +- Total entities: N +- Last 30 days active: N (N%) +- Knowledge types: N decisions, N patterns, N lessons, N auto-tracked + +### Health Score: N/100 +- Activity: N% (accessed in last 30 days) +- Quality: N% (high confidence, well-tagged) +- Freshness: N% (new this week) +- Self-Improvement: N% (lessons learned ratio) -### Summary -- Total memories recalled: N -- Active: N | Archived: N +### Quality Issues Found -### Stale (not accessed in 30+ days) -- "entity-name" — last observation: "..." — Suggest: archive or keep? +**Stale (not accessed 30+ days, low confidence)** +- "entity-name" — confidence: N% — Suggest: archive? -### Could be consolidated (5+ observations) -- "entity-name" (8 observations) — Suggest: run consolidate +**Verbose (5+ observations, needs consolidation)** +- "entity-name" (N observations) — Suggest: `memesh consolidate --name "entity-name"` -### Potential conflicts -- "use-jwt" vs "no-jwt" — contradicting auth decisions +**Potential conflicts** +- "entity-A" vs "entity-B" — contradicting decisions -### Recommended actions -1. Archive "old-design" (superseded by "new-design") -2. Consolidate "auth-history" (12 observations → ~3) -3. Review conflict between "X" and "Y" +**Noise ratio** +- N% auto-tracked (session_keypoint, commit) vs N% intentional knowledge +- If noise > 80%: recommend more deliberate `memesh remember` usage + +### Recommended Actions +1. `memesh forget --name "old-design"` (superseded) +2. `memesh consolidate --name "auth-history"` (12 obs → ~3) +3. `memesh remember ...` (knowledge gap in [area]) ``` ### Step 3: Execute approved actions -After presenting the report, ask the user which actions to execute. Then use `forget`, `consolidate`, or `remember` with `supersedes` accordingly. +Present the report first. Ask which actions to execute. Then run the commands: + +```bash +memesh forget --name "outdated-entity" +memesh consolidate --name "verbose-entity" +memesh remember --name "missing-knowledge" --type decision --obs "..." +``` + +### Step 4: Verify + +```bash +memesh recall --limit 5 --json # confirm changes took effect +``` + +## Tips + +- Run every 1-2 weeks to keep memory healthy +- Health score < 50 → too many stale or low-quality memories +- Noise > 80% → encourage deliberate `memesh remember` for decisions +- Dashboard available at: http://localhost:3737/dashboard (run `memesh serve` first) diff --git a/skills/memesh/SKILL.md b/skills/memesh/SKILL.md index f7e93a43..c457953b 100644 --- a/skills/memesh/SKILL.md +++ b/skills/memesh/SKILL.md @@ -1,73 +1,131 @@ --- name: memesh -description: Use MeMesh to remember, recall, and manage AI knowledge across sessions. Triggers when the user asks to remember something, recall past decisions, forget outdated info, or manage their memory. Also triggers proactively when you make important decisions, fix bugs, or learn lessons worth preserving. +description: Use MeMesh to remember, recall, and manage AI knowledge across sessions. Triggers when the user asks to remember something, recall past decisions, forget outdated info, learn from mistakes, or analyze work patterns. Also triggers proactively when you make important decisions, fix bugs, or learn lessons worth preserving. user-invocable: true --- # MeMesh — AI Memory Management -You have access to MeMesh, a persistent memory layer with 6 tools. Use them to remember important knowledge across sessions. - -## When to Use (Proactive) - -**Remember automatically when:** -- A design decision is made ("let's use OAuth" → remember it) -- A bug is fixed (root cause + fix → remember as lesson) -- A pattern is established ("we always use Zod for validation" → remember as pattern) -- Architecture changes ("moved from monolith to microservices" → remember) -- The user explicitly says "remember this" or "don't forget" - -**Recall automatically when:** -- Starting work on a feature that might have prior decisions -- The user asks "what did we decide about X?" -- You need context about a project's conventions - -**Forget when:** -- A decision is superseded by a new one (use `supersedes` relation) -- The user says "forget about X" or "that's outdated" - -## Tools Reference - -### remember -```json -{ - "name": "auth-decision", - "type": "decision", - "observations": ["Use OAuth 2.0 with PKCE for authentication"], - "tags": ["project:myapp", "topic:auth"], - "relations": [{"to": "old-auth", "type": "supersedes"}] -} +Persistent memory layer for AI agents. Remember decisions, recall context, learn from mistakes — across sessions. + +## How to Access (auto-detect) + +``` +1. MCP tools available? (remember, recall, forget, learn in your tool list) + → YES: use MCP tools directly (fastest, structured I/O) + → NO: continue to step 2 + +2. CLI available? Run: memesh status + → Works: use CLI commands below + → "command not found": Run: npx @pcircle/memesh status + → Works: use npx @pcircle/memesh for all commands below +``` + +All examples below use CLI. MCP tools accept the same parameters as JSON objects. + +## What's Already Automatic (Claude Code Plugin Hooks) + +If MeMesh is installed as a Claude Code plugin, these happen **without any action from you**: + +| Hook | When | What it does | +|------|------|-------------| +| **SessionStart** | Every session begins | Auto-recalls top memories for current project + surfaces lesson warnings | +| **PostToolUse** | After `git commit` | Auto-tracks commit with diff stats as a memory entity | +| **Stop** | Session ends | Auto-captures session knowledge + runs LLM failure analysis → lessons | +| **PreCompact** | Before context compaction | Saves important knowledge before conversation history is compressed | + +**You do NOT need to manually:** +- Recall at session start (hook does it) +- Remember commits (hook does it) +- Summarize sessions (hook does it) + +**You DO need to manually** use the commands below for intentional knowledge management. + +## When to Use + +### Proactive triggers — do these WITHOUT being asked + +| Situation | Action | +|-----------|--------| +| Design decision made | `memesh remember --name "auth-choice" --type decision --obs "Use OAuth 2.0 with PKCE" --tags "project:myapp"` | +| Bug fixed | `memesh learn --error "what broke" --fix "what fixed it" --root-cause "why" --severity major` | +| Pattern established | `memesh remember --name "validation-pattern" --type pattern --obs "Always use Zod"` | +| Starting work on a feature | `memesh recall "feature-name" --json` | +| User asks "what did we decide?" | `memesh recall "topic" --tag "project:myapp"` | +| Info is outdated | `memesh forget --name "old-decision"` | + +### When NOT to remember +- Trivial implementation details (variable names, import paths) +- Anything that took < 5 minutes to decide +- Information already in the codebase (comments, README, config) + +## Common Scenarios + +### You just fixed a bug +```bash +memesh learn \ + --error "SIGSEGV when running vitest with threads" \ + --fix "Use pool: 'forks' instead of 'threads' for native modules" \ + --root-cause "better-sqlite3 native module is not thread-safe" \ + --prevention "Check if test framework supports native modules before choosing pool" \ + --severity major +``` +This creates a `lesson_learned` entity. Lessons are surfaced as **proactive warnings** at next session start. + +### You need context before working +```bash +memesh recall "authentication" --json +memesh recall --tag "project:myapp" --limit 10 +memesh recall --cross-project # search across all projects +``` +Results are ranked by relevance, recency, frequency, confidence, and temporal validity. + +### A decision was just made +```bash +memesh remember \ + --name "db-choice-2026" \ + --type decision \ + --obs "Use SQLite for local-first" "Rejected PostgreSQL due to deployment complexity" \ + --tags "project:myapp" "topic:database" ``` -Types: `decision`, `pattern`, `lesson`, `bug_fix`, `architecture`, `convention` +Types: `decision` `pattern` `lesson_learned` `bug_fix` `architecture` `convention` `feature` `best_practice` `concept` `tool` `note` -### recall -```json -{"query": "authentication", "tag": "project:myapp", "limit": 10} +### Old info needs updating +```bash +memesh forget --name "old-auth-approach" # archive entire entity +memesh forget --name "auth-approach" --observation "Use JWT" # remove one fact only ``` -Omit query to list recent memories. Results are ranked by relevance, recency, and access frequency. +Archives (soft-delete). Never permanently removes. -### forget -```json -{"name": "outdated-design"} +### Memories are getting verbose +```bash +memesh consolidate --name "entity-with-many-observations" +memesh consolidate --tag "project:myapp" --min-obs 5 ``` -Archives the entity (soft-delete). Add `"observation": "specific text"` to remove just one fact. +Compresses observations using LLM. Requires Smart Mode configured. -### consolidate -```json -{"name": "auth-history", "min_observations": 5} +### Backup or share memories +```bash +memesh export --tag "project:myapp" > memories.json +memesh import memories.json --merge skip # skip | overwrite | append ``` -Compresses verbose memories using LLM. Requires Smart Mode configured. -### export / import -```json -{"tag": "project:myapp"} +### Check MeMesh health +```bash +memesh status # version, search level, embeddings +memesh config list # current configuration ``` -Export memories as JSON for sharing. Import with merge strategies: `skip`, `overwrite`, `append`. + +## MCP-Only Features + +These require MCP tools or the HTTP API (`memesh serve` + REST calls): + +- **user_patterns** — Analyzes work patterns (schedule, tool preferences, strengths) from existing memories. Categories: `workSchedule`, `toolPreferences`, `strengths`, `focusAreas`. ## Best Practices 1. **Be specific** — "Use OAuth 2.0 with PKCE" not "auth stuff decided" 2. **Tag by project** — Always include `project:` tag -3. **Use relations** — Link related decisions with `related-to`, replace old ones with `supersedes` -4. **Type correctly** — Use the right type (decision/pattern/lesson) for better search -5. **Don't over-remember** — Skip trivial things. Remember decisions that took > 5 minutes to make. +3. **Use `--json`** — When you need to parse output programmatically +4. **Learn from every bug** — Every fix is a future warning. Use `learn`, not just `remember`. +5. **Don't over-remember** — Decisions that took > 5 minutes. Patterns worth preserving. Not trivia. diff --git a/src/core/auto-tagger.ts b/src/core/auto-tagger.ts new file mode 100644 index 00000000..4090f014 --- /dev/null +++ b/src/core/auto-tagger.ts @@ -0,0 +1,121 @@ +import { getDatabase } from '../db.js'; +import type { LLMConfig } from './config.js'; +import type { AnthropicResponse, OpenAIResponse, OllamaResponse } from './types.js'; + +const VALID_PREFIXES = ['project:', 'topic:', 'tech:', 'severity:', 'scope:']; + +/** + * Generate tags for an entity using LLM. + * Returns 2-5 tags in format: project:X, topic:X, tech:X. + * Returns empty array if LLM is unavailable or fails. + */ +export async function autoTag( + name: string, + type: string, + observations: string[], + llmConfig: LLMConfig +): Promise { + const prompt = `Given this memory entity, suggest 2-5 tags. Each tag must use one of these prefixes: project:, topic:, tech:, severity:, scope:. + +Entity name: ${name} +Entity type: ${type} +Facts: ${observations.slice(0, 5).join('; ')} + +Return ONLY a JSON array of tag strings, nothing else. Example: ["project:memesh", "topic:auth", "tech:sqlite"]`; + + try { + const text = await callLLM(prompt, llmConfig); + return parseTags(text); + } catch { + return []; + } +} + +/** + * Apply auto-generated tags to an existing entity. + * Fire-and-forget: caller should not await this. + */ +export async function autoTagAndApply( + entityId: number, + name: string, + type: string, + observations: string[], + llmConfig: LLMConfig +): Promise { + const tags = await autoTag(name, type, observations, llmConfig); + if (tags.length === 0) return; + + try { + const db = getDatabase(); + const insertTag = db.prepare('INSERT OR IGNORE INTO tags (entity_id, tag) VALUES (?, ?)'); + for (const tag of tags) { + insertTag.run(entityId, tag); + } + } catch { + // DB write failed — non-critical + } +} + +export function parseTags(text: string): string[] { + try { + const match = text.match(/\[[\s\S]*?\]/); + if (!match) return []; + const arr = JSON.parse(match[0]); + if (!Array.isArray(arr)) return []; + + return arr + .filter((t: unknown): t is string => typeof t === 'string') + .map((t: string) => t.toLowerCase().trim()) + .filter((t: string) => VALID_PREFIXES.some(p => t.startsWith(p))) + .slice(0, 5); + } catch { + return []; + } +} + +async function callLLM(prompt: string, config: LLMConfig): Promise { + // Use provider-specific env var to avoid sending wrong key to wrong provider + let apiKey = config.apiKey; + if (!apiKey) { + if (config.provider === 'anthropic') apiKey = process.env.ANTHROPIC_API_KEY; + else if (config.provider === 'openai') apiKey = process.env.OPENAI_API_KEY; + } + + if (config.provider === 'anthropic') { + if (!apiKey) throw new Error('No API key'); + const res = await fetch('https://api.anthropic.com/v1/messages', { + method: 'POST', + headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' }, + body: JSON.stringify({ model: config.model || 'claude-haiku-4-5', max_tokens: 200, messages: [{ role: 'user', content: prompt }] }), + }); + if (!res.ok) throw new Error(`API ${res.status}`); + const data = await res.json() as AnthropicResponse; + return data.content?.[0]?.text || ''; + } + + if (config.provider === 'openai') { + if (!apiKey) throw new Error('No API key'); + const res = await fetch('https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ model: config.model || 'gpt-4o-mini', max_tokens: 200, messages: [{ role: 'user', content: prompt }] }), + }); + if (!res.ok) throw new Error(`API ${res.status}`); + const data = await res.json() as OpenAIResponse; + return data.choices?.[0]?.message?.content || ''; + } + + if (config.provider === 'ollama') { + const host = process.env.OLLAMA_HOST || 'http://localhost:11434'; + const res = await fetch(`${host}/api/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ model: config.model || 'llama3.2', prompt, stream: false }), + }); + if (!res.ok) throw new Error(`Ollama ${res.status}`); + const data = await res.json() as OllamaResponse; + return data.response || ''; + } + + return ''; +} diff --git a/src/core/config.ts b/src/core/config.ts index f7472f1d..0c17e3d8 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -1,6 +1,7 @@ import fs from 'fs'; import path from 'path'; import os from 'os'; +import { createRequire } from 'module'; // --- Config Types --- @@ -101,12 +102,49 @@ export function detectCapabilities(config?: MeMeshConfig): Capabilities { vectorSearch: true, scoring: true, knowledgeEvolution: true, - embeddings: llm?.provider ?? 'tfidf', + embeddings: detectEmbeddingSource(llm), llm, searchLevel: llm ? 1 : 0, }; } +/** + * Determine the actual embedding source based on provider. + * Anthropic has no embedding API — falls back to ONNX or tfidf. + */ +function detectEmbeddingSource(llm: LLMConfig | null): Capabilities['embeddings'] { + if (!llm) return 'tfidf'; + if (llm.provider === 'openai') return 'openai'; + if (llm.provider === 'ollama') return 'ollama'; + // Anthropic has no embedding API — check if ONNX is available + try { + const require = createRequire(import.meta.url); + require.resolve('@xenova/transformers'); + return 'onnx'; + } catch { + return 'tfidf'; + } +} + +// --- Embedding Dimensions --- + +const EMBEDDING_DIMENSIONS: Record = { + openai: 1536, // text-embedding-3-small + ollama: 768, // nomic-embed-text (default) + onnx: 384, // all-MiniLM-L6-v2 +}; + +/** + * Get the current embedding vector dimension based on configured provider. + * Used by db.ts to create/migrate the entities_vec table. + */ +export function getEmbeddingDimension(config?: MeMeshConfig): number { + const cfg = config ?? readConfig(); + const llm = cfg.llm ?? detectFromEnv() ?? detectOllama() ?? null; + const source = detectEmbeddingSource(llm); + return EMBEDDING_DIMENSIONS[source] ?? 384; +} + // --- Startup Capability Logging --- /** diff --git a/src/core/embedder.ts b/src/core/embedder.ts index b2e571e2..12783e2d 100644 --- a/src/core/embedder.ts +++ b/src/core/embedder.ts @@ -1,97 +1,69 @@ // ============================================================================= -// Embedder — local neural embedding generation via ONNX -// Uses @xenova/transformers (Xenova/all-MiniLM-L6-v2, 384 dimensions) -// Gracefully no-ops if package is not installed or model unavailable +// Embedder — multi-provider embedding generation +// Supports: OpenAI API, Ollama, ONNX (@xenova/transformers), none +// Provider selection: config.llm.provider → API embeddings if available, +// ONNX fallback, graceful no-op if nothing available // ============================================================================= import { createRequire } from 'node:module'; import { getDatabase } from '../db.js'; import { homedir } from 'os'; import { join } from 'path'; +import { detectCapabilities, type LLMConfig } from './config.js'; -let pipelineInstance: any = null; -let pipelineLoading: Promise | null = null; -let availableChecked = false; -let availableResult = false; +let onnxPipelineInstance: any = null; +let onnxPipelineLoading: Promise | null = null; +let onnxAvailableChecked = false; +let onnxAvailableResult = false; + +// --- Public API --- /** - * Check if @xenova/transformers can be imported. - * Cached after first check. + * Check if any embedding method is available. + * Returns true if ONNX or a provider API is configured. */ export function isEmbeddingAvailable(): boolean { - if (availableChecked) return availableResult; - availableChecked = true; - try { - // ESM project — use createRequire for synchronous resolution check - const require = createRequire(import.meta.url); - require.resolve('@xenova/transformers'); - availableResult = true; - } catch { - availableResult = false; - } - return availableResult; + const caps = detectCapabilities(); + if (caps.llm?.provider === 'openai') return true; + if (caps.llm?.provider === 'ollama') return true; + return isOnnxAvailable(); } -/** - * Reset the cached availability check (for testing). - */ -export function resetEmbeddingState(): void { - availableChecked = false; - availableResult = false; - pipelineInstance = null; - pipelineLoading = null; -} +// getEmbeddingDimension() is in config.ts to avoid circular dependency with db.ts +export { getEmbeddingDimension } from './config.js'; /** - * Get or create the embedding pipeline (singleton). - * First call downloads the model (~30MB) to ~/.memesh/models/. + * Reset cached state (for testing). */ -async function getPipeline(): Promise { - if (pipelineInstance) return pipelineInstance; - if (pipelineLoading) return pipelineLoading; - - pipelineLoading = (async () => { - // Dynamic import — type as any to avoid TS errors with optional dependency - const mod: any = await import('@xenova/transformers'); - const createPipeline = mod.pipeline; - const env = mod.env; - if (env) { - env.cacheDir = join(homedir(), '.memesh', 'models'); - env.allowLocalModels = true; - } - pipelineInstance = await createPipeline( - 'feature-extraction', - 'Xenova/all-MiniLM-L6-v2' - ); - return pipelineInstance; - })(); - - return pipelineLoading; +export function resetEmbeddingState(): void { + onnxAvailableChecked = false; + onnxAvailableResult = false; + onnxPipelineInstance = null; + onnxPipelineLoading = null; } /** - * Generate a 384-dim embedding for the given text. - * Returns null if embedding is unavailable or fails. + * Generate an embedding for the given text. + * Tries providers in order: OpenAI API → Ollama → ONNX → null. */ export async function embedText(text: string): Promise { - if (!isEmbeddingAvailable()) return null; - try { - const pipe = await getPipeline(); - const output = await pipe(text, { pooling: 'mean', normalize: true }); - return new Float32Array(output.data); - } catch { - return null; + const caps = detectCapabilities(); + + // Try provider API first + if (caps.llm) { + const result = await embedWithProvider(text, caps.llm); + if (result) return result; } + + // Fallback to ONNX + return embedWithOnnx(text); } /** - * Generate embedding for entity text and store in entities_vec. + * Generate embedding and store in entities_vec. * Silently skips if embedding generation fails. */ -export async function embedAndStore( - entityId: number, - text: string -): Promise { +export async function embedAndStore(entityId: number, text: string): Promise { const embedding = await embedText(text); if (!embedding) return; @@ -107,7 +79,6 @@ export async function embedAndStore( /** * Search entities_vec for similar embeddings by cosine distance. - * Returns entity IDs sorted by distance (lower = more similar). */ export function vectorSearch( queryEmbedding: Float32Array, @@ -127,3 +98,109 @@ export function vectorSearch( return []; } } + +// --- Provider Implementations --- + +async function embedWithProvider(text: string, config: LLMConfig): Promise { + try { + if (config.provider === 'openai') return await embedWithOpenAI(text, config); + if (config.provider === 'ollama') return await embedWithOllama(text, config); + // Anthropic has no embedding API — fall through to ONNX + return null; + } catch { + return null; + } +} + +async function embedWithOpenAI(text: string, config: LLMConfig): Promise { + const apiKey = config.apiKey || process.env.OPENAI_API_KEY; + if (!apiKey) return null; + + const res = await fetch('https://api.openai.com/v1/embeddings', { + method: 'POST', + headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: 'text-embedding-3-small', + input: text.slice(0, 8000), // API input limit + }), + }); + if (!res.ok) return null; + + const data = await res.json() as { data?: Array<{ embedding?: number[] }> }; + const embedding = data.data?.[0]?.embedding; + if (!embedding || !Array.isArray(embedding)) return null; + + return new Float32Array(embedding); +} + +async function embedWithOllama(text: string, config: LLMConfig): Promise { + const host = process.env.OLLAMA_HOST || 'http://localhost:11434'; + const model = config.model || 'nomic-embed-text'; + + const res = await fetch(`${host}/api/embed`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ model, input: text.slice(0, 8000) }), + }); + if (!res.ok) return null; + + const data = await res.json() as { embeddings?: number[][] }; + const embedding = data.embeddings?.[0]; + if (!embedding || !Array.isArray(embedding)) return null; + + return new Float32Array(embedding); +} + +// --- ONNX (local, @xenova/transformers) --- + +function isOnnxAvailable(): boolean { + if (onnxAvailableChecked) return onnxAvailableResult; + onnxAvailableChecked = true; + try { + const require = createRequire(import.meta.url); + require.resolve('@xenova/transformers'); + onnxAvailableResult = true; + } catch { + onnxAvailableResult = false; + } + return onnxAvailableResult; +} + +async function getOnnxPipeline(): Promise { + if (onnxPipelineInstance) return onnxPipelineInstance; + if (onnxPipelineLoading) return onnxPipelineLoading; + + onnxPipelineLoading = (async () => { + try { + const mod: any = await import('@xenova/transformers'); + const createPipeline = mod.pipeline; + const env = mod.env; + if (env) { + env.cacheDir = join(homedir(), '.memesh', 'models'); + env.allowLocalModels = true; + } + onnxPipelineInstance = await createPipeline( + 'feature-extraction', + 'Xenova/all-MiniLM-L6-v2' + ); + return onnxPipelineInstance; + } catch (err) { + // Reset so next call retries instead of returning cached rejected promise + onnxPipelineLoading = null; + throw err; + } + })(); + + return onnxPipelineLoading; +} + +async function embedWithOnnx(text: string): Promise { + if (!isOnnxAvailable()) return null; + try { + const pipe = await getOnnxPipeline(); + const output = await pipe(text, { pooling: 'mean', normalize: true }); + return new Float32Array(output.data); + } catch { + return null; + } +} diff --git a/src/core/lifecycle.ts b/src/core/lifecycle.ts index c16d0468..4e33c339 100644 --- a/src/core/lifecycle.ts +++ b/src/core/lifecycle.ts @@ -82,6 +82,150 @@ export function getDecayStatus(db: Database.Database): { }; } +// Types that should NEVER be compressed — they represent intentional knowledge +const PRESERVED_TYPES = new Set([ + 'decision', 'pattern', 'lesson_learned', 'bug_fix', 'architecture', + 'convention', 'feature', 'best_practice', 'concept', 'tool', 'note', + 'plan', 'release', 'refactoring', 'maintenance', +]); + +// Types considered auto-generated noise +const NOISE_TYPES = new Set([ + 'session_keypoint', 'commit', 'session-insight', 'session-summary', +]); + +const COMPRESS_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours +const NOISE_AGE_DAYS = 7; +const NOISE_THRESHOLD = 20; // minimum noise entities per week to trigger compression + +/** + * Compress old auto-generated entities into weekly summaries. + * Groups session_keypoint, commit, session-insight entities older than 7 days + * by ISO week. Creates one summary entity per week and archives the originals. + * + * - Throttled to once per 24 hours via memesh_metadata + * - Only compresses if > 20 noise entities exist for a given week + * - Never touches: decisions, patterns, lessons, bug_fixes, or any intentional knowledge + */ +export function compressWeeklyNoise(db: Database.Database): { compressed: number; weeksProcessed: number } { + ensureMetadataTable(db); + + // Throttle: skip if last compression was less than 24h ago + const lastRun = db.prepare( + "SELECT value FROM memesh_metadata WHERE key = 'last_noise_compress_at'" + ).get() as { value: string } | undefined; + + if (lastRun) { + const elapsed = Date.now() - new Date(lastRun.value).getTime(); + if (elapsed < COMPRESS_INTERVAL_MS) { + return { compressed: 0, weeksProcessed: 0 }; + } + } + + // Backward-compat: skip if status column doesn't exist + const cols = db.prepare('PRAGMA table_info(entities)').all() as Array<{ name: string }>; + if (!cols.some((c) => c.name === 'status')) { + return { compressed: 0, weeksProcessed: 0 }; + } + + // Find old noise entities grouped by calendar week + const cutoff = new Date(Date.now() - NOISE_AGE_DAYS * 24 * 60 * 60 * 1000).toISOString(); + const noiseTypePlaceholders = Array.from(NOISE_TYPES).map(() => '?').join(','); + const noiseTypeValues = Array.from(NOISE_TYPES); + + const weekGroups = db.prepare(` + SELECT strftime('%Y-W%W', created_at) as week, + COUNT(*) as count + FROM entities + WHERE type IN (${noiseTypePlaceholders}) + AND status = 'active' + AND created_at < ? + GROUP BY week + HAVING count >= ? + ORDER BY week + `).all(...noiseTypeValues, cutoff, NOISE_THRESHOLD) as Array<{ week: string; count: number }>; + + let totalCompressed = 0; + + for (const { week, count } of weekGroups) { + // Get entities for this week — include cutoff filter to avoid archiving recent entities + const entities = db.prepare(` + SELECT e.id, e.name, e.type + FROM entities e + WHERE e.type IN (${noiseTypePlaceholders}) + AND e.status = 'active' + AND strftime('%Y-W%W', e.created_at) = ? + AND e.created_at < ? + `).all(...noiseTypeValues, week, cutoff) as Array<{ id: number; name: string; type: string }>; + + if (entities.length === 0) continue; + + // Count by type + const typeCounts = new Map(); + for (const e of entities) { + typeCounts.set(e.type, (typeCounts.get(e.type) || 0) + 1); + } + + const typeBreakdown = Array.from(typeCounts.entries()) + .map(([t, c]) => `${c} ${t}`) + .join(', '); + + // Create or update weekly summary entity + const summaryName = `weekly-summary-${week}`; + const existing = db.prepare('SELECT id FROM entities WHERE name = ?').get(summaryName) as { id: number } | undefined; + + if (existing) { + // Append observation to existing summary + db.prepare('INSERT INTO observations (entity_id, content) VALUES (?, ?)').run( + existing.id, + `+${entities.length} entities archived (${typeBreakdown})` + ); + } else { + db.prepare('INSERT INTO entities (name, type) VALUES (?, ?)').run(summaryName, 'weekly-summary'); + const summaryRow = db.prepare('SELECT id FROM entities WHERE name = ?').get(summaryName) as { id: number }; + const obsText = `${week}: ${count} auto-tracked entities compressed (${typeBreakdown})`; + db.prepare('INSERT INTO observations (entity_id, content) VALUES (?, ?)').run( + summaryRow.id, obsText + ); + // Index in FTS5 so summary is searchable + db.prepare('INSERT INTO entities_fts (rowid, name, observations) VALUES (?, ?, ?)').run( + summaryRow.id, summaryName, obsText + ); + // Copy project tags from originals + const entityIdPlaceholders = entities.map(() => '?').join(','); + const projectTags = db.prepare(` + SELECT DISTINCT t.tag FROM tags t + JOIN entities e ON e.id = t.entity_id + WHERE e.id IN (${entityIdPlaceholders}) + AND t.tag LIKE 'project:%' + `).all(...entities.map(e => e.id)) as Array<{ tag: string }>; + for (const { tag } of projectTags) { + db.prepare('INSERT OR IGNORE INTO tags (entity_id, tag) VALUES (?, ?)').run(summaryRow.id, tag); + } + db.prepare('INSERT OR IGNORE INTO tags (entity_id, tag) VALUES (?, ?)').run(summaryRow.id, 'source:noise-filter'); + } + + // Archive originals + const archiveIdPlaceholders = entities.map(() => '?').join(','); + db.prepare(` + UPDATE entities SET status = 'archived' + WHERE id IN (${archiveIdPlaceholders}) + `).run(...entities.map(e => e.id)); + + totalCompressed += entities.length; + } + + // Record timestamp + db.prepare( + "INSERT OR REPLACE INTO memesh_metadata (key, value) VALUES ('last_noise_compress_at', ?)" + ).run(new Date().toISOString()); + + return { compressed: totalCompressed, weeksProcessed: weekGroups.length }; +} + +// Export preserved/noise type sets for testing +export { PRESERVED_TYPES, NOISE_TYPES }; + /** * Ensure the memesh_metadata table exists. * Used to store decay timestamps and other operational metadata. diff --git a/src/core/operations.ts b/src/core/operations.ts index cb96830b..07dc1617 100644 --- a/src/core/operations.ts +++ b/src/core/operations.ts @@ -15,6 +15,8 @@ import { expandQuery, isExpansionAvailable } from './query-expander.js'; import { rankEntities } from './scoring.js'; import { createExplicitLesson } from './lesson-engine.js'; import { embedAndStore, isEmbeddingAvailable, embedText, vectorSearch } from './embedder.js'; +import { autoTagAndApply } from './auto-tagger.js'; +import { detectCapabilities } from './config.js'; import path from 'path'; import type { RememberInput, @@ -76,6 +78,14 @@ export function remember(args: RememberInput): RememberResult { embedAndStore(entityId, text).catch(() => {}); } + // Fire-and-forget: auto-generate tags if none provided and LLM is configured + if ((!args.tags || args.tags.length === 0) && args.observations?.length) { + const caps = detectCapabilities(); + if (caps.llm) { + autoTagAndApply(entityId, args.name, args.type, args.observations, caps.llm).catch(() => {}); + } + } + return { stored: true, entityId, @@ -243,6 +253,9 @@ export { consolidate } from './consolidator.js'; // --- Serialization (extracted to serializer.ts) --- export { exportMemories, importMemories } from './serializer.js'; +// --- Noise compression (extracted to lifecycle.ts) --- +export { compressWeeklyNoise } from './lifecycle.js'; + /** * Create a structured lesson_learned entity from explicit user input. * Does not require an LLM — the user provides the structured fields directly. diff --git a/src/core/scoring.ts b/src/core/scoring.ts index 67791083..db72ef2d 100644 --- a/src/core/scoring.ts +++ b/src/core/scoring.ts @@ -1,17 +1,19 @@ export interface ScoringWeights { - searchRelevance: number; // default 0.35 + searchRelevance: number; // default 0.30 recency: number; // default 0.25 - frequency: number; // default 0.20 + frequency: number; // default 0.15 confidence: number; // default 0.15 temporalValidity: number; // default 0.05 + impact: number; // default 0.10 } export const DEFAULT_WEIGHTS: ScoringWeights = { - searchRelevance: 0.35, + searchRelevance: 0.30, recency: 0.25, - frequency: 0.20, + frequency: 0.15, confidence: 0.15, temporalValidity: 0.05, + impact: 0.10, }; /** @@ -44,12 +46,22 @@ export function temporalValidityScore(validUntil: string | null | undefined): nu return new Date(validUntil).getTime() > Date.now() ? 1.0 : 0.5; } +/** + * Calculate impact score using Laplace-smoothed recall effectiveness. + * Score = (recall_hits + 1) / (recall_hits + recall_misses + 2) + * Laplace smoothing gives new entities (0 hits, 0 misses) a neutral 0.5. + * Entities with high hit rate rise; ignored entities fade. + */ +export function impactScore(recallHits: number, recallMisses: number): number { + return (recallHits + 1) / (recallHits + recallMisses + 2); +} + /** * Score a single entity. * searchRelevanceValue is provided by the search engine (FTS5 rank or vector distance). */ export function scoreEntity( - entity: { access_count?: number; last_accessed_at?: string; confidence?: number; valid_until?: string }, + entity: { access_count?: number; last_accessed_at?: string; confidence?: number; valid_until?: string; recall_hits?: number; recall_misses?: number }, searchRelevanceValue: number, maxAccessCount: number, weights: ScoringWeights = DEFAULT_WEIGHTS @@ -59,14 +71,15 @@ export function scoreEntity( const fq = frequencyScore(entity.access_count ?? 0, maxAccessCount) * weights.frequency; const cf = (entity.confidence ?? 1.0) * weights.confidence; const tv = temporalValidityScore(entity.valid_until) * weights.temporalValidity; - return sr + rc + fq + cf + tv; + const im = impactScore(entity.recall_hits ?? 0, entity.recall_misses ?? 0) * weights.impact; + return sr + rc + fq + cf + tv + im; } /** * Sort entities by score descending. * searchRelevanceValues maps entity name → search relevance (0-1). */ -export function rankEntities( +export function rankEntities( entities: T[], searchRelevanceValues: Map, weights?: ScoringWeights diff --git a/src/db.ts b/src/db.ts index 185989e7..7e50fbcb 100644 --- a/src/db.ts +++ b/src/db.ts @@ -4,6 +4,7 @@ import path from 'path'; import os from 'os'; import fs from 'fs'; import { runAutoDecay } from './core/lifecycle.js'; +import { getEmbeddingDimension } from './core/config.js'; import type { PragmaColumnRow } from './core/types.js'; let db: Database.Database | null = null; @@ -104,21 +105,73 @@ export function openDatabase(dbPath?: string): Database.Database { db.exec("CREATE INDEX IF NOT EXISTS idx_entities_namespace ON entities(namespace)"); } + // Migrate: add recall effectiveness columns if missing (v4.0.0) + const recallCols = db.prepare("PRAGMA table_info(entities)").all() as PragmaColumnRow[]; + if (!recallCols.some((c) => c.name === 'recall_hits')) { + db.exec("ALTER TABLE entities ADD COLUMN recall_hits INTEGER DEFAULT 0"); + db.exec("ALTER TABLE entities ADD COLUMN recall_misses INTEGER DEFAULT 0"); + } + // Run auto-decay: reduce confidence for stale entities (throttled to once per 24h) runAutoDecay(db); // Load sqlite-vec extension for vector similarity search sqliteVec.load(db); - // Create vector table for entity embeddings (if not exists) - // 384 dimensions = all-MiniLM-L6-v2 embedding size + // Create/migrate vector table for entity embeddings + // Dimension depends on embedding provider (384=ONNX, 1536=OpenAI, 768=Ollama) + const targetDim = getEmbeddingDimension(); + ensureVecTable(db, targetDim); + + return db; +} + +/** + * Ensure entities_vec table exists with the correct dimension. + * If dimension changed (provider switch), drops and recreates the table. + * Old embeddings are lost — new ones regenerated as entities are accessed. + */ +function ensureVecTable(db: Database.Database, targetDim: number): void { + // Ensure metadata table exists + db.exec(` + CREATE TABLE IF NOT EXISTS memesh_metadata ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) + `); + + const storedDim = db.prepare( + "SELECT value FROM memesh_metadata WHERE key = 'embedding_dimension'" + ).get() as { value: string } | undefined; + + const currentDim = storedDim ? parseInt(storedDim.value, 10) : 0; + + // Check if vec table exists + const vecExists = db.prepare( + "SELECT name FROM sqlite_master WHERE type='table' AND name='entities_vec'" + ).get(); + + if (vecExists && currentDim === targetDim) { + return; // table exists with correct dimension + } + + // Drop old table if dimension changed — embeddings will be regenerated + if (vecExists && currentDim !== targetDim) { + process.stderr.write(`MeMesh: Embedding dimension changed (${currentDim} → ${targetDim}). Rebuilding vector index — old embeddings will be regenerated as entities are accessed.\n`); + db.exec('DROP TABLE entities_vec'); + } + + // Create with target dimension db.exec(` CREATE VIRTUAL TABLE IF NOT EXISTS entities_vec USING vec0( - embedding float[384] + embedding float[${targetDim}] ); `); - return db; + // Store current dimension + db.prepare( + "INSERT OR REPLACE INTO memesh_metadata (key, value) VALUES ('embedding_dimension', ?)" + ).run(String(targetDim)); } export function closeDatabase(): void { diff --git a/src/transports/http/server.ts b/src/transports/http/server.ts index 812d5ac0..3437b24d 100644 --- a/src/transports/http/server.ts +++ b/src/transports/http/server.ts @@ -8,7 +8,7 @@ import { KnowledgeGraph } from '../../knowledge-graph.js'; import { getDatabase } from '../../db.js'; import { logCapabilities, readConfig, updateConfig, detectCapabilities } from '../../core/config.js'; import { computePatterns } from '../../core/patterns.js'; -import type { CountRow } from '../../core/types.js'; +import type { CountRow, PragmaColumnRow } from '../../core/types.js'; import { RememberSchema as RememberBody, RecallSchema as RecallBody, ForgetSchema as ForgetBody, ConsolidateSchema as ConsolidateBody, @@ -236,7 +236,7 @@ const ConfigBody = z.object({ sessionLimit: z.number().int().min(1).max(100).optional(), theme: z.enum(['light', 'dark']).optional(), setupCompleted: z.boolean().optional(), -}).passthrough(); +}).strip(); app.post('/v1/config', (req, res) => { const parsed = ConfigBody.safeParse(req.body); @@ -433,6 +433,54 @@ app.get('/v1/analytics', (_req, res) => { duplicateCandidates, }; + // --- Recall Effectiveness --- + let recallEffectiveness: { + overallHitRate: number; + totalHits: number; + totalMisses: number; + trackedEntities: number; + topEffective: Array<{ name: string; type: string; hits: number; misses: number; hitRate: number }>; + mostIgnored: Array<{ name: string; type: string; hits: number; misses: number; hitRate: number }>; + } | null = null; + + try { + const recallColCheck = db.prepare("PRAGMA table_info(entities)").all() as PragmaColumnRow[]; + if (recallColCheck.some((c: PragmaColumnRow) => c.name === 'recall_hits')) { + const totals = db.prepare( + `SELECT COALESCE(SUM(recall_hits), 0) as hits, COALESCE(SUM(recall_misses), 0) as misses, + COUNT(*) as tracked FROM entities WHERE (recall_hits > 0 OR recall_misses > 0)` + ).get() as { hits: number; misses: number; tracked: number }; + + const total = totals.hits + totals.misses; + const overallHitRate = total > 0 ? totals.hits / total : 0; + + const topEffective = db.prepare( + `SELECT name, type, COALESCE(recall_hits, 0) as hits, COALESCE(recall_misses, 0) as misses, + CAST(COALESCE(recall_hits, 0) AS REAL) / (COALESCE(recall_hits, 0) + COALESCE(recall_misses, 0)) as hitRate + FROM entities WHERE (COALESCE(recall_hits, 0) + COALESCE(recall_misses, 0)) > 0 + ORDER BY hitRate DESC, hits DESC LIMIT 5` + ).all() as Array<{ name: string; type: string; hits: number; misses: number; hitRate: number }>; + + const mostIgnored = db.prepare( + `SELECT name, type, COALESCE(recall_hits, 0) as hits, COALESCE(recall_misses, 0) as misses, + CAST(COALESCE(recall_hits, 0) AS REAL) / (COALESCE(recall_hits, 0) + COALESCE(recall_misses, 0)) as hitRate + FROM entities WHERE (COALESCE(recall_hits, 0) + COALESCE(recall_misses, 0)) > 0 + ORDER BY hitRate ASC, misses DESC LIMIT 5` + ).all() as Array<{ name: string; type: string; hits: number; misses: number; hitRate: number }>; + + recallEffectiveness = { + overallHitRate, + totalHits: totals.hits, + totalMisses: totals.misses, + trackedEntities: totals.tracked, + topEffective, + mostIgnored, + }; + } + } catch { + // recall_hits column may not exist yet — skip gracefully + } + res.json({ success: true, data: { @@ -440,6 +488,7 @@ app.get('/v1/analytics', (_req, res) => { healthFactors, timeline, valueMetrics, + recallEffectiveness, cleanup, }, }); diff --git a/tests/core/auto-tagger.test.ts b/tests/core/auto-tagger.test.ts new file mode 100644 index 00000000..be29445f --- /dev/null +++ b/tests/core/auto-tagger.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { openDatabase, closeDatabase, getDatabase } from '../../src/db.js'; +import { parseTags } from '../../src/core/auto-tagger.js'; +import { remember } from '../../src/core/operations.js'; + +let tmpDir: string; + +beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'memesh-autotag-')); + openDatabase(path.join(tmpDir, 'test.db')); +}); + +afterEach(() => { + closeDatabase(); + fs.rmSync(tmpDir, { recursive: true, force: true }); +}); + +describe('Auto-Tagger parseTags', () => { + it('should parse valid JSON array of tags', () => { + const result = parseTags('["project:memesh", "topic:auth", "tech:sqlite"]'); + expect(result).toEqual(['project:memesh', 'topic:auth', 'tech:sqlite']); + }); + + it('should extract JSON array from surrounding text', () => { + const result = parseTags('Here are the tags: ["project:myapp", "topic:database"] Hope this helps!'); + expect(result).toEqual(['project:myapp', 'topic:database']); + }); + + it('should filter out tags without valid prefixes', () => { + const result = parseTags('["project:memesh", "invalid-tag", "random", "tech:node"]'); + expect(result).toEqual(['project:memesh', 'tech:node']); + }); + + it('should return empty array for invalid JSON', () => { + expect(parseTags('not json')).toEqual([]); + expect(parseTags('{}')).toEqual([]); + expect(parseTags('')).toEqual([]); + }); + + it('should limit to 5 tags maximum', () => { + const input = '["project:a", "project:b", "topic:c", "tech:d", "scope:e", "severity:f", "topic:g"]'; + const result = parseTags(input); + expect(result.length).toBeLessThanOrEqual(5); + }); + + it('should lowercase tags', () => { + const result = parseTags('["Project:MyApp", "Topic:AUTH"]'); + expect(result).toEqual(['project:myapp', 'topic:auth']); + }); +}); + +describe('Auto-Tagger integration', () => { + it('remember without tags does not crash (auto-tag is fire-and-forget)', () => { + // This test verifies remember() doesn't throw when no tags provided + // Auto-tagging runs async and won't actually call LLM in test env + const result = remember({ + name: 'no-tags-entity', + type: 'decision', + observations: ['Use SQLite for local storage'], + }); + expect(result.stored).toBe(true); + expect(result.tags).toBe(0); + }); + + it('remember with tags skips auto-tagging', () => { + const result = remember({ + name: 'tagged-entity', + type: 'decision', + observations: ['Use SQLite'], + tags: ['project:memesh'], + }); + expect(result.stored).toBe(true); + expect(result.tags).toBe(1); + }); +}); diff --git a/tests/core/config.test.ts b/tests/core/config.test.ts index 5210d78b..2bed36ba 100644 --- a/tests/core/config.test.ts +++ b/tests/core/config.test.ts @@ -72,7 +72,8 @@ describe('Config: detectCapabilities', () => { }); expect(caps.searchLevel).toBe(1); expect(caps.llm?.provider).toBe('anthropic'); - expect(caps.embeddings).toBe('anthropic'); + // Anthropic has no embedding API — falls back to ONNX if available, otherwise tfidf + expect(['onnx', 'tfidf']).toContain(caps.embeddings); }); it('returns Level 1 with openai LLM config', () => { diff --git a/tests/core/embedder.test.ts b/tests/core/embedder.test.ts index 23d844e7..c155f552 100644 --- a/tests/core/embedder.test.ts +++ b/tests/core/embedder.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { isEmbeddingAvailable, resetEmbeddingState, + getEmbeddingDimension, } from '../../src/core/embedder.js'; describe('Embedder', () => { @@ -34,4 +35,16 @@ describe('Embedder', () => { // resetEmbeddingState actually clears the cache expect(first).toBe(second); }); + + it('getEmbeddingDimension returns a positive integer', () => { + const dim = getEmbeddingDimension(); + expect(dim).toBeGreaterThan(0); + expect(Number.isInteger(dim)).toBe(true); + }); + + it('getEmbeddingDimension returns known dimension value', () => { + // Should be one of: 384 (ONNX), 1536 (OpenAI), 768 (Ollama) + const dim = getEmbeddingDimension(); + expect([384, 768, 1536]).toContain(dim); + }); }); diff --git a/tests/core/lifecycle.test.ts b/tests/core/lifecycle.test.ts index 1fb9424f..04d2e143 100644 --- a/tests/core/lifecycle.test.ts +++ b/tests/core/lifecycle.test.ts @@ -3,7 +3,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; import { openDatabase, closeDatabase, getDatabase } from '../../src/db.js'; -import { runAutoDecay, getDecayStatus } from '../../src/core/lifecycle.js'; +import { runAutoDecay, getDecayStatus, compressWeeklyNoise, PRESERVED_TYPES, NOISE_TYPES } from '../../src/core/lifecycle.js'; let tmpDir: string; @@ -159,3 +159,87 @@ describe('Decay Status', () => { expect(status.entitiesBelowThreshold).toBeGreaterThanOrEqual(1); }); }); + +describe('Noise Filter (compressWeeklyNoise)', () => { + function insertOldNoiseEntities(db: ReturnType, count: number, type: string, weeksAgo: number) { + const date = new Date(Date.now() - weeksAgo * 7 * 24 * 60 * 60 * 1000); + for (let i = 0; i < count; i++) { + db.prepare('INSERT INTO entities (name, type, created_at) VALUES (?, ?, ?)').run( + `${type}-${weeksAgo}w-${i}`, type, date.toISOString() + ); + const row = db.prepare('SELECT id FROM entities WHERE name = ?').get(`${type}-${weeksAgo}w-${i}`) as any; + db.prepare('INSERT INTO tags (entity_id, tag) VALUES (?, ?)').run(row.id, 'project:test'); + } + } + + it('should compress noise entities older than 7 days when count exceeds threshold', () => { + const db = getDatabase(); + db.exec("DELETE FROM memesh_metadata WHERE key = 'last_noise_compress_at'"); + insertOldNoiseEntities(db, 25, 'session_keypoint', 2); + + const result = compressWeeklyNoise(db); + expect(result.compressed).toBe(25); + expect(result.weeksProcessed).toBe(1); + + // Originals should be archived + const active = (db.prepare( + "SELECT COUNT(*) as c FROM entities WHERE type = 'session_keypoint' AND status = 'active'" + ).get() as any).c; + expect(active).toBe(0); + + // Summary entity should exist + const summaries = db.prepare("SELECT name FROM entities WHERE type = 'weekly-summary'").all() as any[]; + expect(summaries.length).toBe(1); + }); + + it('should NOT compress noise entities below threshold', () => { + const db = getDatabase(); + db.exec("DELETE FROM memesh_metadata WHERE key = 'last_noise_compress_at'"); + insertOldNoiseEntities(db, 10, 'commit', 2); // below threshold of 20 + + const result = compressWeeklyNoise(db); + expect(result.compressed).toBe(0); + expect(result.weeksProcessed).toBe(0); + }); + + it('should NEVER compress preserved types (decisions, lessons, etc.)', () => { + const db = getDatabase(); + db.exec("DELETE FROM memesh_metadata WHERE key = 'last_noise_compress_at'"); + // Create old decision entities + const date = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(); + for (let i = 0; i < 25; i++) { + db.prepare('INSERT INTO entities (name, type, created_at) VALUES (?, ?, ?)').run( + `decision-old-${i}`, 'decision', date + ); + } + + const result = compressWeeklyNoise(db); + expect(result.compressed).toBe(0); + + // All decisions should remain active + const active = (db.prepare( + "SELECT COUNT(*) as c FROM entities WHERE type = 'decision' AND status = 'active'" + ).get() as any).c; + expect(active).toBe(25); + }); + + it('should be throttled to once per 24 hours', () => { + const db = getDatabase(); + db.exec("DELETE FROM memesh_metadata WHERE key = 'last_noise_compress_at'"); + insertOldNoiseEntities(db, 25, 'session_keypoint', 2); + + const result1 = compressWeeklyNoise(db); + expect(result1.compressed).toBe(25); + + // Second call should be throttled + insertOldNoiseEntities(db, 25, 'commit', 3); + const result2 = compressWeeklyNoise(db); + expect(result2.compressed).toBe(0); // throttled + }); + + it('should ensure PRESERVED_TYPES and NOISE_TYPES do not overlap', () => { + for (const noiseType of NOISE_TYPES) { + expect(PRESERVED_TYPES.has(noiseType)).toBe(false); + } + }); +}); diff --git a/tests/core/scoring.test.ts b/tests/core/scoring.test.ts index 13008079..689f4f6b 100644 --- a/tests/core/scoring.test.ts +++ b/tests/core/scoring.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { recencyScore, frequencyScore, temporalValidityScore, rankEntities } from '../../src/core/scoring.js'; +import { recencyScore, frequencyScore, temporalValidityScore, impactScore, rankEntities } from '../../src/core/scoring.js'; describe('Scoring Engine', () => { describe('recencyScore', () => { @@ -48,6 +48,24 @@ describe('Scoring Engine', () => { }); }); + describe('impactScore', () => { + it('returns 0.5 for new entity (0 hits, 0 misses) — Laplace smoothing', () => { + expect(impactScore(0, 0)).toBeCloseTo(0.5, 2); + }); + + it('returns high score for entity with many hits', () => { + expect(impactScore(10, 0)).toBeCloseTo(11 / 12, 2); // ~0.917 + }); + + it('returns low score for entity with many misses', () => { + expect(impactScore(0, 10)).toBeCloseTo(1 / 12, 2); // ~0.083 + }); + + it('returns ~0.5 for balanced hits and misses', () => { + expect(impactScore(5, 5)).toBeCloseTo(0.5, 2); + }); + }); + describe('rankEntities', () => { it('ranks frequently accessed higher', () => { const entities = [ @@ -59,6 +77,16 @@ describe('Scoring Engine', () => { expect(ranked[0].name).toBe('popular'); }); + it('ranks high-impact entities higher when other factors are equal', () => { + const entities = [ + { name: 'ignored', access_count: 5, confidence: 1.0, recall_hits: 0, recall_misses: 10 }, + { name: 'effective', access_count: 5, confidence: 1.0, recall_hits: 10, recall_misses: 0 }, + ]; + const relevance = new Map([['ignored', 0.5], ['effective', 0.5]]); + const ranked = rankEntities(entities, relevance); + expect(ranked[0].name).toBe('effective'); + }); + it('ranks recent higher when access is equal', () => { const now = new Date().toISOString(); const old = new Date(Date.now() - 60 * 24 * 60 * 60 * 1000).toISOString(); diff --git a/tests/db.test.ts b/tests/db.test.ts index 50659aff..2e457914 100644 --- a/tests/db.test.ts +++ b/tests/db.test.ts @@ -181,4 +181,22 @@ describe('Feature: Database Management', () => { expect(idx).toBeDefined(); }); }); + + describe('Scenario: Recall effectiveness columns migration (v4.0.0)', () => { + it('should have recall_hits column with default 0', () => { + const db = openDatabase(testDbPath); + const info = db.prepare("PRAGMA table_info(entities)").all() as any[]; + const col = info.find((c: any) => c.name === 'recall_hits'); + expect(col).toBeDefined(); + expect(col.dflt_value).toBe('0'); + }); + + it('should have recall_misses column with default 0', () => { + const db = openDatabase(testDbPath); + const info = db.prepare("PRAGMA table_info(entities)").all() as any[]; + const col = info.find((c: any) => c.name === 'recall_misses'); + expect(col).toBeDefined(); + expect(col.dflt_value).toBe('0'); + }); + }); }); diff --git a/tests/hooks/pre-edit-recall.test.ts b/tests/hooks/pre-edit-recall.test.ts new file mode 100644 index 00000000..bd6f0487 --- /dev/null +++ b/tests/hooks/pre-edit-recall.test.ts @@ -0,0 +1,118 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { execFileSync } from 'child_process'; +import { createRequire } from 'module'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; + +const require = createRequire(import.meta.url); + +describe('Feature: Pre-Edit Recall Hook', () => { + let testDir: string; + let dbPath: string; + + beforeEach(() => { + testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'memesh-hook-test-')); + dbPath = path.join(testDir, 'test.db'); + // Create .memesh dir for throttle file + fs.mkdirSync(path.join(testDir, '.memesh'), { recursive: true }); + }); + + afterEach(() => { + fs.rmSync(testDir, { recursive: true, force: true }); + }); + + function runHook(input: object): string { + const hookPath = path.resolve('scripts/hooks/pre-edit-recall.js'); + const jsonInput = JSON.stringify(input); + try { + return execFileSync('node', [hookPath], { + input: jsonInput, + env: { ...process.env, MEMESH_DB_PATH: dbPath, HOME: testDir }, + encoding: 'utf8', + timeout: 10000, + }).trim(); + } catch { + return ''; + } + } + + function createTestDb() { + const Database = require('better-sqlite3'); + const db = new Database(dbPath); + db.pragma('journal_mode = WAL'); + db.exec(` + CREATE TABLE IF NOT EXISTS entities ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + type TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + metadata JSON, + status TEXT NOT NULL DEFAULT 'active' + ); + CREATE TABLE IF NOT EXISTS observations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + entity_id INTEGER NOT NULL, + content TEXT NOT NULL, + FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE + ); + CREATE TABLE IF NOT EXISTS tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + entity_id INTEGER NOT NULL, + tag TEXT NOT NULL, + FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE + ); + CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_entity_tag_unique ON tags(entity_id, tag); + CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5( + name, observations, content='', + tokenize='unicode61 remove_diacritics 1' + ); + `); + return db; + } + + it('should return empty when no database exists', () => { + const result = runHook({ tool_input: { file_path: '/some/file.ts' } }); + expect(result).toBe(''); + }); + + it('should return empty when no relevant memories found', () => { + createTestDb().close(); + const result = runHook({ tool_input: { file_path: '/some/unknown-file.ts' } }); + expect(result).toBe(''); + }); + + it('should return memories matching file tag', () => { + const db = createTestDb(); + db.prepare('INSERT INTO entities (name, type) VALUES (?, ?)').run('auth-decision', 'decision'); + const row = db.prepare('SELECT id FROM entities WHERE name = ?').get('auth-decision') as any; + db.prepare('INSERT INTO observations (entity_id, content) VALUES (?, ?)').run(row.id, 'Use OAuth 2.0'); + db.prepare('INSERT INTO tags (entity_id, tag) VALUES (?, ?)').run(row.id, 'file:auth'); + db.close(); + + const result = runHook({ tool_input: { file_path: '/src/auth.ts' } }); + expect(result).toContain('auth-decision'); + expect(result).toContain('Use OAuth 2.0'); + }); + + it('should throttle: second call for same file returns empty', () => { + const db = createTestDb(); + db.prepare('INSERT INTO entities (name, type) VALUES (?, ?)').run('auth-decision', 'decision'); + const row = db.prepare('SELECT id FROM entities WHERE name = ?').get('auth-decision') as any; + db.prepare('INSERT INTO observations (entity_id, content) VALUES (?, ?)').run(row.id, 'Use OAuth 2.0'); + db.prepare('INSERT INTO tags (entity_id, tag) VALUES (?, ?)').run(row.id, 'file:auth'); + db.close(); + + const result1 = runHook({ tool_input: { file_path: '/src/auth.ts' } }); + expect(result1).toContain('auth-decision'); + + const result2 = runHook({ tool_input: { file_path: '/src/auth.ts' } }); + expect(result2).toBe(''); + }); + + it('should return empty when no file_path in tool_input', () => { + createTestDb().close(); + const result = runHook({ tool_input: { command: 'ls' } }); + expect(result).toBe(''); + }); +}); diff --git a/tests/installation.test.ts b/tests/installation.test.ts index 86b9a3d7..e73b146b 100644 --- a/tests/installation.test.ts +++ b/tests/installation.test.ts @@ -27,10 +27,11 @@ describe('Installation Verification', () => { expect(fs.existsSync('.mcp.json')).toBe(true); }); - it('should have hooks.json with 4 hooks', () => { + it('should have hooks.json with 5 hook types', () => { const hooks = JSON.parse(fs.readFileSync('hooks/hooks.json', 'utf8')); const hookTypes = Object.keys(hooks.hooks); - expect(hookTypes).toHaveLength(4); + expect(hookTypes).toHaveLength(5); + expect(hookTypes).toContain('PreToolUse'); expect(hookTypes).toContain('SessionStart'); expect(hookTypes).toContain('PostToolUse'); expect(hookTypes).toContain('Stop'); diff --git a/tests/transports/analytics.test.ts b/tests/transports/analytics.test.ts index 0bb9b9bb..27f2062f 100644 --- a/tests/transports/analytics.test.ts +++ b/tests/transports/analytics.test.ts @@ -105,3 +105,60 @@ describe('/v1/analytics computation', () => { expect(types[1]).toEqual({ type: 'decision', count: 1 }); }); }); + +// ── Recall Effectiveness ───────────────────────────────────────────────── + +describe('recall effectiveness tracking', () => { + it('recall_hits and recall_misses default to 0 for new entities', () => { + remember({ name: 'eff-test', type: 'decision', observations: ['test'] }); + const row = db.prepare( + 'SELECT recall_hits, recall_misses FROM entities WHERE name = ?' + ).get('eff-test') as { recall_hits: number; recall_misses: number }; + expect(row.recall_hits).toBe(0); + expect(row.recall_misses).toBe(0); + }); + + it('recall_hits increments correctly', () => { + remember({ name: 'hit-entity', type: 'decision', observations: ['test'] }); + db.prepare('UPDATE entities SET recall_hits = recall_hits + 1 WHERE name = ?').run('hit-entity'); + db.prepare('UPDATE entities SET recall_hits = recall_hits + 1 WHERE name = ?').run('hit-entity'); + const row = db.prepare( + 'SELECT recall_hits FROM entities WHERE name = ?' + ).get('hit-entity') as { recall_hits: number }; + expect(row.recall_hits).toBe(2); + }); + + it('recall_misses increments correctly', () => { + remember({ name: 'miss-entity', type: 'decision', observations: ['test'] }); + db.prepare('UPDATE entities SET recall_misses = recall_misses + 1 WHERE name = ?').run('miss-entity'); + const row = db.prepare( + 'SELECT recall_misses FROM entities WHERE name = ?' + ).get('miss-entity') as { recall_misses: number }; + expect(row.recall_misses).toBe(1); + }); + + it('hit rate calculation is correct', () => { + remember({ name: 'rate-entity', type: 'decision', observations: ['test'] }); + db.prepare('UPDATE entities SET recall_hits = 7, recall_misses = 3 WHERE name = ?').run('rate-entity'); + const row = db.prepare( + `SELECT CAST(recall_hits AS REAL) / (recall_hits + recall_misses) as hitRate + FROM entities WHERE name = ?` + ).get('rate-entity') as { hitRate: number }; + expect(row.hitRate).toBeCloseTo(0.7, 2); + }); + + it('overall hit rate aggregation works across multiple entities', () => { + remember({ name: 'agg-1', type: 'concept', observations: ['a'] }); + remember({ name: 'agg-2', type: 'concept', observations: ['b'] }); + db.prepare('UPDATE entities SET recall_hits = 5, recall_misses = 5 WHERE name = ?').run('agg-1'); + db.prepare('UPDATE entities SET recall_hits = 8, recall_misses = 2 WHERE name = ?').run('agg-2'); + const totals = db.prepare( + `SELECT COALESCE(SUM(recall_hits), 0) as hits, COALESCE(SUM(recall_misses), 0) as misses + FROM entities WHERE (recall_hits > 0 OR recall_misses > 0)` + ).get() as { hits: number; misses: number }; + expect(totals.hits).toBe(13); + expect(totals.misses).toBe(7); + const rate = totals.hits / (totals.hits + totals.misses); + expect(rate).toBeCloseTo(0.65, 2); + }); +});