This is page 28 of 47. Use http://codebase.md/doobidoo/mcp-memory-service?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .claude
│ ├── agents
│ │ ├── amp-bridge.md
│ │ ├── amp-pr-automator.md
│ │ ├── code-quality-guard.md
│ │ ├── gemini-pr-automator.md
│ │ └── github-release-manager.md
│ ├── settings.local.json.backup
│ └── settings.local.json.local
├── .commit-message
├── .dockerignore
├── .env.example
├── .env.sqlite.backup
├── .envnn#
├── .gitattributes
├── .github
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ ├── feature_request.yml
│ │ └── performance_issue.yml
│ ├── pull_request_template.md
│ └── workflows
│ ├── bridge-tests.yml
│ ├── CACHE_FIX.md
│ ├── claude-code-review.yml
│ ├── claude.yml
│ ├── cleanup-images.yml.disabled
│ ├── dev-setup-validation.yml
│ ├── docker-publish.yml
│ ├── LATEST_FIXES.md
│ ├── main-optimized.yml.disabled
│ ├── main.yml
│ ├── publish-and-test.yml
│ ├── README_OPTIMIZATION.md
│ ├── release-tag.yml.disabled
│ ├── release.yml
│ ├── roadmap-review-reminder.yml
│ ├── SECRET_CONDITIONAL_FIX.md
│ └── WORKFLOW_FIXES.md
├── .gitignore
├── .mcp.json.backup
├── .mcp.json.template
├── .pyscn
│ ├── .gitignore
│ └── reports
│ └── analyze_20251123_214224.html
├── AGENTS.md
├── archive
│ ├── deployment
│ │ ├── deploy_fastmcp_fixed.sh
│ │ ├── deploy_http_with_mcp.sh
│ │ └── deploy_mcp_v4.sh
│ ├── deployment-configs
│ │ ├── empty_config.yml
│ │ └── smithery.yaml
│ ├── development
│ │ └── test_fastmcp.py
│ ├── docs-removed-2025-08-23
│ │ ├── authentication.md
│ │ ├── claude_integration.md
│ │ ├── claude-code-compatibility.md
│ │ ├── claude-code-integration.md
│ │ ├── claude-code-quickstart.md
│ │ ├── claude-desktop-setup.md
│ │ ├── complete-setup-guide.md
│ │ ├── database-synchronization.md
│ │ ├── development
│ │ │ ├── autonomous-memory-consolidation.md
│ │ │ ├── CLEANUP_PLAN.md
│ │ │ ├── CLEANUP_README.md
│ │ │ ├── CLEANUP_SUMMARY.md
│ │ │ ├── dream-inspired-memory-consolidation.md
│ │ │ ├── hybrid-slm-memory-consolidation.md
│ │ │ ├── mcp-milestone.md
│ │ │ ├── multi-client-architecture.md
│ │ │ ├── test-results.md
│ │ │ └── TIMESTAMP_FIX_SUMMARY.md
│ │ ├── distributed-sync.md
│ │ ├── invocation_guide.md
│ │ ├── macos-intel.md
│ │ ├── master-guide.md
│ │ ├── mcp-client-configuration.md
│ │ ├── multi-client-server.md
│ │ ├── service-installation.md
│ │ ├── sessions
│ │ │ └── MCP_ENHANCEMENT_SESSION_MEMORY_v4.1.0.md
│ │ ├── UBUNTU_SETUP.md
│ │ ├── ubuntu.md
│ │ ├── windows-setup.md
│ │ └── windows.md
│ ├── docs-root-cleanup-2025-08-23
│ │ ├── AWESOME_LIST_SUBMISSION.md
│ │ ├── CLOUDFLARE_IMPLEMENTATION.md
│ │ ├── DOCUMENTATION_ANALYSIS.md
│ │ ├── DOCUMENTATION_CLEANUP_PLAN.md
│ │ ├── DOCUMENTATION_CONSOLIDATION_COMPLETE.md
│ │ ├── LITESTREAM_SETUP_GUIDE.md
│ │ ├── lm_studio_system_prompt.md
│ │ ├── PYTORCH_DOWNLOAD_FIX.md
│ │ └── README-ORIGINAL-BACKUP.md
│ ├── investigations
│ │ └── MACOS_HOOKS_INVESTIGATION.md
│ ├── litestream-configs-v6.3.0
│ │ ├── install_service.sh
│ │ ├── litestream_master_config_fixed.yml
│ │ ├── litestream_master_config.yml
│ │ ├── litestream_replica_config_fixed.yml
│ │ ├── litestream_replica_config.yml
│ │ ├── litestream_replica_simple.yml
│ │ ├── litestream-http.service
│ │ ├── litestream.service
│ │ └── requirements-cloudflare.txt
│ ├── release-notes
│ │ └── release-notes-v7.1.4.md
│ └── setup-development
│ ├── README.md
│ ├── setup_consolidation_mdns.sh
│ ├── STARTUP_SETUP_GUIDE.md
│ └── test_service.sh
├── CHANGELOG-HISTORIC.md
├── CHANGELOG.md
├── claude_commands
│ ├── memory-context.md
│ ├── memory-health.md
│ ├── memory-ingest-dir.md
│ ├── memory-ingest.md
│ ├── memory-recall.md
│ ├── memory-search.md
│ ├── memory-store.md
│ ├── README.md
│ └── session-start.md
├── claude-hooks
│ ├── config.json
│ ├── config.template.json
│ ├── CONFIGURATION.md
│ ├── core
│ │ ├── memory-retrieval.js
│ │ ├── mid-conversation.js
│ │ ├── session-end.js
│ │ ├── session-start.js
│ │ └── topic-change.js
│ ├── debug-pattern-test.js
│ ├── install_claude_hooks_windows.ps1
│ ├── install_hooks.py
│ ├── memory-mode-controller.js
│ ├── MIGRATION.md
│ ├── README-NATURAL-TRIGGERS.md
│ ├── README-phase2.md
│ ├── README.md
│ ├── simple-test.js
│ ├── statusline.sh
│ ├── test-adaptive-weights.js
│ ├── test-dual-protocol-hook.js
│ ├── test-mcp-hook.js
│ ├── test-natural-triggers.js
│ ├── test-recency-scoring.js
│ ├── tests
│ │ ├── integration-test.js
│ │ ├── phase2-integration-test.js
│ │ ├── test-code-execution.js
│ │ ├── test-cross-session.json
│ │ ├── test-session-tracking.json
│ │ └── test-threading.json
│ ├── utilities
│ │ ├── adaptive-pattern-detector.js
│ │ ├── context-formatter.js
│ │ ├── context-shift-detector.js
│ │ ├── conversation-analyzer.js
│ │ ├── dynamic-context-updater.js
│ │ ├── git-analyzer.js
│ │ ├── mcp-client.js
│ │ ├── memory-client.js
│ │ ├── memory-scorer.js
│ │ ├── performance-manager.js
│ │ ├── project-detector.js
│ │ ├── session-tracker.js
│ │ ├── tiered-conversation-monitor.js
│ │ └── version-checker.js
│ └── WINDOWS-SESSIONSTART-BUG.md
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Development-Sprint-November-2025.md
├── docs
│ ├── amp-cli-bridge.md
│ ├── api
│ │ ├── code-execution-interface.md
│ │ ├── memory-metadata-api.md
│ │ ├── PHASE1_IMPLEMENTATION_SUMMARY.md
│ │ ├── PHASE2_IMPLEMENTATION_SUMMARY.md
│ │ ├── PHASE2_REPORT.md
│ │ └── tag-standardization.md
│ ├── architecture
│ │ ├── search-enhancement-spec.md
│ │ └── search-examples.md
│ ├── architecture.md
│ ├── archive
│ │ └── obsolete-workflows
│ │ ├── load_memory_context.md
│ │ └── README.md
│ ├── assets
│ │ └── images
│ │ ├── dashboard-v3.3.0-preview.png
│ │ ├── memory-awareness-hooks-example.png
│ │ ├── project-infographic.svg
│ │ └── README.md
│ ├── CLAUDE_CODE_QUICK_REFERENCE.md
│ ├── cloudflare-setup.md
│ ├── deployment
│ │ ├── docker.md
│ │ ├── dual-service.md
│ │ ├── production-guide.md
│ │ └── systemd-service.md
│ ├── development
│ │ ├── ai-agent-instructions.md
│ │ ├── code-quality
│ │ │ ├── phase-2a-completion.md
│ │ │ ├── phase-2a-handle-get-prompt.md
│ │ │ ├── phase-2a-index.md
│ │ │ ├── phase-2a-install-package.md
│ │ │ └── phase-2b-session-summary.md
│ │ ├── code-quality-workflow.md
│ │ ├── dashboard-workflow.md
│ │ ├── issue-management.md
│ │ ├── pr-review-guide.md
│ │ ├── refactoring-notes.md
│ │ ├── release-checklist.md
│ │ └── todo-tracker.md
│ ├── docker-optimized-build.md
│ ├── document-ingestion.md
│ ├── DOCUMENTATION_AUDIT.md
│ ├── enhancement-roadmap-issue-14.md
│ ├── examples
│ │ ├── analysis-scripts.js
│ │ ├── maintenance-session-example.md
│ │ ├── memory-distribution-chart.jsx
│ │ └── tag-schema.json
│ ├── first-time-setup.md
│ ├── glama-deployment.md
│ ├── guides
│ │ ├── advanced-command-examples.md
│ │ ├── chromadb-migration.md
│ │ ├── commands-vs-mcp-server.md
│ │ ├── mcp-enhancements.md
│ │ ├── mdns-service-discovery.md
│ │ ├── memory-consolidation-guide.md
│ │ ├── migration.md
│ │ ├── scripts.md
│ │ └── STORAGE_BACKENDS.md
│ ├── HOOK_IMPROVEMENTS.md
│ ├── hooks
│ │ └── phase2-code-execution-migration.md
│ ├── http-server-management.md
│ ├── ide-compatability.md
│ ├── IMAGE_RETENTION_POLICY.md
│ ├── images
│ │ └── dashboard-placeholder.md
│ ├── implementation
│ │ ├── health_checks.md
│ │ └── performance.md
│ ├── IMPLEMENTATION_PLAN_HTTP_SSE.md
│ ├── integration
│ │ ├── homebrew.md
│ │ └── multi-client.md
│ ├── integrations
│ │ ├── gemini.md
│ │ ├── groq-bridge.md
│ │ ├── groq-integration-summary.md
│ │ └── groq-model-comparison.md
│ ├── integrations.md
│ ├── legacy
│ │ └── dual-protocol-hooks.md
│ ├── LM_STUDIO_COMPATIBILITY.md
│ ├── maintenance
│ │ └── memory-maintenance.md
│ ├── mastery
│ │ ├── api-reference.md
│ │ ├── architecture-overview.md
│ │ ├── configuration-guide.md
│ │ ├── local-setup-and-run.md
│ │ ├── testing-guide.md
│ │ └── troubleshooting.md
│ ├── migration
│ │ └── code-execution-api-quick-start.md
│ ├── natural-memory-triggers
│ │ ├── cli-reference.md
│ │ ├── installation-guide.md
│ │ └── performance-optimization.md
│ ├── oauth-setup.md
│ ├── pr-graphql-integration.md
│ ├── quick-setup-cloudflare-dual-environment.md
│ ├── README.md
│ ├── remote-configuration-wiki-section.md
│ ├── research
│ │ ├── code-execution-interface-implementation.md
│ │ └── code-execution-interface-summary.md
│ ├── ROADMAP.md
│ ├── sqlite-vec-backend.md
│ ├── statistics
│ │ ├── charts
│ │ │ ├── activity_patterns.png
│ │ │ ├── contributors.png
│ │ │ ├── growth_trajectory.png
│ │ │ ├── monthly_activity.png
│ │ │ └── october_sprint.png
│ │ ├── data
│ │ │ ├── activity_by_day.csv
│ │ │ ├── activity_by_hour.csv
│ │ │ ├── contributors.csv
│ │ │ └── monthly_activity.csv
│ │ ├── generate_charts.py
│ │ └── REPOSITORY_STATISTICS.md
│ ├── technical
│ │ ├── development.md
│ │ ├── memory-migration.md
│ │ ├── migration-log.md
│ │ ├── sqlite-vec-embedding-fixes.md
│ │ └── tag-storage.md
│ ├── testing
│ │ └── regression-tests.md
│ ├── testing-cloudflare-backend.md
│ ├── troubleshooting
│ │ ├── cloudflare-api-token-setup.md
│ │ ├── cloudflare-authentication.md
│ │ ├── general.md
│ │ ├── hooks-quick-reference.md
│ │ ├── pr162-schema-caching-issue.md
│ │ ├── session-end-hooks.md
│ │ └── sync-issues.md
│ └── tutorials
│ ├── advanced-techniques.md
│ ├── data-analysis.md
│ └── demo-session-walkthrough.md
├── examples
│ ├── claude_desktop_config_template.json
│ ├── claude_desktop_config_windows.json
│ ├── claude-desktop-http-config.json
│ ├── config
│ │ └── claude_desktop_config.json
│ ├── http-mcp-bridge.js
│ ├── memory_export_template.json
│ ├── README.md
│ ├── setup
│ │ └── setup_multi_client_complete.py
│ └── start_https_example.sh
├── install_service.py
├── install.py
├── LICENSE
├── NOTICE
├── pyproject.toml
├── pytest.ini
├── README.md
├── run_server.py
├── scripts
│ ├── .claude
│ │ └── settings.local.json
│ ├── archive
│ │ └── check_missing_timestamps.py
│ ├── backup
│ │ ├── backup_memories.py
│ │ ├── backup_sqlite_vec.sh
│ │ ├── export_distributable_memories.sh
│ │ └── restore_memories.py
│ ├── benchmarks
│ │ ├── benchmark_code_execution_api.py
│ │ ├── benchmark_hybrid_sync.py
│ │ └── benchmark_server_caching.py
│ ├── database
│ │ ├── analyze_sqlite_vec_db.py
│ │ ├── check_sqlite_vec_status.py
│ │ ├── db_health_check.py
│ │ └── simple_timestamp_check.py
│ ├── development
│ │ ├── debug_server_initialization.py
│ │ ├── find_orphaned_files.py
│ │ ├── fix_mdns.sh
│ │ ├── fix_sitecustomize.py
│ │ ├── remote_ingest.sh
│ │ ├── setup-git-merge-drivers.sh
│ │ ├── uv-lock-merge.sh
│ │ └── verify_hybrid_sync.py
│ ├── hooks
│ │ └── pre-commit
│ ├── installation
│ │ ├── install_linux_service.py
│ │ ├── install_macos_service.py
│ │ ├── install_uv.py
│ │ ├── install_windows_service.py
│ │ ├── install.py
│ │ ├── setup_backup_cron.sh
│ │ ├── setup_claude_mcp.sh
│ │ └── setup_cloudflare_resources.py
│ ├── linux
│ │ ├── service_status.sh
│ │ ├── start_service.sh
│ │ ├── stop_service.sh
│ │ ├── uninstall_service.sh
│ │ └── view_logs.sh
│ ├── maintenance
│ │ ├── assign_memory_types.py
│ │ ├── check_memory_types.py
│ │ ├── cleanup_corrupted_encoding.py
│ │ ├── cleanup_memories.py
│ │ ├── cleanup_organize.py
│ │ ├── consolidate_memory_types.py
│ │ ├── consolidation_mappings.json
│ │ ├── delete_orphaned_vectors_fixed.py
│ │ ├── fast_cleanup_duplicates_with_tracking.sh
│ │ ├── find_all_duplicates.py
│ │ ├── find_cloudflare_duplicates.py
│ │ ├── find_duplicates.py
│ │ ├── memory-types.md
│ │ ├── README.md
│ │ ├── recover_timestamps_from_cloudflare.py
│ │ ├── regenerate_embeddings.py
│ │ ├── repair_malformed_tags.py
│ │ ├── repair_memories.py
│ │ ├── repair_sqlite_vec_embeddings.py
│ │ ├── repair_zero_embeddings.py
│ │ ├── restore_from_json_export.py
│ │ └── scan_todos.sh
│ ├── migration
│ │ ├── cleanup_mcp_timestamps.py
│ │ ├── legacy
│ │ │ └── migrate_chroma_to_sqlite.py
│ │ ├── mcp-migration.py
│ │ ├── migrate_sqlite_vec_embeddings.py
│ │ ├── migrate_storage.py
│ │ ├── migrate_tags.py
│ │ ├── migrate_timestamps.py
│ │ ├── migrate_to_cloudflare.py
│ │ ├── migrate_to_sqlite_vec.py
│ │ ├── migrate_v5_enhanced.py
│ │ ├── TIMESTAMP_CLEANUP_README.md
│ │ └── verify_mcp_timestamps.py
│ ├── pr
│ │ ├── amp_collect_results.sh
│ │ ├── amp_detect_breaking_changes.sh
│ │ ├── amp_generate_tests.sh
│ │ ├── amp_pr_review.sh
│ │ ├── amp_quality_gate.sh
│ │ ├── amp_suggest_fixes.sh
│ │ ├── auto_review.sh
│ │ ├── detect_breaking_changes.sh
│ │ ├── generate_tests.sh
│ │ ├── lib
│ │ │ └── graphql_helpers.sh
│ │ ├── quality_gate.sh
│ │ ├── resolve_threads.sh
│ │ ├── run_pyscn_analysis.sh
│ │ ├── run_quality_checks.sh
│ │ ├── thread_status.sh
│ │ └── watch_reviews.sh
│ ├── quality
│ │ ├── fix_dead_code_install.sh
│ │ ├── phase1_dead_code_analysis.md
│ │ ├── phase2_complexity_analysis.md
│ │ ├── README_PHASE1.md
│ │ ├── README_PHASE2.md
│ │ ├── track_pyscn_metrics.sh
│ │ └── weekly_quality_review.sh
│ ├── README.md
│ ├── run
│ │ ├── run_mcp_memory.sh
│ │ ├── run-with-uv.sh
│ │ └── start_sqlite_vec.sh
│ ├── run_memory_server.py
│ ├── server
│ │ ├── check_http_server.py
│ │ ├── check_server_health.py
│ │ ├── memory_offline.py
│ │ ├── preload_models.py
│ │ ├── run_http_server.py
│ │ ├── run_memory_server.py
│ │ ├── start_http_server.bat
│ │ └── start_http_server.sh
│ ├── service
│ │ ├── deploy_dual_services.sh
│ │ ├── install_http_service.sh
│ │ ├── mcp-memory-http.service
│ │ ├── mcp-memory.service
│ │ ├── memory_service_manager.sh
│ │ ├── service_control.sh
│ │ ├── service_utils.py
│ │ └── update_service.sh
│ ├── sync
│ │ ├── check_drift.py
│ │ ├── claude_sync_commands.py
│ │ ├── export_memories.py
│ │ ├── import_memories.py
│ │ ├── litestream
│ │ │ ├── apply_local_changes.sh
│ │ │ ├── enhanced_memory_store.sh
│ │ │ ├── init_staging_db.sh
│ │ │ ├── io.litestream.replication.plist
│ │ │ ├── manual_sync.sh
│ │ │ ├── memory_sync.sh
│ │ │ ├── pull_remote_changes.sh
│ │ │ ├── push_to_remote.sh
│ │ │ ├── README.md
│ │ │ ├── resolve_conflicts.sh
│ │ │ ├── setup_local_litestream.sh
│ │ │ ├── setup_remote_litestream.sh
│ │ │ ├── staging_db_init.sql
│ │ │ ├── stash_local_changes.sh
│ │ │ ├── sync_from_remote_noconfig.sh
│ │ │ └── sync_from_remote.sh
│ │ ├── README.md
│ │ ├── safe_cloudflare_update.sh
│ │ ├── sync_memory_backends.py
│ │ └── sync_now.py
│ ├── testing
│ │ ├── run_complete_test.py
│ │ ├── run_memory_test.sh
│ │ ├── simple_test.py
│ │ ├── test_cleanup_logic.py
│ │ ├── test_cloudflare_backend.py
│ │ ├── test_docker_functionality.py
│ │ ├── test_installation.py
│ │ ├── test_mdns.py
│ │ ├── test_memory_api.py
│ │ ├── test_memory_simple.py
│ │ ├── test_migration.py
│ │ ├── test_search_api.py
│ │ ├── test_sqlite_vec_embeddings.py
│ │ ├── test_sse_events.py
│ │ ├── test-connection.py
│ │ └── test-hook.js
│ ├── utils
│ │ ├── claude_commands_utils.py
│ │ ├── generate_personalized_claude_md.sh
│ │ ├── groq
│ │ ├── groq_agent_bridge.py
│ │ ├── list-collections.py
│ │ ├── memory_wrapper_uv.py
│ │ ├── query_memories.py
│ │ ├── smithery_wrapper.py
│ │ ├── test_groq_bridge.sh
│ │ └── uv_wrapper.py
│ └── validation
│ ├── check_dev_setup.py
│ ├── check_documentation_links.py
│ ├── diagnose_backend_config.py
│ ├── validate_configuration_complete.py
│ ├── validate_memories.py
│ ├── validate_migration.py
│ ├── validate_timestamp_integrity.py
│ ├── verify_environment.py
│ ├── verify_pytorch_windows.py
│ └── verify_torch.py
├── SECURITY.md
├── selective_timestamp_recovery.py
├── SPONSORS.md
├── src
│ └── mcp_memory_service
│ ├── __init__.py
│ ├── api
│ │ ├── __init__.py
│ │ ├── client.py
│ │ ├── operations.py
│ │ ├── sync_wrapper.py
│ │ └── types.py
│ ├── backup
│ │ ├── __init__.py
│ │ └── scheduler.py
│ ├── cli
│ │ ├── __init__.py
│ │ ├── ingestion.py
│ │ ├── main.py
│ │ └── utils.py
│ ├── config.py
│ ├── consolidation
│ │ ├── __init__.py
│ │ ├── associations.py
│ │ ├── base.py
│ │ ├── clustering.py
│ │ ├── compression.py
│ │ ├── consolidator.py
│ │ ├── decay.py
│ │ ├── forgetting.py
│ │ ├── health.py
│ │ └── scheduler.py
│ ├── dependency_check.py
│ ├── discovery
│ │ ├── __init__.py
│ │ ├── client.py
│ │ └── mdns_service.py
│ ├── embeddings
│ │ ├── __init__.py
│ │ └── onnx_embeddings.py
│ ├── ingestion
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── chunker.py
│ │ ├── csv_loader.py
│ │ ├── json_loader.py
│ │ ├── pdf_loader.py
│ │ ├── registry.py
│ │ ├── semtools_loader.py
│ │ └── text_loader.py
│ ├── lm_studio_compat.py
│ ├── mcp_server.py
│ ├── models
│ │ ├── __init__.py
│ │ └── memory.py
│ ├── server.py
│ ├── services
│ │ ├── __init__.py
│ │ └── memory_service.py
│ ├── storage
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── cloudflare.py
│ │ ├── factory.py
│ │ ├── http_client.py
│ │ ├── hybrid.py
│ │ └── sqlite_vec.py
│ ├── sync
│ │ ├── __init__.py
│ │ ├── exporter.py
│ │ ├── importer.py
│ │ └── litestream_config.py
│ ├── utils
│ │ ├── __init__.py
│ │ ├── cache_manager.py
│ │ ├── content_splitter.py
│ │ ├── db_utils.py
│ │ ├── debug.py
│ │ ├── document_processing.py
│ │ ├── gpu_detection.py
│ │ ├── hashing.py
│ │ ├── http_server_manager.py
│ │ ├── port_detection.py
│ │ ├── system_detection.py
│ │ └── time_parser.py
│ └── web
│ ├── __init__.py
│ ├── api
│ │ ├── __init__.py
│ │ ├── analytics.py
│ │ ├── backup.py
│ │ ├── consolidation.py
│ │ ├── documents.py
│ │ ├── events.py
│ │ ├── health.py
│ │ ├── manage.py
│ │ ├── mcp.py
│ │ ├── memories.py
│ │ ├── search.py
│ │ └── sync.py
│ ├── app.py
│ ├── dependencies.py
│ ├── oauth
│ │ ├── __init__.py
│ │ ├── authorization.py
│ │ ├── discovery.py
│ │ ├── middleware.py
│ │ ├── models.py
│ │ ├── registration.py
│ │ └── storage.py
│ ├── sse.py
│ └── static
│ ├── app.js
│ ├── index.html
│ ├── README.md
│ ├── sse_test.html
│ └── style.css
├── start_http_debug.bat
├── start_http_server.sh
├── test_document.txt
├── test_version_checker.js
├── tests
│ ├── __init__.py
│ ├── api
│ │ ├── __init__.py
│ │ ├── test_compact_types.py
│ │ └── test_operations.py
│ ├── bridge
│ │ ├── mock_responses.js
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ └── test_http_mcp_bridge.js
│ ├── conftest.py
│ ├── consolidation
│ │ ├── __init__.py
│ │ ├── conftest.py
│ │ ├── test_associations.py
│ │ ├── test_clustering.py
│ │ ├── test_compression.py
│ │ ├── test_consolidator.py
│ │ ├── test_decay.py
│ │ └── test_forgetting.py
│ ├── contracts
│ │ └── api-specification.yml
│ ├── integration
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── test_api_key_fallback.py
│ │ ├── test_api_memories_chronological.py
│ │ ├── test_api_tag_time_search.py
│ │ ├── test_api_with_memory_service.py
│ │ ├── test_bridge_integration.js
│ │ ├── test_cli_interfaces.py
│ │ ├── test_cloudflare_connection.py
│ │ ├── test_concurrent_clients.py
│ │ ├── test_data_serialization_consistency.py
│ │ ├── test_http_server_startup.py
│ │ ├── test_mcp_memory.py
│ │ ├── test_mdns_integration.py
│ │ ├── test_oauth_basic_auth.py
│ │ ├── test_oauth_flow.py
│ │ ├── test_server_handlers.py
│ │ └── test_store_memory.py
│ ├── performance
│ │ ├── test_background_sync.py
│ │ └── test_hybrid_live.py
│ ├── README.md
│ ├── smithery
│ │ └── test_smithery.py
│ ├── sqlite
│ │ └── simple_sqlite_vec_test.py
│ ├── test_client.py
│ ├── test_content_splitting.py
│ ├── test_database.py
│ ├── test_hybrid_cloudflare_limits.py
│ ├── test_hybrid_storage.py
│ ├── test_memory_ops.py
│ ├── test_semantic_search.py
│ ├── test_sqlite_vec_storage.py
│ ├── test_time_parser.py
│ ├── test_timestamp_preservation.py
│ ├── timestamp
│ │ ├── test_hook_vs_manual_storage.py
│ │ ├── test_issue99_final_validation.py
│ │ ├── test_search_retrieval_inconsistency.py
│ │ ├── test_timestamp_issue.py
│ │ └── test_timestamp_simple.py
│ └── unit
│ ├── conftest.py
│ ├── test_cloudflare_storage.py
│ ├── test_csv_loader.py
│ ├── test_fastapi_dependencies.py
│ ├── test_import.py
│ ├── test_json_loader.py
│ ├── test_mdns_simple.py
│ ├── test_mdns.py
│ ├── test_memory_service.py
│ ├── test_memory.py
│ ├── test_semtools_loader.py
│ ├── test_storage_interface_compatibility.py
│ └── test_tag_time_filtering.py
├── tools
│ ├── docker
│ │ ├── DEPRECATED.md
│ │ ├── docker-compose.http.yml
│ │ ├── docker-compose.pythonpath.yml
│ │ ├── docker-compose.standalone.yml
│ │ ├── docker-compose.uv.yml
│ │ ├── docker-compose.yml
│ │ ├── docker-entrypoint-persistent.sh
│ │ ├── docker-entrypoint-unified.sh
│ │ ├── docker-entrypoint.sh
│ │ ├── Dockerfile
│ │ ├── Dockerfile.glama
│ │ ├── Dockerfile.slim
│ │ ├── README.md
│ │ └── test-docker-modes.sh
│ └── README.md
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/scripts/validation/validate_migration.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | # Copyright 2024 Heinrich Krupp
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | """
17 | Migration Validation Script for MCP Memory Service
18 |
19 | This script validates that a migration from ChromaDB to SQLite-vec was successful
20 | by checking data integrity, required fields, and identifying any corruption.
21 | """
22 |
23 | import argparse
24 | import asyncio
25 | import hashlib
26 | import json
27 | import os
28 | import sqlite3
29 | import sys
30 | from datetime import datetime
31 | from pathlib import Path
32 | from typing import List, Dict, Any, Optional, Tuple
33 |
34 | # Add project root to path
35 | project_root = Path(__file__).parent.parent
36 | sys.path.insert(0, str(project_root / "src"))
37 |
38 | try:
39 | from mcp_memory_service.storage.chroma import ChromaMemoryStorage
40 | from mcp_memory_service.storage.sqlite_vec import SqliteVecMemoryStorage
41 | from mcp_memory_service.utils.hashing import generate_content_hash
42 | IMPORTS_AVAILABLE = True
43 | except ImportError:
44 | IMPORTS_AVAILABLE = False
45 | print("Warning: MCP modules not available. Running in standalone mode.")
46 |
47 |
48 | class ValidationReport:
49 | """Container for validation results."""
50 |
51 | def __init__(self):
52 | self.total_memories = 0
53 | self.valid_memories = 0
54 | self.issues = []
55 | self.warnings = []
56 | self.tag_issues = []
57 | self.hash_mismatches = []
58 | self.missing_fields = []
59 | self.timestamp_issues = []
60 | self.encoding_issues = []
61 |
62 | def add_issue(self, issue: str):
63 | """Add a critical issue."""
64 | self.issues.append(issue)
65 |
66 | def add_warning(self, warning: str):
67 | """Add a warning."""
68 | self.warnings.append(warning)
69 |
70 | def is_valid(self) -> bool:
71 | """Check if validation passed."""
72 | return len(self.issues) == 0
73 |
74 | def print_report(self):
75 | """Print the validation report."""
76 | print("\n" + "="*60)
77 | print("MIGRATION VALIDATION REPORT")
78 | print("="*60)
79 |
80 | print(f"\n📊 Statistics:")
81 | print(f" Total memories: {self.total_memories}")
82 | print(f" Valid memories: {self.valid_memories}")
83 | print(f" Validation rate: {self.valid_memories/self.total_memories*100:.1f}%" if self.total_memories > 0 else "N/A")
84 |
85 | if self.issues:
86 | print(f"\n❌ Critical Issues ({len(self.issues)}):")
87 | for i, issue in enumerate(self.issues[:10], 1):
88 | print(f" {i}. {issue}")
89 | if len(self.issues) > 10:
90 | print(f" ... and {len(self.issues) - 10} more")
91 |
92 | if self.warnings:
93 | print(f"\n⚠️ Warnings ({len(self.warnings)}):")
94 | for i, warning in enumerate(self.warnings[:10], 1):
95 | print(f" {i}. {warning}")
96 | if len(self.warnings) > 10:
97 | print(f" ... and {len(self.warnings) - 10} more")
98 |
99 | if self.tag_issues:
100 | print(f"\n🏷️ Tag Issues ({len(self.tag_issues)}):")
101 | for i, issue in enumerate(self.tag_issues[:5], 1):
102 | print(f" {i}. {issue}")
103 | if len(self.tag_issues) > 5:
104 | print(f" ... and {len(self.tag_issues) - 5} more")
105 |
106 | if self.hash_mismatches:
107 | print(f"\n🔑 Hash Mismatches ({len(self.hash_mismatches)}):")
108 | for i, mismatch in enumerate(self.hash_mismatches[:5], 1):
109 | print(f" {i}. {mismatch}")
110 | if len(self.hash_mismatches) > 5:
111 | print(f" ... and {len(self.hash_mismatches) - 5} more")
112 |
113 | if self.timestamp_issues:
114 | print(f"\n🕐 Timestamp Issues ({len(self.timestamp_issues)}):")
115 | for i, issue in enumerate(self.timestamp_issues[:5], 1):
116 | print(f" {i}. {issue}")
117 | if len(self.timestamp_issues) > 5:
118 | print(f" ... and {len(self.timestamp_issues) - 5} more")
119 |
120 | # Final verdict
121 | print("\n" + "="*60)
122 | if self.is_valid():
123 | print("✅ VALIDATION PASSED")
124 | if self.warnings:
125 | print(f" (with {len(self.warnings)} warnings)")
126 | else:
127 | print("❌ VALIDATION FAILED")
128 | print(f" Found {len(self.issues)} critical issues")
129 | print("="*60)
130 |
131 |
132 | class MigrationValidator:
133 | """Tool for validating migrated data."""
134 |
135 | def __init__(self, sqlite_path: str, chroma_path: Optional[str] = None):
136 | self.sqlite_path = sqlite_path
137 | self.chroma_path = chroma_path
138 | self.report = ValidationReport()
139 |
140 | def validate_content_hash(self, content: str, stored_hash: str) -> bool:
141 | """Validate that content hash is correct."""
142 | if not stored_hash:
143 | return False
144 |
145 | # Generate expected hash
146 | expected_hash = hashlib.sha256(content.encode()).hexdigest()
147 | return expected_hash == stored_hash
148 |
149 | def validate_tags(self, tags_str: str) -> Tuple[bool, List[str]]:
150 | """Validate tag format and return cleaned tags."""
151 | if not tags_str:
152 | return True, []
153 |
154 | try:
155 | # Tags should be comma-separated
156 | tags = [tag.strip() for tag in tags_str.split(',') if tag.strip()]
157 |
158 | # Check for common corruption patterns
159 | issues = []
160 | for tag in tags:
161 | if '\n' in tag or '\r' in tag:
162 | issues.append(f"Newline in tag: {repr(tag)}")
163 | if len(tag) > 100:
164 | issues.append(f"Tag too long: {tag[:50]}...")
165 | if tag.startswith('[') or tag.endswith(']'):
166 | issues.append(f"Array syntax in tag: {tag}")
167 |
168 | return len(issues) == 0, tags
169 |
170 | except Exception as e:
171 | return False, []
172 |
173 | def validate_timestamp(self, timestamp: Any, field_name: str) -> bool:
174 | """Validate timestamp value."""
175 | if timestamp is None:
176 | return False
177 |
178 | try:
179 | if isinstance(timestamp, (int, float)):
180 | # Check if timestamp is reasonable (between 2000 and 2100)
181 | if 946684800 <= float(timestamp) <= 4102444800:
182 | return True
183 | return False
184 | except:
185 | return False
186 |
187 | def validate_metadata(self, metadata_str: str) -> Tuple[bool, Dict]:
188 | """Validate metadata JSON."""
189 | if not metadata_str:
190 | return True, {}
191 |
192 | try:
193 | metadata = json.loads(metadata_str)
194 | if not isinstance(metadata, dict):
195 | return False, {}
196 | return True, metadata
197 | except json.JSONDecodeError:
198 | return False, {}
199 |
200 | async def validate_sqlite_database(self) -> bool:
201 | """Validate the SQLite-vec database."""
202 | print(f"Validating SQLite database: {self.sqlite_path}")
203 |
204 | if not Path(self.sqlite_path).exists():
205 | self.report.add_issue(f"Database file not found: {self.sqlite_path}")
206 | return False
207 |
208 | try:
209 | conn = sqlite3.connect(self.sqlite_path)
210 |
211 | # Check if tables exist
212 | tables = conn.execute(
213 | "SELECT name FROM sqlite_master WHERE type='table'"
214 | ).fetchall()
215 | table_names = [t[0] for t in tables]
216 |
217 | if 'memories' not in table_names:
218 | self.report.add_issue("Missing 'memories' table")
219 | return False
220 |
221 | # Check schema
222 | schema = conn.execute("PRAGMA table_info(memories)").fetchall()
223 | column_names = [col[1] for col in schema]
224 |
225 | required_columns = [
226 | 'content_hash', 'content', 'tags', 'memory_type',
227 | 'metadata', 'created_at', 'updated_at'
228 | ]
229 |
230 | for col in required_columns:
231 | if col not in column_names:
232 | self.report.add_issue(f"Missing required column: {col}")
233 |
234 | # Validate each memory
235 | cursor = conn.execute("""
236 | SELECT id, content_hash, content, tags, memory_type,
237 | metadata, created_at, updated_at
238 | FROM memories
239 | ORDER BY id
240 | """)
241 |
242 | for row in cursor:
243 | memory_id, content_hash, content, tags, memory_type = row[:5]
244 | metadata, created_at, updated_at = row[5:]
245 |
246 | self.report.total_memories += 1
247 | memory_valid = True
248 |
249 | # Validate content and hash
250 | if not content:
251 | self.report.missing_fields.append(f"Memory {memory_id}: missing content")
252 | memory_valid = False
253 | elif not content_hash:
254 | self.report.missing_fields.append(f"Memory {memory_id}: missing content_hash")
255 | memory_valid = False
256 | elif not self.validate_content_hash(content, content_hash):
257 | self.report.hash_mismatches.append(
258 | f"Memory {memory_id}: hash mismatch (hash: {content_hash[:8]}...)"
259 | )
260 | memory_valid = False
261 |
262 | # Validate tags
263 | if tags:
264 | valid_tags, cleaned_tags = self.validate_tags(tags)
265 | if not valid_tags:
266 | self.report.tag_issues.append(
267 | f"Memory {memory_id}: malformed tags: {tags[:50]}..."
268 | )
269 |
270 | # Validate timestamps
271 | if not self.validate_timestamp(created_at, 'created_at'):
272 | self.report.timestamp_issues.append(
273 | f"Memory {memory_id}: invalid created_at: {created_at}"
274 | )
275 | memory_valid = False
276 |
277 | if not self.validate_timestamp(updated_at, 'updated_at'):
278 | self.report.timestamp_issues.append(
279 | f"Memory {memory_id}: invalid updated_at: {updated_at}"
280 | )
281 |
282 | # Validate metadata
283 | if metadata:
284 | valid_meta, meta_dict = self.validate_metadata(metadata)
285 | if not valid_meta:
286 | self.report.warnings.append(
287 | f"Memory {memory_id}: invalid metadata JSON"
288 | )
289 |
290 | # Check for encoding issues
291 | try:
292 | content.encode('utf-8')
293 | except UnicodeEncodeError:
294 | self.report.encoding_issues.append(
295 | f"Memory {memory_id}: encoding issues in content"
296 | )
297 | memory_valid = False
298 |
299 | if memory_valid:
300 | self.report.valid_memories += 1
301 |
302 | conn.close()
303 |
304 | # Check for critical issues
305 | if self.report.total_memories == 0:
306 | self.report.add_issue("No memories found in database")
307 | elif self.report.valid_memories < self.report.total_memories * 0.5:
308 | self.report.add_issue(
309 | f"Less than 50% of memories are valid ({self.report.valid_memories}/{self.report.total_memories})"
310 | )
311 |
312 | return True
313 |
314 | except Exception as e:
315 | self.report.add_issue(f"Database validation error: {e}")
316 | return False
317 |
318 | async def compare_with_chroma(self) -> bool:
319 | """Compare migrated data with original ChromaDB."""
320 | if not self.chroma_path or not IMPORTS_AVAILABLE:
321 | print("Skipping ChromaDB comparison (not available)")
322 | return True
323 |
324 | print(f"Comparing with ChromaDB: {self.chroma_path}")
325 |
326 | try:
327 | # Load ChromaDB memories
328 | chroma_storage = ChromaMemoryStorage(path=self.chroma_path)
329 | collection = chroma_storage.collection
330 |
331 | if not collection:
332 | self.report.add_warning("Could not access ChromaDB collection")
333 | return True
334 |
335 | # Get count from ChromaDB
336 | chroma_results = collection.get()
337 | chroma_count = len(chroma_results.get('ids', []))
338 |
339 | print(f" ChromaDB memories: {chroma_count}")
340 | print(f" SQLite memories: {self.report.total_memories}")
341 |
342 | if chroma_count > 0:
343 | migration_rate = self.report.total_memories / chroma_count * 100
344 | if migration_rate < 95:
345 | self.report.add_warning(
346 | f"Only {migration_rate:.1f}% of ChromaDB memories were migrated"
347 | )
348 | elif migration_rate > 105:
349 | self.report.add_warning(
350 | f"SQLite has {migration_rate:.1f}% of ChromaDB count (possible duplicates)"
351 | )
352 |
353 | return True
354 |
355 | except Exception as e:
356 | self.report.add_warning(f"Could not compare with ChromaDB: {e}")
357 | return True
358 |
359 | async def run(self) -> bool:
360 | """Run the validation process."""
361 | print("\n" + "="*60)
362 | print("MCP Memory Service - Migration Validator")
363 | print("="*60)
364 |
365 | # Validate SQLite database
366 | if not await self.validate_sqlite_database():
367 | self.report.print_report()
368 | return False
369 |
370 | # Compare with ChromaDB if available
371 | if self.chroma_path:
372 | await self.compare_with_chroma()
373 |
374 | # Print report
375 | self.report.print_report()
376 |
377 | return self.report.is_valid()
378 |
379 |
380 | def find_databases() -> Tuple[Optional[str], Optional[str]]:
381 | """Try to find SQLite and ChromaDB databases."""
382 | sqlite_path = None
383 | chroma_path = None
384 |
385 | # Check environment variables
386 | sqlite_path = os.environ.get('MCP_MEMORY_SQLITE_PATH')
387 | chroma_path = os.environ.get('MCP_MEMORY_CHROMA_PATH')
388 |
389 | # Check default locations
390 | home = Path.home()
391 | if sys.platform == 'darwin': # macOS
392 | default_base = home / 'Library' / 'Application Support' / 'mcp-memory'
393 | elif sys.platform == 'win32': # Windows
394 | default_base = Path(os.getenv('LOCALAPPDATA', '')) / 'mcp-memory'
395 | else: # Linux
396 | default_base = home / '.local' / 'share' / 'mcp-memory'
397 |
398 | if not sqlite_path:
399 | # Try to find SQLite database
400 | possible_sqlite = [
401 | default_base / 'sqlite_vec.db',
402 | default_base / 'sqlite_vec_migrated.db',
403 | default_base / 'memory_migrated.db',
404 | Path.cwd() / 'sqlite_vec.db',
405 | ]
406 |
407 | for path in possible_sqlite:
408 | if path.exists():
409 | sqlite_path = str(path)
410 | print(f"Found SQLite database: {path}")
411 | break
412 |
413 | if not chroma_path:
414 | # Try to find ChromaDB
415 | possible_chroma = [
416 | home / '.mcp_memory_chroma',
417 | default_base / 'chroma_db',
418 | Path.cwd() / 'chroma_db',
419 | ]
420 |
421 | for path in possible_chroma:
422 | if path.exists():
423 | chroma_path = str(path)
424 | print(f"Found ChromaDB: {path}")
425 | break
426 |
427 | return sqlite_path, chroma_path
428 |
429 |
430 | def main():
431 | """Main entry point."""
432 | parser = argparse.ArgumentParser(
433 | description="Validate ChromaDB to SQLite-vec migration",
434 | formatter_class=argparse.RawDescriptionHelpFormatter
435 | )
436 |
437 | parser.add_argument(
438 | 'sqlite_path',
439 | nargs='?',
440 | help='Path to SQLite-vec database (default: auto-detect)'
441 | )
442 | parser.add_argument(
443 | '--chroma-path',
444 | help='Path to original ChromaDB for comparison'
445 | )
446 | parser.add_argument(
447 | '--compare',
448 | action='store_true',
449 | help='Compare with ChromaDB (requires ChromaDB path)'
450 | )
451 | parser.add_argument(
452 | '--fix',
453 | action='store_true',
454 | help='Attempt to fix common issues (experimental)'
455 | )
456 |
457 | args = parser.parse_args()
458 |
459 | # Find databases
460 | if args.sqlite_path:
461 | sqlite_path = args.sqlite_path
462 | else:
463 | sqlite_path, detected_chroma = find_databases()
464 | if not args.chroma_path and detected_chroma:
465 | args.chroma_path = detected_chroma
466 |
467 | if not sqlite_path:
468 | print("Error: Could not find SQLite database")
469 | print("Please specify the path or set MCP_MEMORY_SQLITE_PATH")
470 | return 1
471 |
472 | # Run validation
473 | validator = MigrationValidator(sqlite_path, args.chroma_path)
474 | success = asyncio.run(validator.run())
475 |
476 | return 0 if success else 1
477 |
478 |
479 | if __name__ == "__main__":
480 | sys.exit(main())
```
--------------------------------------------------------------------------------
/src/mcp_memory_service/consolidation/compression.py:
--------------------------------------------------------------------------------
```python
1 | # Copyright 2024 Heinrich Krupp
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Semantic compression engine for memory cluster summarization."""
16 |
17 | import numpy as np
18 | from typing import List, Dict, Any, Optional, Tuple, Set
19 | from datetime import datetime
20 | from dataclasses import dataclass
21 | from collections import Counter
22 | import re
23 | import hashlib
24 |
25 | from .base import ConsolidationBase, ConsolidationConfig, MemoryCluster
26 | from ..models.memory import Memory
27 |
28 | @dataclass
29 | class CompressionResult:
30 | """Result of compressing a memory cluster."""
31 | cluster_id: str
32 | compressed_memory: Memory
33 | compression_ratio: float
34 | key_concepts: List[str]
35 | temporal_span: Dict[str, Any]
36 | source_memory_count: int
37 | compression_metadata: Dict[str, Any]
38 |
39 | class SemanticCompressionEngine(ConsolidationBase):
40 | """
41 | Creates condensed representations of memory clusters for efficient storage.
42 |
43 | This creates higher-level abstractions while preserving key information,
44 | using statistical methods and concept extraction to summarize clusters.
45 | """
46 |
47 | def __init__(self, config: ConsolidationConfig):
48 | super().__init__(config)
49 | self.max_summary_length = config.max_summary_length
50 | self.preserve_originals = config.preserve_originals
51 |
52 | # Word importance patterns
53 | self._important_patterns = {
54 | 'technical_terms': re.compile(r'\b[A-Z][a-z]*[A-Z][a-zA-Z]*\b'), # CamelCase
55 | 'acronyms': re.compile(r'\b[A-Z]{2,}\b'),
56 | 'numbers': re.compile(r'\b\d+(?:\.\d+)?\b'),
57 | 'urls': re.compile(r'https?://[^\s]+'),
58 | 'file_paths': re.compile(r'[/\\][^\s]+'),
59 | 'quoted_text': re.compile(r'"([^"]*)"'),
60 | 'code_blocks': re.compile(r'```[\s\S]*?```|`[^`]+`')
61 | }
62 |
63 | async def process(self, clusters: List[MemoryCluster], memories: List[Memory], **kwargs) -> List[CompressionResult]:
64 | """Compress memory clusters into condensed representations."""
65 | if not clusters:
66 | return []
67 |
68 | # Create memory hash lookup
69 | memory_lookup = {m.content_hash: m for m in memories}
70 |
71 | compression_results = []
72 | for cluster in clusters:
73 | # Get memories for this cluster
74 | cluster_memories = []
75 | for hash_val in cluster.memory_hashes:
76 | if hash_val in memory_lookup:
77 | cluster_memories.append(memory_lookup[hash_val])
78 |
79 | if not cluster_memories:
80 | continue
81 |
82 | # Compress the cluster
83 | result = await self._compress_cluster(cluster, cluster_memories)
84 | if result:
85 | compression_results.append(result)
86 |
87 | self.logger.info(f"Compressed {len(compression_results)} clusters")
88 | return compression_results
89 |
90 | async def _compress_cluster(self, cluster: MemoryCluster, memories: List[Memory]) -> Optional[CompressionResult]:
91 | """Compress a single memory cluster."""
92 | if len(memories) < 2:
93 | return None
94 |
95 | # Extract key concepts and themes
96 | key_concepts = await self._extract_key_concepts(memories, cluster.theme_keywords)
97 |
98 | # Generate thematic summary
99 | summary = await self._generate_thematic_summary(memories, key_concepts)
100 |
101 | # Calculate temporal information
102 | temporal_span = self._calculate_temporal_span(memories)
103 |
104 | # Aggregate tags and metadata
105 | aggregated_tags = self._aggregate_tags(memories)
106 | aggregated_metadata = self._aggregate_metadata(memories)
107 |
108 | # Create compressed memory embedding (cluster centroid)
109 | compressed_embedding = cluster.centroid_embedding
110 |
111 | # Calculate compression ratio
112 | original_size = sum(len(m.content) for m in memories)
113 | compressed_size = len(summary)
114 | compression_ratio = compressed_size / original_size if original_size > 0 else 0
115 |
116 | # Create content hash for the compressed memory
117 | content_hash = hashlib.sha256(summary.encode()).hexdigest()
118 |
119 | # Create compressed memory object
120 | compressed_memory = Memory(
121 | content=summary,
122 | content_hash=content_hash,
123 | tags=aggregated_tags,
124 | memory_type='compressed_cluster',
125 | metadata={
126 | **aggregated_metadata,
127 | 'cluster_id': cluster.cluster_id,
128 | 'compression_date': datetime.now().isoformat(),
129 | 'source_memory_count': len(memories),
130 | 'compression_ratio': compression_ratio,
131 | 'key_concepts': key_concepts,
132 | 'temporal_span': temporal_span,
133 | 'theme_keywords': cluster.theme_keywords,
134 | 'coherence_score': cluster.coherence_score,
135 | 'compression_version': '1.0'
136 | },
137 | embedding=compressed_embedding,
138 | created_at=datetime.now().timestamp(),
139 | created_at_iso=datetime.now().isoformat() + 'Z'
140 | )
141 |
142 | return CompressionResult(
143 | cluster_id=cluster.cluster_id,
144 | compressed_memory=compressed_memory,
145 | compression_ratio=compression_ratio,
146 | key_concepts=key_concepts,
147 | temporal_span=temporal_span,
148 | source_memory_count=len(memories),
149 | compression_metadata={
150 | 'algorithm': 'semantic_compression_v1',
151 | 'original_total_length': original_size,
152 | 'compressed_length': compressed_size,
153 | 'concept_count': len(key_concepts),
154 | 'theme_keywords': cluster.theme_keywords
155 | }
156 | )
157 |
158 | async def _extract_key_concepts(self, memories: List[Memory], theme_keywords: List[str]) -> List[str]:
159 | """Extract key concepts from cluster memories."""
160 | all_text = ' '.join([m.content for m in memories])
161 | concepts = set()
162 |
163 | # Add theme keywords as primary concepts
164 | concepts.update(theme_keywords)
165 |
166 | # Extract important patterns
167 | for pattern_name, pattern in self._important_patterns.items():
168 | matches = pattern.findall(all_text)
169 | if pattern_name == 'quoted_text':
170 | # For quoted text, add the content inside quotes
171 | concepts.update(matches)
172 | else:
173 | concepts.update(matches)
174 |
175 | # Extract capitalized terms (potential proper nouns)
176 | capitalized = re.findall(r'\b[A-Z][a-z]{2,}\b', all_text)
177 | concepts.update(capitalized)
178 |
179 | # Extract frequent meaningful words
180 | words = re.findall(r'\b[a-zA-Z]{4,}\b', all_text.lower())
181 | word_counts = Counter(words)
182 |
183 | # Filter out common words
184 | stop_words = {
185 | 'this', 'that', 'with', 'have', 'will', 'from', 'they', 'know',
186 | 'want', 'been', 'good', 'much', 'some', 'time', 'very', 'when',
187 | 'come', 'here', 'just', 'like', 'long', 'make', 'many', 'over',
188 | 'such', 'take', 'than', 'them', 'well', 'were', 'what', 'work',
189 | 'your', 'could', 'should', 'would', 'there', 'their', 'these',
190 | 'about', 'after', 'again', 'before', 'being', 'between', 'during',
191 | 'under', 'where', 'while', 'other', 'through', 'against', 'without'
192 | }
193 |
194 | # Add frequent non-stop words
195 | for word, count in word_counts.most_common(20):
196 | if word not in stop_words and count >= 2: # Must appear at least twice
197 | concepts.add(word)
198 |
199 | # Convert to list and limit
200 | concept_list = list(concepts)
201 | concept_list.sort(key=lambda x: word_counts.get(x.lower(), 0), reverse=True)
202 |
203 | return concept_list[:15] # Limit to top 15 concepts
204 |
205 | async def _generate_thematic_summary(self, memories: List[Memory], key_concepts: List[str]) -> str:
206 | """Generate a thematic summary of the memory cluster."""
207 | # Analyze the memories to identify common themes and patterns
208 | all_content = [m.content for m in memories]
209 |
210 | # Extract representative sentences that contain key concepts
211 | representative_sentences = []
212 | concept_coverage = set()
213 |
214 | for memory in memories:
215 | sentences = self._split_into_sentences(memory.content)
216 | for sentence in sentences:
217 | sentence_concepts = set()
218 | sentence_lower = sentence.lower()
219 |
220 | # Check which concepts this sentence covers
221 | for concept in key_concepts:
222 | if concept.lower() in sentence_lower:
223 | sentence_concepts.add(concept)
224 |
225 | # If this sentence covers new concepts, include it
226 | new_concepts = sentence_concepts - concept_coverage
227 | if new_concepts and len(sentence) > 20: # Minimum sentence length
228 | representative_sentences.append({
229 | 'sentence': sentence.strip(),
230 | 'concepts': sentence_concepts,
231 | 'new_concepts': new_concepts,
232 | 'score': len(new_concepts) + len(sentence_concepts) * 0.1
233 | })
234 | concept_coverage.update(new_concepts)
235 |
236 | # Sort by score and select best sentences
237 | representative_sentences.sort(key=lambda x: x['score'], reverse=True)
238 |
239 | # Build summary
240 | summary_parts = []
241 |
242 | # Add cluster overview
243 | memory_count = len(memories)
244 | time_span = self._calculate_temporal_span(memories)
245 | concept_str = ', '.join(key_concepts[:5])
246 |
247 | overview = f"Cluster of {memory_count} related memories about {concept_str}"
248 | if time_span['span_days'] > 0:
249 | overview += f" spanning {time_span['span_days']} days"
250 | overview += "."
251 |
252 | summary_parts.append(overview)
253 |
254 | # Add key insights from representative sentences
255 | used_length = len(overview)
256 | remaining_length = self.max_summary_length - used_length - 100 # Reserve space for conclusion
257 |
258 | for sent_info in representative_sentences:
259 | sentence = sent_info['sentence']
260 | if used_length + len(sentence) < remaining_length:
261 | summary_parts.append(sentence)
262 | used_length += len(sentence)
263 | else:
264 | break
265 |
266 | # Add concept summary if space allows
267 | if used_length < self.max_summary_length - 50:
268 | concept_summary = f"Key concepts: {', '.join(key_concepts[:8])}."
269 | if used_length + len(concept_summary) < self.max_summary_length:
270 | summary_parts.append(concept_summary)
271 |
272 | summary = ' '.join(summary_parts)
273 |
274 | # Truncate if still too long
275 | if len(summary) > self.max_summary_length:
276 | summary = summary[:self.max_summary_length - 3] + '...'
277 |
278 | return summary
279 |
280 | def _split_into_sentences(self, text: str) -> List[str]:
281 | """Split text into sentences using simple heuristics."""
282 | # Simple sentence splitting (could be improved with NLTK)
283 | sentences = re.split(r'[.!?]+\s+', text)
284 |
285 | # Filter out very short sentences and clean up
286 | clean_sentences = []
287 | for sentence in sentences:
288 | sentence = sentence.strip()
289 | if len(sentence) > 10: # Minimum sentence length
290 | clean_sentences.append(sentence)
291 |
292 | return clean_sentences
293 |
294 | def _calculate_temporal_span(self, memories: List[Memory]) -> Dict[str, Any]:
295 | """Calculate temporal information for the memory cluster."""
296 | timestamps = []
297 |
298 | for memory in memories:
299 | if memory.created_at:
300 | timestamps.append(memory.created_at)
301 | elif memory.timestamp:
302 | timestamps.append(memory.timestamp.timestamp())
303 |
304 | if not timestamps:
305 | return {
306 | 'start_time': None,
307 | 'end_time': None,
308 | 'span_days': 0,
309 | 'span_description': 'unknown'
310 | }
311 |
312 | start_time = min(timestamps)
313 | end_time = max(timestamps)
314 | span_seconds = end_time - start_time
315 | span_days = int(span_seconds / (24 * 3600))
316 |
317 | # Create human-readable span description
318 | if span_days == 0:
319 | span_description = 'same day'
320 | elif span_days < 7:
321 | span_description = f'{span_days} days'
322 | elif span_days < 30:
323 | weeks = span_days // 7
324 | span_description = f'{weeks} week{"s" if weeks > 1 else ""}'
325 | elif span_days < 365:
326 | months = span_days // 30
327 | span_description = f'{months} month{"s" if months > 1 else ""}'
328 | else:
329 | years = span_days // 365
330 | span_description = f'{years} year{"s" if years > 1 else ""}'
331 |
332 | return {
333 | 'start_time': start_time,
334 | 'end_time': end_time,
335 | 'span_days': span_days,
336 | 'span_description': span_description,
337 | 'start_iso': datetime.utcfromtimestamp(start_time).isoformat() + 'Z',
338 | 'end_iso': datetime.utcfromtimestamp(end_time).isoformat() + 'Z'
339 | }
340 |
341 | def _aggregate_tags(self, memories: List[Memory]) -> List[str]:
342 | """Aggregate tags from cluster memories."""
343 | all_tags = []
344 | for memory in memories:
345 | all_tags.extend(memory.tags)
346 |
347 | # Count tag frequency
348 | tag_counts = Counter(all_tags)
349 |
350 | # Include tags that appear in multiple memories or are important
351 | aggregated_tags = ['cluster', 'compressed'] # Always include these
352 |
353 | for tag, count in tag_counts.most_common():
354 | if count > 1 or tag in {'important', 'critical', 'reference', 'project'}:
355 | if tag not in aggregated_tags:
356 | aggregated_tags.append(tag)
357 |
358 | return aggregated_tags[:10] # Limit to 10 tags
359 |
360 | def _aggregate_metadata(self, memories: List[Memory]) -> Dict[str, Any]:
361 | """Aggregate metadata from cluster memories."""
362 | aggregated = {
363 | 'source_memory_hashes': [m.content_hash for m in memories]
364 | }
365 |
366 | # Collect unique metadata keys and their values
367 | all_metadata = {}
368 | for memory in memories:
369 | for key, value in memory.metadata.items():
370 | if key not in all_metadata:
371 | all_metadata[key] = []
372 | all_metadata[key].append(value)
373 |
374 | # Aggregate metadata intelligently
375 | for key, values in all_metadata.items():
376 | unique_values = list(set(str(v) for v in values))
377 |
378 | if len(unique_values) == 1:
379 | # All memories have the same value
380 | aggregated[f'common_{key}'] = unique_values[0]
381 | elif len(unique_values) <= 5:
382 | # Small number of unique values, list them
383 | aggregated[f'varied_{key}'] = unique_values
384 | else:
385 | # Many unique values, just note the variety
386 | aggregated[f'{key}_variety_count'] = len(unique_values)
387 |
388 | return aggregated
389 |
390 | async def estimate_compression_benefit(
391 | self,
392 | clusters: List[MemoryCluster],
393 | memories: List[Memory]
394 | ) -> Dict[str, Any]:
395 | """Estimate the benefit of compressing given clusters."""
396 | memory_lookup = {m.content_hash: m for m in memories}
397 |
398 | total_original_size = 0
399 | total_compressed_size = 0
400 | compressible_clusters = 0
401 |
402 | for cluster in clusters:
403 | cluster_memories = [memory_lookup[h] for h in cluster.memory_hashes if h in memory_lookup]
404 |
405 | if len(cluster_memories) < 2:
406 | continue
407 |
408 | compressible_clusters += 1
409 | original_size = sum(len(m.content) for m in cluster_memories)
410 |
411 | # Estimate compressed size (rough approximation)
412 | estimated_compressed_size = min(self.max_summary_length, original_size // 3)
413 |
414 | total_original_size += original_size
415 | total_compressed_size += estimated_compressed_size
416 |
417 | overall_ratio = total_compressed_size / total_original_size if total_original_size > 0 else 1.0
418 | savings = total_original_size - total_compressed_size
419 |
420 | return {
421 | 'compressible_clusters': compressible_clusters,
422 | 'total_original_size': total_original_size,
423 | 'estimated_compressed_size': total_compressed_size,
424 | 'compression_ratio': overall_ratio,
425 | 'estimated_savings_bytes': savings,
426 | 'estimated_savings_percent': (1 - overall_ratio) * 100
427 | }
```
--------------------------------------------------------------------------------
/claude-hooks/utilities/session-tracker.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Session Tracker Utility
3 | * Provides cross-session intelligence and conversation continuity
4 | * Phase 2: Intelligent Context Updates
5 | */
6 |
7 | const fs = require('fs').promises;
8 | const path = require('path');
9 | const crypto = require('crypto');
10 |
11 | /**
12 | * Session tracking data structure
13 | */
14 | class SessionTracker {
15 | constructor(options = {}) {
16 | this.options = {
17 | maxSessionHistory: 50, // Maximum sessions to track
18 | maxConversationDepth: 10, // Maximum conversation thread depth
19 | sessionExpiryDays: 30, // Days after which sessions are considered expired
20 | trackingDataPath: options.trackingDataPath || path.join(__dirname, '../session-tracking.json'),
21 | ...options
22 | };
23 |
24 | this.sessions = new Map();
25 | this.conversationThreads = new Map();
26 | this.projectSessions = new Map();
27 | this.loaded = false;
28 | }
29 |
30 | /**
31 | * Initialize session tracking system
32 | */
33 | async initialize() {
34 | console.log('[Session Tracker] Initializing session tracking system...');
35 |
36 | try {
37 | await this.loadTrackingData();
38 | this.cleanupExpiredSessions();
39 | this.loaded = true;
40 |
41 | console.log(`[Session Tracker] Loaded ${this.sessions.size} sessions, ${this.conversationThreads.size} threads`);
42 | } catch (error) {
43 | console.error('[Session Tracker] Failed to initialize:', error.message);
44 | this.loaded = false;
45 | }
46 | }
47 |
48 | /**
49 | * Start tracking a new session
50 | */
51 | async startSession(sessionId, context = {}) {
52 | if (!this.loaded) {
53 | await this.initialize();
54 | }
55 |
56 | const session = {
57 | id: sessionId,
58 | startTime: new Date().toISOString(),
59 | endTime: null,
60 | projectContext: context.projectContext || {},
61 | workingDirectory: context.workingDirectory,
62 | initialTopics: [],
63 | finalTopics: [],
64 | memoriesLoaded: [],
65 | memoriesCreated: [],
66 | conversationSummary: null,
67 | outcome: null,
68 | threadId: null,
69 | parentSessionId: null,
70 | childSessionIds: [],
71 | status: 'active'
72 | };
73 |
74 | // Try to link to existing conversation thread
75 | await this.linkToConversationThread(session, context);
76 |
77 | this.sessions.set(sessionId, session);
78 |
79 | // Track by project
80 | const projectName = context.projectContext?.name;
81 | if (projectName) {
82 | if (!this.projectSessions.has(projectName)) {
83 | this.projectSessions.set(projectName, []);
84 | }
85 | this.projectSessions.get(projectName).push(sessionId);
86 | }
87 |
88 | console.log(`[Session Tracker] Started session ${sessionId} for project: ${projectName || 'unknown'}`);
89 |
90 | await this.saveTrackingData();
91 | return session;
92 | }
93 |
94 | /**
95 | * End session tracking and record outcomes
96 | */
97 | async endSession(sessionId, outcome = {}) {
98 | const session = this.sessions.get(sessionId);
99 | if (!session) {
100 | console.warn(`[Session Tracker] Session ${sessionId} not found`);
101 | return null;
102 | }
103 |
104 | session.endTime = new Date().toISOString();
105 | session.status = 'completed';
106 | session.outcome = outcome;
107 | session.conversationSummary = outcome.summary;
108 | session.finalTopics = outcome.topics || [];
109 |
110 | // Update conversation thread with session outcome
111 | if (session.threadId) {
112 | await this.updateConversationThread(session.threadId, session);
113 | }
114 |
115 | console.log(`[Session Tracker] Ended session ${sessionId} with outcome: ${outcome.type || 'unknown'}`);
116 |
117 | await this.saveTrackingData();
118 | return session;
119 | }
120 |
121 | /**
122 | * Link session to existing conversation thread or create new one
123 | */
124 | async linkToConversationThread(session, context) {
125 | // Try to find related sessions based on project and recent activity
126 | const relatedSessions = await this.findRelatedSessions(session, context);
127 |
128 | if (relatedSessions.length > 0) {
129 | // Link to existing thread
130 | const parentSession = relatedSessions[0];
131 | session.threadId = parentSession.threadId;
132 | session.parentSessionId = parentSession.id;
133 |
134 | // Update parent session
135 | if (this.sessions.has(parentSession.id)) {
136 | this.sessions.get(parentSession.id).childSessionIds.push(session.id);
137 | }
138 |
139 | console.log(`[Session Tracker] Linked session ${session.id} to thread ${session.threadId}`);
140 | } else {
141 | // Create new conversation thread
142 | const threadId = this.generateThreadId();
143 | session.threadId = threadId;
144 |
145 | const thread = {
146 | id: threadId,
147 | createdAt: new Date().toISOString(),
148 | projectContext: session.projectContext,
149 | sessionIds: [session.id],
150 | topics: new Set(),
151 | outcomes: [],
152 | status: 'active'
153 | };
154 |
155 | this.conversationThreads.set(threadId, thread);
156 | console.log(`[Session Tracker] Created new conversation thread ${threadId}`);
157 | }
158 | }
159 |
160 | /**
161 | * Find related sessions for conversation threading
162 | */
163 | async findRelatedSessions(session, context) {
164 | const projectName = context.projectContext?.name;
165 | if (!projectName) {
166 | return [];
167 | }
168 |
169 | const projectSessionIds = this.projectSessions.get(projectName) || [];
170 | const relatedSessions = [];
171 |
172 | // Look for recent sessions in same project
173 | const cutoffTime = new Date();
174 | cutoffTime.setHours(cutoffTime.getHours() - 24); // 24 hour window
175 |
176 | for (const sessionId of projectSessionIds.slice(-10)) { // Check last 10 sessions
177 | const session = this.sessions.get(sessionId);
178 | if (!session || session.status === 'active') continue;
179 |
180 | const sessionTime = new Date(session.endTime || session.startTime);
181 | if (sessionTime > cutoffTime) {
182 | // Calculate relatedness score
183 | const relatednessScore = this.calculateSessionRelatedness(session, context);
184 | if (relatednessScore > 0.3) {
185 | relatedSessions.push({
186 | ...session,
187 | relatednessScore
188 | });
189 | }
190 | }
191 | }
192 |
193 | // Sort by relatedness score
194 | return relatedSessions.sort((a, b) => b.relatednessScore - a.relatednessScore);
195 | }
196 |
197 | /**
198 | * Calculate how related two sessions are
199 | */
200 | calculateSessionRelatedness(existingSession, newContext) {
201 | let score = 0;
202 |
203 | // Same project bonus
204 | if (existingSession.projectContext?.name === newContext.projectContext?.name) {
205 | score += 0.4;
206 | }
207 |
208 | // Same working directory bonus
209 | if (existingSession.workingDirectory === newContext.workingDirectory) {
210 | score += 0.3;
211 | }
212 |
213 | // Technology stack similarity
214 | const existingTech = [
215 | ...(existingSession.projectContext?.languages || []),
216 | ...(existingSession.projectContext?.frameworks || [])
217 | ];
218 | const newTech = [
219 | ...(newContext.projectContext?.languages || []),
220 | ...(newContext.projectContext?.frameworks || [])
221 | ];
222 |
223 | const techOverlap = existingTech.filter(tech => newTech.includes(tech)).length;
224 | if (existingTech.length > 0) {
225 | score += (techOverlap / existingTech.length) * 0.3;
226 | }
227 |
228 | return Math.min(score, 1.0);
229 | }
230 |
231 | /**
232 | * Update conversation thread with session information
233 | */
234 | async updateConversationThread(threadId, session) {
235 | const thread = this.conversationThreads.get(threadId);
236 | if (!thread) {
237 | console.warn(`[Session Tracker] Thread ${threadId} not found`);
238 | return;
239 | }
240 |
241 | // Add session to thread if not already present
242 | if (!thread.sessionIds.includes(session.id)) {
243 | thread.sessionIds.push(session.id);
244 | }
245 |
246 | // Update thread topics
247 | if (session.finalTopics && session.finalTopics.length > 0) {
248 | session.finalTopics.forEach(topic => thread.topics.add(topic));
249 | }
250 |
251 | // Add outcome to thread history
252 | if (session.outcome) {
253 | thread.outcomes.push({
254 | sessionId: session.id,
255 | outcome: session.outcome,
256 | timestamp: session.endTime
257 | });
258 | }
259 |
260 | thread.lastUpdated = new Date().toISOString();
261 | }
262 |
263 | /**
264 | * Get conversation context for a new session
265 | */
266 | async getConversationContext(projectContext, options = {}) {
267 | const {
268 | maxPreviousSessions = 3,
269 | maxDaysBack = 7
270 | } = options;
271 |
272 | const projectName = projectContext?.name;
273 | if (!projectName) {
274 | return null;
275 | }
276 |
277 | const projectSessionIds = this.projectSessions.get(projectName) || [];
278 | if (projectSessionIds.length === 0) {
279 | return null;
280 | }
281 |
282 | // Get recent sessions
283 | const cutoffTime = new Date();
284 | cutoffTime.setDate(cutoffTime.getDate() - maxDaysBack);
285 |
286 | const recentSessions = [];
287 | for (const sessionId of projectSessionIds.slice(-10)) {
288 | const session = this.sessions.get(sessionId);
289 | if (!session || session.status === 'active') continue;
290 |
291 | const sessionTime = new Date(session.endTime || session.startTime);
292 | if (sessionTime > cutoffTime) {
293 | recentSessions.push(session);
294 | }
295 | }
296 |
297 | // Sort by end time and take most recent
298 | const sortedSessions = recentSessions
299 | .sort((a, b) => new Date(b.endTime || b.startTime) - new Date(a.endTime || a.startTime))
300 | .slice(0, maxPreviousSessions);
301 |
302 | if (sortedSessions.length === 0) {
303 | return null;
304 | }
305 |
306 | // Build conversation context
307 | const context = {
308 | projectName: projectName,
309 | recentSessions: sortedSessions.map(session => ({
310 | id: session.id,
311 | endTime: session.endTime,
312 | outcome: session.outcome,
313 | topics: session.finalTopics,
314 | memoriesCreated: session.memoriesCreated?.length || 0
315 | })),
316 | continuityInsights: this.extractContinuityInsights(sortedSessions),
317 | activeThreads: this.getActiveThreadsForProject(projectName)
318 | };
319 |
320 | return context;
321 | }
322 |
323 | /**
324 | * Extract insights about conversation continuity
325 | */
326 | extractContinuityInsights(sessions) {
327 | const insights = {
328 | recurringTopics: this.findRecurringTopics(sessions),
329 | progressionPatterns: this.analyzeProgressionPatterns(sessions),
330 | uncompletedTasks: this.findUncompletedTasks(sessions)
331 | };
332 |
333 | return insights;
334 | }
335 |
336 | /**
337 | * Find topics that appear across multiple sessions
338 | */
339 | findRecurringTopics(sessions) {
340 | const topicCounts = new Map();
341 |
342 | sessions.forEach(session => {
343 | (session.finalTopics || []).forEach(topic => {
344 | topicCounts.set(topic, (topicCounts.get(topic) || 0) + 1);
345 | });
346 | });
347 |
348 | return Array.from(topicCounts.entries())
349 | .filter(([topic, count]) => count > 1)
350 | .sort((a, b) => b[1] - a[1])
351 | .slice(0, 5)
352 | .map(([topic, count]) => ({ topic, frequency: count }));
353 | }
354 |
355 | /**
356 | * Analyze how work progresses across sessions
357 | */
358 | analyzeProgressionPatterns(sessions) {
359 | const patterns = [];
360 |
361 | // Look for planning -> implementation -> testing patterns
362 | const outcomePairs = [];
363 | for (let i = 0; i < sessions.length - 1; i++) {
364 | outcomePairs.push([
365 | sessions[i].outcome?.type,
366 | sessions[i + 1].outcome?.type
367 | ]);
368 | }
369 |
370 | return patterns;
371 | }
372 |
373 | /**
374 | * Find tasks or decisions that weren't completed
375 | */
376 | findUncompletedTasks(sessions) {
377 | const tasks = [];
378 |
379 | sessions.forEach(session => {
380 | if (session.outcome?.type === 'planning' || session.outcome?.type === 'partial') {
381 | tasks.push({
382 | sessionId: session.id,
383 | description: session.outcome?.summary,
384 | timestamp: session.endTime
385 | });
386 | }
387 | });
388 |
389 | return tasks;
390 | }
391 |
392 | /**
393 | * Get active conversation threads for a project
394 | */
395 | getActiveThreadsForProject(projectName) {
396 | const threads = [];
397 |
398 | this.conversationThreads.forEach((thread, threadId) => {
399 | if (thread.projectContext?.name === projectName && thread.status === 'active') {
400 | threads.push({
401 | id: threadId,
402 | sessionCount: thread.sessionIds.length,
403 | topics: Array.from(thread.topics),
404 | lastUpdated: thread.lastUpdated
405 | });
406 | }
407 | });
408 |
409 | return threads;
410 | }
411 |
412 | /**
413 | * Cleanup expired sessions and threads
414 | */
415 | cleanupExpiredSessions() {
416 | const cutoffTime = new Date();
417 | cutoffTime.setDate(cutoffTime.getDate() - this.options.sessionExpiryDays);
418 |
419 | let cleanedCount = 0;
420 |
421 | // Cleanup sessions
422 | for (const [sessionId, session] of this.sessions.entries()) {
423 | const sessionTime = new Date(session.endTime || session.startTime);
424 | if (sessionTime < cutoffTime) {
425 | this.sessions.delete(sessionId);
426 | cleanedCount++;
427 | }
428 | }
429 |
430 | // Cleanup project session references
431 | this.projectSessions.forEach((sessionIds, projectName) => {
432 | const validSessions = sessionIds.filter(id => this.sessions.has(id));
433 | if (validSessions.length !== sessionIds.length) {
434 | this.projectSessions.set(projectName, validSessions);
435 | }
436 | });
437 |
438 | if (cleanedCount > 0) {
439 | console.log(`[Session Tracker] Cleaned up ${cleanedCount} expired sessions`);
440 | }
441 | }
442 |
443 | /**
444 | * Generate unique thread ID
445 | */
446 | generateThreadId() {
447 | return 'thread-' + crypto.randomBytes(8).toString('hex');
448 | }
449 |
450 | /**
451 | * Load tracking data from disk
452 | */
453 | async loadTrackingData() {
454 | try {
455 | const data = await fs.readFile(this.options.trackingDataPath, 'utf8');
456 | const parsed = JSON.parse(data);
457 |
458 | // Restore sessions
459 | if (parsed.sessions) {
460 | parsed.sessions.forEach(session => {
461 | this.sessions.set(session.id, session);
462 | });
463 | }
464 |
465 | // Restore conversation threads (convert topics Set back from array)
466 | if (parsed.conversationThreads) {
467 | parsed.conversationThreads.forEach(thread => {
468 | thread.topics = new Set(thread.topics || []);
469 | this.conversationThreads.set(thread.id, thread);
470 | });
471 | }
472 |
473 | // Restore project sessions
474 | if (parsed.projectSessions) {
475 | Object.entries(parsed.projectSessions).forEach(([project, sessionIds]) => {
476 | this.projectSessions.set(project, sessionIds);
477 | });
478 | }
479 |
480 | } catch (error) {
481 | if (error.code !== 'ENOENT') {
482 | console.warn('[Session Tracker] Failed to load tracking data:', error.message);
483 | }
484 | // Initialize empty structures if file doesn't exist
485 | }
486 | }
487 |
488 | /**
489 | * Save tracking data to disk
490 | */
491 | async saveTrackingData() {
492 | try {
493 | const data = {
494 | sessions: Array.from(this.sessions.values()),
495 | conversationThreads: Array.from(this.conversationThreads.values()).map(thread => ({
496 | ...thread,
497 | topics: Array.from(thread.topics) // Convert Set to Array for JSON
498 | })),
499 | projectSessions: Object.fromEntries(this.projectSessions.entries()),
500 | lastSaved: new Date().toISOString()
501 | };
502 |
503 | await fs.writeFile(this.options.trackingDataPath, JSON.stringify(data, null, 2));
504 | } catch (error) {
505 | console.error('[Session Tracker] Failed to save tracking data:', error.message);
506 | }
507 | }
508 |
509 | /**
510 | * Get statistics about session tracking
511 | */
512 | getStats() {
513 | return {
514 | totalSessions: this.sessions.size,
515 | activeSessions: Array.from(this.sessions.values()).filter(s => s.status === 'active').length,
516 | totalThreads: this.conversationThreads.size,
517 | trackedProjects: this.projectSessions.size,
518 | loaded: this.loaded
519 | };
520 | }
521 | }
522 |
523 | // Create global session tracker instance
524 | let globalSessionTracker = null;
525 |
526 | /**
527 | * Get or create global session tracker instance
528 | */
529 | function getSessionTracker(options = {}) {
530 | if (!globalSessionTracker) {
531 | globalSessionTracker = new SessionTracker(options);
532 | }
533 | return globalSessionTracker;
534 | }
535 |
536 | module.exports = {
537 | SessionTracker,
538 | getSessionTracker
539 | };
```
--------------------------------------------------------------------------------
/docs/testing/regression-tests.md:
--------------------------------------------------------------------------------
```markdown
1 | # Regression Tests
2 |
3 | This document provides structured test scenarios for validating critical functionality and preventing regressions. Each test includes setup instructions, expected results, evidence collection, and pass/fail criteria.
4 |
5 | ## Purpose
6 |
7 | Regression tests ensure that:
8 | - Critical bugs don't reappear after being fixed
9 | - Performance optimizations don't degrade over time
10 | - Platform-specific issues are caught before release
11 | - Integration points (MCP, HTTP API, storage backends) work correctly
12 |
13 | ## Test Categories
14 |
15 | 1. [Database Locking & Concurrency](#database-locking--concurrency)
16 | 2. [Storage Backend Integrity](#storage-backend-integrity)
17 | 3. [Dashboard Performance](#dashboard-performance)
18 | 4. [Tag Filtering Correctness](#tag-filtering-correctness)
19 | 5. [MCP Protocol Compliance](#mcp-protocol-compliance)
20 |
21 | ---
22 |
23 | ## Database Locking & Concurrency
24 |
25 | ### Test 1: Concurrent MCP Server Startup
26 |
27 | **Context:** v8.9.0+ fixed "database is locked" errors by setting SQLite pragmas before connection
28 |
29 | **Setup:**
30 | 1. Close all Claude Desktop instances
31 | 2. Ensure SQLite database exists at `~/Library/Application Support/mcp-memory/sqlite_vec.db` (macOS)
32 | 3. Verify `.env` contains `MCP_MEMORY_SQLITE_PRAGMAS=busy_timeout=15000,journal_mode=WAL`
33 |
34 | **Execution:**
35 | 1. Open 3 Claude Desktop instances simultaneously (within 5 seconds)
36 | 2. In each instance, trigger memory service initialization:
37 | ```
38 | /mcp
39 | # Wait for MCP servers to connect
40 | # Try storing a memory in each instance
41 | ```
42 | 3. Monitor logs in `~/Library/Logs/Claude/mcp-server-memory.log`
43 |
44 | **Expected Results:**
45 | - ✅ All 3 instances connect successfully
46 | - ✅ Zero "database is locked" errors in logs
47 | - ✅ All instances show healthy status via `/api/health`
48 | - ✅ Memory operations work in all instances
49 |
50 | **Evidence Collection:**
51 | ```bash
52 | # Check for lock errors
53 | grep -i "database is locked" ~/Library/Logs/Claude/mcp-server-memory.log
54 |
55 | # Verify pragma settings
56 | sqlite3 ~/Library/Application\ Support/mcp-memory/sqlite_vec.db "PRAGMA busy_timeout;"
57 | # Expected output: 15000
58 |
59 | # Check journal mode
60 | sqlite3 ~/Library/Application\ Support/mcp-memory/sqlite_vec.db "PRAGMA journal_mode;"
61 | # Expected output: wal
62 | ```
63 |
64 | **Pass Criteria:**
65 | - ✅ Zero lock errors
66 | - ✅ All servers initialize within 10 seconds
67 | - ✅ Concurrent memory operations succeed
68 | - ❌ FAIL if any server shows "database is locked"
69 |
70 | ---
71 |
72 | ### Test 2: Concurrent Memory Operations
73 |
74 | **Context:** Test simultaneous read/write operations from multiple clients
75 |
76 | **Setup:**
77 | 1. Start HTTP server: `uv run memory server --http`
78 | 2. Verify server is healthy: `curl http://127.0.0.1:8000/api/health`
79 |
80 | **Execution:**
81 | 1. Run concurrent memory stores from multiple terminals:
82 | ```bash
83 | # Terminal 1
84 | for i in {1..50}; do
85 | curl -X POST http://127.0.0.1:8000/api/memories \
86 | -H "Content-Type: application/json" \
87 | -d "{\"content\":\"Test memory $i from terminal 1\",\"tags\":[\"test\",\"concurrent\"]}"
88 | done
89 |
90 | # Terminal 2 (run simultaneously)
91 | for i in {1..50}; do
92 | curl -X POST http://127.0.0.1:8000/api/memories \
93 | -H "Content-Type: application/json" \
94 | -d "{\"content\":\"Test memory $i from terminal 2\",\"tags\":[\"test\",\"concurrent\"]}"
95 | done
96 | ```
97 |
98 | 2. While stores are running, perform searches:
99 | ```bash
100 | # Terminal 3
101 | for i in {1..20}; do
102 | curl -s "http://127.0.0.1:8000/api/search" \
103 | -H "Content-Type: application/json" \
104 | -d '{"query":"test memory","limit":10}'
105 | done
106 | ```
107 |
108 | **Expected Results:**
109 | - ✅ All 100 memory stores complete successfully
110 | - ✅ Zero HTTP 500 errors
111 | - ✅ Search operations return results during writes
112 | - ✅ No database lock errors in server logs
113 |
114 | **Evidence Collection:**
115 | ```bash
116 | # Count successful stores
117 | curl -s "http://127.0.0.1:8000/api/search/by-tag" \
118 | -H "Content-Type: application/json" \
119 | -d '{"tags":["concurrent"],"limit":1000}' | jq '.memories | length'
120 | # Expected: 100
121 |
122 | # Check server logs for errors
123 | tail -100 ~/Library/Logs/Claude/mcp-server-memory.log | grep -i error
124 | ```
125 |
126 | **Pass Criteria:**
127 | - ✅ 100 memories stored successfully
128 | - ✅ Zero database lock errors
129 | - ✅ Zero HTTP 500 responses
130 | - ❌ FAIL if any operation times out or errors
131 |
132 | ---
133 |
134 | ## Storage Backend Integrity
135 |
136 | ### Test 3: Hybrid Backend Synchronization
137 |
138 | **Context:** Verify hybrid backend syncs SQLite → Cloudflare without data loss
139 |
140 | **Setup:**
141 | 1. Configure hybrid backend in `.env`:
142 | ```bash
143 | MCP_MEMORY_STORAGE_BACKEND=hybrid
144 | MCP_HYBRID_SYNC_INTERVAL=10 # Frequent sync for testing
145 | CLOUDFLARE_API_TOKEN=your-token
146 | CLOUDFLARE_ACCOUNT_ID=your-account
147 | CLOUDFLARE_D1_DATABASE_ID=your-db-id
148 | CLOUDFLARE_VECTORIZE_INDEX=mcp-memory-index
149 | ```
150 | 2. Clear Cloudflare backend: `python scripts/database/clear_cloudflare.py --confirm`
151 | 3. Start server: `uv run memory server --http`
152 |
153 | **Execution:**
154 | 1. Store 10 test memories via API:
155 | ```bash
156 | for i in {1..10}; do
157 | curl -X POST http://127.0.0.1:8000/api/memories \
158 | -H "Content-Type: application/json" \
159 | -d "{\"content\":\"Hybrid test memory $i\",\"tags\":[\"hybrid-test\"]}"
160 | done
161 | ```
162 |
163 | 2. Wait 30 seconds (3x sync interval) for background sync
164 |
165 | 3. Query Cloudflare backend directly:
166 | ```bash
167 | python scripts/sync/check_cloudflare_sync.py --tag hybrid-test
168 | ```
169 |
170 | **Expected Results:**
171 | - ✅ All 10 memories present in SQLite (immediate)
172 | - ✅ All 10 memories synced to Cloudflare (within 30s)
173 | - ✅ Content hashes match between backends
174 | - ✅ No sync errors in server logs
175 |
176 | **Evidence Collection:**
177 | ```bash
178 | # Check SQLite count
179 | curl -s "http://127.0.0.1:8000/api/search/by-tag" \
180 | -H "Content-Type: application/json" \
181 | -d '{"tags":["hybrid-test"]}' | jq '.memories | length'
182 |
183 | # Check Cloudflare count
184 | python scripts/sync/check_cloudflare_sync.py --tag hybrid-test --count
185 |
186 | # Compare content hashes
187 | python scripts/sync/check_cloudflare_sync.py --tag hybrid-test --verify-hashes
188 | ```
189 |
190 | **Pass Criteria:**
191 | - ✅ SQLite count == Cloudflare count
192 | - ✅ All content hashes match
193 | - ✅ Sync completes within 30 seconds
194 | - ❌ FAIL if any memory missing or hash mismatch
195 |
196 | ---
197 |
198 | ### Test 4: Storage Backend Switching
199 |
200 | **Context:** Verify switching backends doesn't corrupt existing data
201 |
202 | **Setup:**
203 | 1. Start with sqlite-vec backend, store 20 memories
204 | 2. Stop server
205 | 3. Configure hybrid backend, restart server
206 | 4. Verify all memories still accessible
207 |
208 | **Execution:**
209 | 1. **SQLite-vec phase:**
210 | ```bash
211 | export MCP_MEMORY_STORAGE_BACKEND=sqlite_vec
212 | uv run memory server --http &
213 | SERVER_PID=$!
214 |
215 | # Store 20 memories
216 | for i in {1..20}; do
217 | curl -X POST http://127.0.0.1:8000/api/memories \
218 | -H "Content-Type: application/json" \
219 | -d "{\"content\":\"Backend switch test $i\",\"tags\":[\"switch-test\"]}"
220 | done
221 |
222 | kill $SERVER_PID
223 | ```
224 |
225 | 2. **Switch to hybrid:**
226 | ```bash
227 | export MCP_MEMORY_STORAGE_BACKEND=hybrid
228 | export MCP_HYBRID_SYNC_INTERVAL=10
229 | # Set Cloudflare credentials...
230 | uv run memory server --http &
231 | SERVER_PID=$!
232 |
233 | # Wait for startup
234 | sleep 5
235 | ```
236 |
237 | 3. **Verify data integrity:**
238 | ```bash
239 | curl -s "http://127.0.0.1:8000/api/search/by-tag" \
240 | -H "Content-Type: application/json" \
241 | -d '{"tags":["switch-test"]}' | jq '.memories | length'
242 | ```
243 |
244 | **Expected Results:**
245 | - ✅ All 20 memories still accessible after switch
246 | - ✅ Memories begin syncing to Cloudflare
247 | - ✅ No data corruption or loss
248 | - ✅ Health check shows "hybrid" backend
249 |
250 | **Pass Criteria:**
251 | - ✅ 20 memories retrieved successfully
252 | - ✅ Backend reported as "hybrid" in health check
253 | - ✅ No errors during backend initialization
254 | - ❌ FAIL if any memory inaccessible or corrupted
255 |
256 | ---
257 |
258 | ## Dashboard Performance
259 |
260 | ### Test 5: Page Load Performance
261 |
262 | **Context:** Dashboard should load in <2 seconds (v7.2.2 benchmark: 25ms)
263 |
264 | **Setup:**
265 | 1. Database with 1000+ memories
266 | 2. HTTP server running: `uv run memory server --http`
267 | 3. Dashboard at `http://127.0.0.1:8000/`
268 |
269 | **Execution:**
270 | ```bash
271 | # Measure page load time (10 iterations)
272 | for i in {1..10}; do
273 | time curl -s "http://127.0.0.1:8000/" > /dev/null
274 | done
275 | ```
276 |
277 | **Expected Results:**
278 | - ✅ Average load time <500ms
279 | - ✅ All static assets (HTML/CSS/JS) load successfully
280 | - ✅ No JavaScript errors in browser console
281 | - ✅ Dashboard functional on first load
282 |
283 | **Evidence Collection:**
284 | ```bash
285 | # Browser DevTools → Network tab
286 | # - Check "Load" time in waterfall
287 | # - Verify no 404/500 errors
288 | # - Measure DOMContentLoaded and Load events
289 |
290 | # Server-side timing
291 | time curl -s "http://127.0.0.1:8000/" -o /dev/null -w "%{time_total}\n"
292 | ```
293 |
294 | **Pass Criteria:**
295 | - ✅ Page load <2 seconds (target: <500ms)
296 | - ✅ Zero resource loading errors
297 | - ✅ Dashboard interactive immediately
298 | - ❌ FAIL if >2 seconds or JavaScript errors
299 |
300 | ---
301 |
302 | ### Test 6: Memory Operation Performance
303 |
304 | **Context:** CRUD operations should complete in <1 second (v7.2.2 benchmark: 26ms)
305 |
306 | **Setup:**
307 | 1. Clean database: `python scripts/database/reset_database.py --confirm`
308 | 2. HTTP server running
309 |
310 | **Execution:**
311 | 1. **Store operation:**
312 | ```bash
313 | time curl -s -X POST http://127.0.0.1:8000/api/memories \
314 | -H "Content-Type: application/json" \
315 | -d '{"content":"Performance test memory","tags":["perf-test"]}' \
316 | -w "\n%{time_total}\n"
317 | ```
318 |
319 | 2. **Search operation:**
320 | ```bash
321 | time curl -s "http://127.0.0.1:8000/api/search" \
322 | -H "Content-Type: application/json" \
323 | -d '{"query":"performance test","limit":10}' \
324 | -w "\n%{time_total}\n"
325 | ```
326 |
327 | 3. **Tag search operation:**
328 | ```bash
329 | time curl -s "http://127.0.0.1:8000/api/search/by-tag" \
330 | -H "Content-Type: application/json" \
331 | -d '{"tags":["perf-test"]}' \
332 | -w "\n%{time_total}\n"
333 | ```
334 |
335 | 4. **Delete operation:**
336 | ```bash
337 | HASH=$(curl -s "http://127.0.0.1:8000/api/search/by-tag" \
338 | -H "Content-Type: application/json" \
339 | -d '{"tags":["perf-test"]}' | jq -r '.memories[0].hash')
340 |
341 | time curl -s -X DELETE "http://127.0.0.1:8000/api/memories/$HASH" \
342 | -w "\n%{time_total}\n"
343 | ```
344 |
345 | **Expected Results:**
346 | - ✅ Store: <100ms
347 | - ✅ Search: <200ms
348 | - ✅ Tag search: <100ms
349 | - ✅ Delete: <100ms
350 |
351 | **Pass Criteria:**
352 | - ✅ All operations <1 second
353 | - ✅ HTTP 200 responses
354 | - ✅ Correct response format
355 | - ❌ FAIL if any operation >1 second
356 |
357 | ---
358 |
359 | ## Tag Filtering Correctness
360 |
361 | ### Test 7: Exact Tag Matching (No False Positives)
362 |
363 | **Context:** v8.13.0 fixed tag filtering to prevent false positives (e.g., "python" shouldn't match "python3")
364 |
365 | **Setup:**
366 | 1. Clear database
367 | 2. Store memories with similar tags
368 |
369 | **Execution:**
370 | ```bash
371 | # Store test memories
372 | curl -X POST http://127.0.0.1:8000/api/memories \
373 | -H "Content-Type: application/json" \
374 | -d '{"content":"Python programming","tags":["python"]}'
375 |
376 | curl -X POST http://127.0.0.1:8000/api/memories \
377 | -H "Content-Type: application/json" \
378 | -d '{"content":"Python 3 features","tags":["python3"]}'
379 |
380 | curl -X POST http://127.0.0.1:8000/api/memories \
381 | -H "Content-Type: application/json" \
382 | -d '{"content":"CPython internals","tags":["cpython"]}'
383 |
384 | curl -X POST http://127.0.0.1:8000/api/memories \
385 | -H "Content-Type: application/json" \
386 | -d '{"content":"Jython compatibility","tags":["jython"]}'
387 |
388 | # Search for exact tag "python"
389 | curl -s "http://127.0.0.1:8000/api/search/by-tag" \
390 | -H "Content-Type: application/json" \
391 | -d '{"tags":["python"]}' | jq '.memories | length'
392 | ```
393 |
394 | **Expected Results:**
395 | - ✅ Searching "python" returns exactly 1 memory
396 | - ✅ Does NOT return python3, cpython, jython
397 | - ✅ Exact substring boundary matching works
398 |
399 | **Evidence Collection:**
400 | ```bash
401 | # Test each tag variation
402 | for tag in python python3 cpython jython; do
403 | echo "Testing tag: $tag"
404 | curl -s "http://127.0.0.1:8000/api/search/by-tag" \
405 | -H "Content-Type: application/json" \
406 | -d "{\"tags\":[\"$tag\"]}" | jq -r '.memories[].tags[]'
407 | done
408 | ```
409 |
410 | **Pass Criteria:**
411 | - ✅ Each search returns only exact tag matches
412 | - ✅ Zero false positives (substring matches)
413 | - ✅ All 4 memories retrievable individually
414 | - ❌ FAIL if any false positive occurs
415 |
416 | ---
417 |
418 | ### Test 8: Tag Index Usage (Performance)
419 |
420 | **Context:** v8.13.0 added tag normalization with relational tables for O(log n) performance
421 |
422 | **Setup:**
423 | 1. Database with 10,000+ memories
424 | 2. Verify migration completed: `python scripts/database/validate_migration.py`
425 |
426 | **Execution:**
427 | ```bash
428 | # Check query plan uses index
429 | sqlite3 ~/Library/Application\ Support/mcp-memory/sqlite_vec.db <<EOF
430 | EXPLAIN QUERY PLAN
431 | SELECT DISTINCT m.*
432 | FROM memories m
433 | JOIN memory_tags mt ON m.id = mt.memory_id
434 | JOIN tags t ON mt.tag_id = t.id
435 | WHERE t.name = 'test-tag';
436 | EOF
437 | ```
438 |
439 | **Expected Results:**
440 | - ✅ Query plan shows `SEARCH` (using index)
441 | - ✅ Query plan does NOT show `SCAN` (table scan)
442 | - ✅ Tag search completes in <200ms even with 10K memories
443 |
444 | **Evidence Collection:**
445 | ```bash
446 | # Verify index exists
447 | sqlite3 ~/Library/Application\ Support/mcp-memory/sqlite_vec.db \
448 | "SELECT name FROM sqlite_master WHERE type='index' AND name='idx_memory_tags_tag_id';"
449 |
450 | # Benchmark tag search
451 | time curl -s "http://127.0.0.1:8000/api/search/by-tag" \
452 | -H "Content-Type: application/json" \
453 | -d '{"tags":["test-tag"]}' -o /dev/null -w "%{time_total}\n"
454 | ```
455 |
456 | **Pass Criteria:**
457 | - ✅ Index exists and is used (SEARCH in query plan)
458 | - ✅ Tag search <200ms with 10K+ memories
459 | - ✅ Sub-linear scaling (2x data ≠ 2x time)
460 | - ❌ FAIL if SCAN appears or >500ms with 10K memories
461 |
462 | ---
463 |
464 | ## MCP Protocol Compliance
465 |
466 | ### Test 9: MCP Tool Schema Validation
467 |
468 | **Context:** Ensure all MCP tools conform to protocol schema
469 |
470 | **Setup:**
471 | 1. Start MCP server: `uv run memory server`
472 | 2. Use MCP Inspector: `npx @modelcontextprotocol/inspector uv run memory server`
473 |
474 | **Execution:**
475 | 1. Connect with MCP Inspector
476 | 2. List all tools: `tools/list`
477 | 3. Validate each tool schema:
478 | - Required fields present (name, description, inputSchema)
479 | - Input schema is valid JSON Schema
480 | - All parameters documented
481 |
482 | **Expected Results:**
483 | - ✅ All 13 core tools listed
484 | - ✅ Each tool has valid JSON Schema
485 | - ✅ No schema validation errors
486 | - ✅ Tool descriptions are concise (<300 tokens each)
487 |
488 | **Evidence Collection:**
489 | ```bash
490 | # Capture tools/list output
491 | npx @modelcontextprotocol/inspector uv run memory server \
492 | --command "tools/list" > tools_schema.json
493 |
494 | # Validate schema format
495 | cat tools_schema.json | jq '.tools[] | {name, inputSchema}'
496 | ```
497 |
498 | **Pass Criteria:**
499 | - ✅ 13 tools exposed (26 after v8.13.0 consolidation → 13)
500 | - ✅ All schemas valid JSON Schema Draft 07
501 | - ✅ No missing required fields
502 | - ❌ FAIL if any tool lacks proper schema
503 |
504 | ---
505 |
506 | ### Test 10: MCP Tool Execution
507 |
508 | **Context:** Verify all tools execute correctly via MCP protocol
509 |
510 | **Setup:**
511 | 1. MCP server running
512 | 2. MCP Inspector connected
513 |
514 | **Execution:**
515 | 1. **Test store_memory:**
516 | ```json
517 | {
518 | "name": "store_memory",
519 | "arguments": {
520 | "content": "MCP protocol test memory",
521 | "tags": ["mcp-test", "protocol-validation"],
522 | "metadata": {"type": "test"}
523 | }
524 | }
525 | ```
526 |
527 | 2. **Test recall_memory:**
528 | ```json
529 | {
530 | "name": "recall_memory",
531 | "arguments": {
532 | "query": "last week",
533 | "n_results": 5
534 | }
535 | }
536 | ```
537 |
538 | 3. **Test search_by_tag:**
539 | ```json
540 | {
541 | "name": "search_by_tag",
542 | "arguments": {
543 | "tags": ["mcp-test"],
544 | "match_mode": "any"
545 | }
546 | }
547 | ```
548 |
549 | 4. **Test delete_by_tag:**
550 | ```json
551 | {
552 | "name": "delete_by_tag",
553 | "arguments": {
554 | "tags": ["mcp-test"],
555 | "match_mode": "all"
556 | }
557 | }
558 | ```
559 |
560 | **Expected Results:**
561 | - ✅ All tool calls return valid MCP responses
562 | - ✅ No protocol errors or timeouts
563 | - ✅ Response format matches tool schema
564 | - ✅ Operations reflect in database
565 |
566 | **Pass Criteria:**
567 | - ✅ 4/4 tools execute successfully
568 | - ✅ Responses valid JSON
569 | - ✅ Database state matches operations
570 | - ❌ FAIL if any tool returns error or invalid format
571 |
572 | ---
573 |
574 | ## Test Execution Guide
575 |
576 | ### Running All Regression Tests
577 |
578 | ```bash
579 | # 1. Set up test environment
580 | export MCP_MEMORY_STORAGE_BACKEND=sqlite_vec
581 | export MCP_MEMORY_SQLITE_PRAGMAS=busy_timeout=15000,journal_mode=WAL
582 |
583 | # 2. Clear test data
584 | python scripts/database/reset_database.py --confirm
585 |
586 | # 3. Run automated tests
587 | pytest tests/unit/test_exact_tag_matching.py
588 | pytest tests/unit/test_query_plan_validation.py
589 | pytest tests/unit/test_performance_benchmark.py
590 |
591 | # 4. Run manual tests (follow each test's Execution section)
592 | # - Document results in checklist format
593 | # - Capture evidence (logs, screenshots, timing data)
594 | # - Mark pass/fail for each test
595 |
596 | # 5. Generate test report
597 | python scripts/testing/generate_regression_report.py \
598 | --output docs/testing/regression-report-$(date +%Y%m%d).md
599 | ```
600 |
601 | ### Test Frequency
602 |
603 | - **Pre-Release:** All regression tests MUST pass
604 | - **Post-PR Merge:** Run affected test categories
605 | - **Weekly:** Automated subset (performance, tag filtering)
606 | - **Monthly:** Full regression suite
607 |
608 | ### Reporting Issues
609 |
610 | If any test fails:
611 | 1. Create GitHub issue with label `regression`
612 | 2. Include test name, evidence, and reproduction steps
613 | 3. Link to relevant commit/PR that may have caused regression
614 | 4. Add to release blockers if critical functionality affected
615 |
616 | ---
617 |
618 | ## Appendix: Test Data Generation
619 |
620 | ### Create Large Test Dataset
621 |
622 | ```bash
623 | # Generate 10,000 test memories for performance testing
624 | python scripts/testing/generate_test_data.py \
625 | --count 10000 \
626 | --tags-per-memory 3 \
627 | --output test-data-10k.json
628 |
629 | # Import into database
630 | curl -X POST http://127.0.0.1:8000/api/memories/batch \
631 | -H "Content-Type: application/json" \
632 | -d @test-data-10k.json
633 | ```
634 |
635 | ### Cleanup Test Data
636 |
637 | ```bash
638 | # Remove all test data by tag
639 | curl -X POST http://127.0.0.1:8000/api/memories/delete-by-tag \
640 | -H "Content-Type: application/json" \
641 | -d '{"tags": ["test", "perf-test", "mcp-test", "hybrid-test", "switch-test"], "match_mode": "any"}'
642 | ```
643 |
644 | ---
645 |
646 | **Last Updated:** 2025-11-05
647 | **Version:** 1.0
648 | **Related:** [Release Checklist](release-checklist.md), [PR Review Guide](pr-review-guide.md)
649 |
```
--------------------------------------------------------------------------------
/docs/integration/multi-client.md:
--------------------------------------------------------------------------------
```markdown
1 | # Multi-Client Setup Guide
2 |
3 | This comprehensive guide covers setting up MCP Memory Service for multiple clients, enabling shared memory access across different applications and devices.
4 |
5 | ## Overview
6 |
7 | MCP Memory Service supports multi-client access through several deployment patterns:
8 |
9 | 1. **🌟 Integrated Setup** (Easiest - during installation)
10 | 2. **📁 Shared File Access** (Local networks with shared storage)
11 | 3. **🌐 Centralized HTTP/SSE Server** (Distributed teams and cloud deployment)
12 |
13 | ## 🌟 Integrated Setup (Recommended)
14 |
15 | ### During Installation
16 |
17 | The easiest way to configure multi-client access is during the initial installation:
18 |
19 | ```bash
20 | # Run the installer - you'll be prompted for multi-client setup
21 | python install.py
22 |
23 | # When prompted, choose 'y':
24 | # 🌐 Multi-Client Access Available!
25 | # Would you like to configure multi-client access? (y/N): y
26 | ```
27 |
28 | **Benefits of integrated setup:**
29 | - ✅ Automatic detection of Claude Desktop, VS Code, Continue, Cursor, and other MCP clients
30 | - ✅ Universal compatibility beyond just Claude applications
31 | - ✅ Zero manual configuration required
32 | - ✅ Future-proof setup for any MCP application
33 |
34 | ### Command Line Options
35 |
36 | ```bash
37 | # Automatic multi-client setup (no prompts)
38 | python install.py --setup-multi-client
39 |
40 | # Skip the multi-client prompt entirely
41 | python install.py --skip-multi-client-prompt
42 |
43 | # Combined with other options
44 | python install.py --storage-backend sqlite_vec --setup-multi-client
45 | ```
46 |
47 | ### Supported Applications
48 |
49 | The integrated setup automatically detects and configures:
50 |
51 | #### Automatically Configured
52 | - **Claude Desktop**: Updates `claude_desktop_config.json` with multi-client settings
53 | - **Continue IDE**: Modifies Continue configuration files
54 | - **VS Code MCP Extension**: Updates VS Code MCP settings
55 | - **Cursor**: Configures Cursor MCP integration
56 | - **Generic MCP Clients**: Updates `.mcp.json` and similar configuration files
57 |
58 | #### Manual Configuration Required
59 | - **Custom MCP implementations**: May require manual configuration file updates
60 | - **Enterprise MCP clients**: Check with your IT department for configuration requirements
61 |
62 | ## 📁 Shared File Access (Local Networks)
63 |
64 | ### Overview
65 |
66 | For local networks with shared storage, multiple clients can access the same SQLite database using Write-Ahead Logging (WAL) mode.
67 |
68 | ### Quick Setup
69 |
70 | 1. **Run the setup script:**
71 | ```bash
72 | python setup_multi_client_complete.py
73 | ```
74 |
75 | 2. **Configure shared database location:**
76 | ```bash
77 | # Path to the SQLite-vec database file (folder will be created if needed)
78 | export MCP_MEMORY_SQLITE_PATH="/shared/network/mcp_memory/memory.db"
79 |
80 | # WAL is enabled by default by the service; no extra env needed
81 | ```
82 |
83 | 3. **Update each client configuration** to point to the shared location.
84 |
85 | ### Technical Implementation
86 |
87 | The shared file access uses SQLite's WAL (Write-Ahead Logging) mode for concurrent access:
88 |
89 | - **WAL Mode**: Enables multiple readers and one writer simultaneously
90 | - **File Locking**: Handles concurrent access safely
91 | - **Automatic Recovery**: SQLite handles crash recovery automatically
92 |
93 | ### Configuration Example
94 |
95 | For Claude Desktop on each client machine:
96 |
97 | ```json
98 | {
99 | "mcpServers": {
100 | "memory": {
101 | "command": "python",
102 | "args": ["/path/to/mcp-memory-service/src/mcp_memory_service/server.py"],
103 | "env": {
104 | "MCP_MEMORY_SQLITE_PATH": "/shared/network/mcp_memory/memory.db",
105 | "MCP_MEMORY_STORAGE_BACKEND": "sqlite_vec"
106 | }
107 | }
108 | }
109 | }
110 | ```
111 |
112 | ### Network Storage Requirements
113 |
114 | - **NFS/SMB Share**: Properly configured network file system
115 | - **File Permissions**: Read/write access for all client users
116 | - **Network Reliability**: Stable network connection to prevent corruption
117 |
118 | ## 🌐 Centralized HTTP/SSE Server (Cloud Deployment)
119 |
120 | ### Why This Approach?
121 |
122 | - ✅ **True Concurrency**: Proper handling of multiple simultaneous clients
123 | - ✅ **Real-time Updates**: Server-Sent Events (SSE) push changes to all clients instantly
124 | - ✅ **Cross-platform**: Works from any device with HTTP access
125 | - ✅ **Secure**: Optional API key authentication
126 | - ✅ **Scalable**: Can handle many concurrent clients
127 | - ✅ **Cloud-friendly**: No file locking issues
128 |
129 | ### Architecture
130 |
131 | ```
132 | ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
133 | │ Client PC 1 │ │ Client PC 2 │ │ Client PC 3 │
134 | │ (Claude App) │ │ (VS Code) │ │ (Web Client) │
135 | └─────────┬───────┘ └─────────┬───────┘ └─────────┬───────┘
136 | │ │ │
137 | │ HTTP/SSE API │ │
138 | └──────────────────────┼──────────────────────┘
139 | │
140 | ┌─────────────▼──────────────┐
141 | │ Central Server │
142 | │ ┌─────────────────────┐ │
143 | │ │ MCP Memory Service │ │
144 | │ │ HTTP/SSE Server │ │
145 | │ └─────────────────────┘ │
146 | │ ┌─────────────────────┐ │
147 | │ │ SQLite-vec DB │ │
148 | │ │ (Single Source) │ │
149 | │ └─────────────────────┘ │
150 | └────────────────────────────┘
151 | ```
152 |
153 | ### Server Installation
154 |
155 | 1. **Install on your server machine:**
156 | ```bash
157 | git clone https://github.com/doobidoo/mcp-memory-service.git
158 | cd mcp-memory-service
159 | python install.py --server-mode --storage-backend sqlite_vec
160 | ```
161 |
162 | 2. **Configure HTTP server:**
163 | ```bash
164 | export MCP_HTTP_HOST=0.0.0.0
165 | export MCP_HTTP_PORT=8000
166 | export MCP_API_KEY=your-secure-api-key
167 | ```
168 |
169 | 3. **Start the HTTP server:**
170 | ```bash
171 | python scripts/run_http_server.py
172 | ```
173 |
174 | ### Client Configuration (HTTP Mode)
175 |
176 | There are two reliable ways for clients to connect to the centralized server:
177 |
178 | - Direct Streamable HTTP (for clients that natively support MCP Streamable HTTP)
179 | - Via mcp-proxy (for stdio-only clients like Codex)
180 |
181 | Option A — Direct Streamable HTTP (preferred when supported):
182 |
183 | ```json
184 | {
185 | "mcpServers": {
186 | "memory": {
187 | "transport": "streamablehttp",
188 | "url": "http://your-server:8000/mcp",
189 | "headers": {
190 | "Authorization": "Bearer your-secure-api-key"
191 | }
192 | }
193 | }
194 | }
195 | ```
196 |
197 | Option B — mcp-proxy bridge (works with any stdio-only client):
198 |
199 | ```json
200 | {
201 | "mcpServers": {
202 | "memory": {
203 | "command": "mcp-proxy",
204 | "args": [
205 | "http://your-server:8000/mcp",
206 | "--transport=streamablehttp"
207 | ],
208 | "env": {
209 | "API_ACCESS_TOKEN": "your-secure-api-key"
210 | }
211 | }
212 | }
213 | }
214 | ```
215 |
216 | ### Security Configuration
217 |
218 | #### API Key Authentication
219 |
220 | ```bash
221 | # Generate a secure API key
222 | export MCP_API_KEY=$(openssl rand -hex 32)
223 |
224 | # Configure HTTPS (recommended for production)
225 | export MCP_HTTPS_ENABLED=true
226 | export MCP_SSL_CERT_FILE=/path/to/cert.pem
227 | export MCP_SSL_KEY_FILE=/path/to/key.pem
228 | ```
229 |
230 | #### Firewall Configuration
231 |
232 | ```bash
233 | # Allow HTTP/HTTPS access (adjust port as needed)
234 | sudo ufw allow 8000/tcp
235 | sudo ufw allow 8443/tcp # For HTTPS
236 | ```
237 |
238 | ### Docker Deployment
239 |
240 | For containerized deployment:
241 |
242 | ```yaml
243 | # docker-compose.yml
244 | version: '3.8'
245 | services:
246 | mcp-memory-service:
247 | build: .
248 | ports:
249 | - "8000:8000"
250 | environment:
251 | - MCP_HTTP_HOST=0.0.0.0
252 | - MCP_HTTP_PORT=8000
253 | - MCP_API_KEY=${MCP_API_KEY}
254 | - MCP_MEMORY_STORAGE_BACKEND=sqlite_vec
255 | volumes:
256 | - ./data:/app/data
257 | restart: unless-stopped
258 | ```
259 |
260 | ```bash
261 | # Deploy with Docker Compose
262 | docker-compose up -d
263 | ```
264 |
265 | ## Advanced Configuration
266 |
267 | Note: For the HTTP server interface, use `MCP_HTTP_HOST`, `MCP_HTTP_PORT`, and `MCP_API_KEY`. These supersede older `MCP_MEMORY_HTTP_*` names in legacy docs. Client-side tools may use different env vars (see below).
268 |
269 | ### Client Environment Variables
270 |
271 | - mcp-proxy: set `API_ACCESS_TOKEN` to pass the bearer token automatically.
272 | - Memory MCP Bridge (`docker-compose/mcp-gateway/scripts/memory-mcp-bridge.js`): set `MCP_MEMORY_API_KEY` and optionally `MCP_MEMORY_HTTP_ENDPOINT`, `MCP_MEMORY_AUTO_DISCOVER`, `MCP_MEMORY_PREFER_HTTPS`.
273 | - Direct Streamable HTTP clients: provide `Authorization: Bearer <MCP_API_KEY>` via headers (no special env var required).
274 |
275 | ### Environment Variables
276 |
277 | | Variable | Default | Description |
278 | |----------|---------|-------------|
279 | | `MCP_HTTP_ENABLED` | `false` | Enable HTTP mode (FastAPI + Streamable HTTP) |
280 | | `MCP_HTTP_HOST` | `0.0.0.0` | HTTP server bind address |
281 | | `MCP_HTTP_PORT` | `8000` | HTTP server port |
282 | | `MCP_API_KEY` | `none` | API key for auth (sent as `Authorization: Bearer ...`) |
283 | | `MCP_HTTPS_ENABLED` | `false` | Enable HTTPS termination |
284 | | `MCP_SSL_CERT_FILE` | `none` | Path to TLS certificate |
285 | | `MCP_SSL_KEY_FILE` | `none` | Path to TLS private key |
286 | | `MCP_CORS_ORIGINS` | `*` | CSV list of allowed origins |
287 | | `MCP_SSE_HEARTBEAT` | `30` | SSE heartbeat interval (seconds) |
288 | | `MCP_MEMORY_STORAGE_BACKEND` | `sqlite_vec` | `sqlite_vec`, `chroma`, or `cloudflare` |
289 | | `MCP_MEMORY_SQLITE_PATH` | `<base>/sqlite_vec.db` | SQLite-vec database file path |
290 | | `MCP_MEMORY_SQLITEVEC_PATH` | `none` | Alternate var for SQLite path (if set, used) |
291 | | `MCP_MEMORY_SQLITE_PRAGMAS` | `none` | Override SQLite pragmas (e.g. `busy_timeout=15000,cache_size=20000`) |
292 | | `MCP_MDNS_ENABLED` | `true` | Enable mDNS advertising/discovery |
293 | | `MCP_MDNS_SERVICE_NAME` | `MCP Memory Service` | mDNS service name |
294 | | `MCP_MDNS_SERVICE_TYPE` | `_mcp-memory._tcp.local.` | mDNS service type |
295 | | `MCP_MDNS_DISCOVERY_TIMEOUT` | `5` | mDNS discovery timeout (seconds) |
296 |
297 | Deprecated (replaced):
298 | - `MCP_MEMORY_HTTP_HOST` → `MCP_HTTP_HOST`
299 | - `MCP_MEMORY_HTTP_PORT` → `MCP_HTTP_PORT`
300 | - `MCP_MEMORY_API_KEY` → `MCP_API_KEY` (server HTTP mode). Note: the standalone Memory MCP Bridge continues to use `MCP_MEMORY_API_KEY`.
301 | - `MCP_MEMORY_ENABLE_WAL`: not needed; WAL is enabled by default. Use `MCP_MEMORY_SQLITE_PRAGMAS` to change.
302 | - `MCP_MEMORY_ENABLE_SSE`: not required; SSE events are enabled with the HTTP server.
303 | - `MCP_MEMORY_MULTI_CLIENT`, `MCP_MEMORY_MAX_CLIENTS`: not used.
304 |
305 | ### Performance Tuning
306 |
307 | #### SQLite Configuration
308 |
309 | ```bash
310 | # Optimize for concurrent access (v8.9.0+)
311 | # Recommended values for HTTP + MCP server concurrent access
312 | export MCP_MEMORY_SQLITE_PRAGMAS="busy_timeout=15000,cache_size=20000"
313 |
314 | # Note: WAL mode (journal_mode=WAL) is enabled by default
315 | # These values are automatically configured by the installer for hybrid/sqlite_vec backends
316 | ```
317 |
318 | #### HTTP Server Tuning
319 |
320 | ```bash
321 | # Adjust for high concurrency
322 | export MCP_HTTP_WORKERS=4
323 | export MCP_HTTP_TIMEOUT=30
324 | export MCP_HTTP_KEEPALIVE=true
325 | ```
326 |
327 | ## Troubleshooting
328 |
329 | ### Common Issues
330 |
331 | #### 1. Database Lock Errors
332 |
333 | **Symptom**: `database is locked` errors during concurrent HTTP + MCP server access
334 |
335 | **Solution (v8.9.0+)**: Configure proper SQLite pragmas for concurrent access:
336 |
337 | ```bash
338 | # Set recommended pragma values (15 second timeout, larger cache)
339 | export MCP_MEMORY_SQLITE_PRAGMAS="busy_timeout=15000,cache_size=20000"
340 |
341 | # Restart both HTTP server and MCP server to apply changes
342 | # Note: The installer automatically configures these values for hybrid/sqlite_vec backends
343 | ```
344 |
345 | **Root Cause**: Default `busy_timeout=5000ms` (5 seconds) is too short when both HTTP server and MCP server access the same SQLite database. The fix increases timeout to 15 seconds and cache to 20,000 pages.
346 |
347 | **Additional checks** (if issue persists):
348 | ```bash
349 | # Verify WAL mode is enabled (should be default)
350 | sqlite3 /path/to/memory.db "PRAGMA journal_mode;"
351 | # Should show: wal
352 |
353 | # Check file permissions
354 | chmod 666 /path/to/memory.db
355 | chmod 666 /path/to/memory.db-wal 2>/dev/null || true
356 | chmod 666 /path/to/memory.db-shm 2>/dev/null || true
357 | ```
358 |
359 | #### 2. Network Access Issues
360 |
361 | **Symptom**: Clients can't connect to HTTP server
362 | **Solution**: Check firewall and network connectivity:
363 |
364 | ```bash
365 | # Test server connectivity
366 | curl http://your-server:8000/health
367 |
368 | # Check firewall rules
369 | sudo ufw status
370 | ```
371 |
372 | #### 3. Configuration Conflicts
373 |
374 | **Symptom**: Clients use different configurations
375 | **Solution**: Verify all clients use the same settings:
376 |
377 | ```bash
378 | # Check environment variables on each client
379 | env | grep MCP_MEMORY
380 |
381 | # Verify database file path matches
382 | ls -la "$MCP_MEMORY_SQLITE_PATH"
383 | ```
384 |
385 | ### Diagnostic Commands
386 |
387 | #### Check Multi-Client Status
388 |
389 | ```bash
390 | # Test multi-client setup
391 | python scripts/test_multi_client.py
392 |
393 | # Verify database access
394 | python -c "
395 | import os, sqlite3
396 | db = os.environ.get('MCP_MEMORY_SQLITE_PATH', '')
397 | conn = sqlite3.connect(db) if db else None
398 | print(f'Database accessible: {bool(conn)} (path={db})')
399 | conn and conn.close()
400 | "
401 | ```
402 |
403 | #### Monitor Client Connections
404 |
405 | ```bash
406 | # For HTTP server deployment
407 | curl http://your-server:8000/stats
408 |
409 | # Check active connections
410 | netstat -an | grep :8000
411 | ```
412 |
413 | ## Migration from Single-Client
414 |
415 | ### Upgrading Existing Installation
416 |
417 | 1. **Backup existing data:**
418 | ```bash
419 | python scripts/backup_memories.py
420 | ```
421 |
422 | 2. **Run multi-client setup:**
423 | ```bash
424 | python install.py --setup-multi-client --migrate-existing
425 | ```
426 |
427 | 3. **Update client configurations** as needed.
428 |
429 | ### Data Migration
430 |
431 | The installer automatically handles data migration, but you can also run it manually:
432 |
433 | ```bash
434 | # Migrate to shared database location
435 | python scripts/migrate_to_multi_client.py \
436 | --source ~/.mcp_memory_chroma \
437 | --target /shared/mcp_memory
438 | ```
439 |
440 | ## Related Documentation
441 |
442 | - [Installation Guide](../installation/master-guide.md) - General installation instructions
443 | - [Deployment Guide](../deployment/docker.md) - Docker and cloud deployment
444 | - [Troubleshooting](../troubleshooting/general.md) - Multi-client specific issues
445 | - [API Reference](../IMPLEMENTATION_PLAN_HTTP_SSE.md) - HTTP/SSE API documentation
446 |
447 | ## Client Setup Recipes (Codex, Cursor, Qwen, Gemini)
448 |
449 | This section provides practical, copy-pasteable setups for popular MCP clients. Use Streamable HTTP at `http://<host>:8000/mcp` when supported, or bridge via `mcp-proxy` for stdio-only clients.
450 |
451 | Important:
452 | - Server API key: set `MCP_API_KEY` on the server. Clients must send `Authorization: Bearer <MCP_API_KEY>`.
453 | - Our MCP endpoint is Streamable HTTP at `/mcp` (not the SSE events feed at `/api/events`).
454 |
455 | ### Codex (via mcp-proxy)
456 |
457 | Codex does not natively support HTTP transport. Use `mcp-proxy` to bridge stdio ⇄ Streamable HTTP.
458 |
459 | 1) Install mcp-proxy
460 | ```bash
461 | pipx install mcp-proxy # or: uv tool install mcp-proxy
462 | ```
463 |
464 | 2) Update Codex MCP config (see Codex docs for exact file location):
465 | ```json
466 | {
467 | "mcpServers": {
468 | "memory": {
469 | "command": "mcp-proxy",
470 | "args": [
471 | "http://your-server:8000/mcp",
472 | "--transport=streamablehttp"
473 | ],
474 | "env": {
475 | "API_ACCESS_TOKEN": "your-secure-api-key"
476 | }
477 | }
478 | }
479 | }
480 | ```
481 |
482 | Reference template: `examples/codex-mcp-config.json` in this repository.
483 |
484 | Notes:
485 | - Replace `your-server` and `your-secure-api-key` accordingly. For local testing use `http://127.0.0.1:8000/mcp`.
486 | - Alternatively pass headers explicitly: `"args": ["http://.../mcp", "--transport=streamablehttp", "--headers", "Authorization", "Bearer your-secure-api-key"]`.
487 |
488 | ### Cursor
489 |
490 | Pick one of these depending on your deployment:
491 |
492 | - Option A — Local stdio (single machine):
493 | ```json
494 | {
495 | "mcpServers": {
496 | "memory": {
497 | "command": "uv",
498 | "args": ["--directory", "/path/to/mcp-memory-service", "run", "memory"],
499 | "env": {
500 | "MCP_MEMORY_STORAGE_BACKEND": "sqlite_vec"
501 | }
502 | }
503 | }
504 | }
505 | ```
506 |
507 | - Option B — Remote central server via mcp-proxy (recommended for multi-client):
508 | ```json
509 | {
510 | "mcpServers": {
511 | "memory": {
512 | "command": "mcp-proxy",
513 | "args": [
514 | "http://your-server:8000/mcp",
515 | "--transport=streamablehttp"
516 | ],
517 | "env": {
518 | "API_ACCESS_TOKEN": "your-secure-api-key"
519 | }
520 | }
521 | }
522 | }
523 | ```
524 |
525 | - Option C — Direct Streamable HTTP (if your Cursor version supports it):
526 | ```json
527 | {
528 | "mcpServers": {
529 | "memory": {
530 | "transport": "streamablehttp",
531 | "url": "http://your-server:8000/mcp",
532 | "headers": { "Authorization": "Bearer your-secure-api-key" }
533 | }
534 | }
535 | }
536 | ```
537 |
538 | ### Qwen
539 |
540 | Qwen clients that support MCP can connect either directly via Streamable HTTP or through `mcp-proxy` when only stdio is available. If your Qwen client UI accepts an MCP server list, use one of the Cursor-style examples above. If it only lets you specify a command, use the `mcp-proxy` form:
541 |
542 | ```json
543 | {
544 | "mcpServers": {
545 | "memory": {
546 | "command": "mcp-proxy",
547 | "args": [
548 | "http://your-server:8000/mcp",
549 | "--transport=streamablehttp"
550 | ],
551 | "env": { "API_ACCESS_TOKEN": "your-secure-api-key" }
552 | }
553 | }
554 | }
555 | ```
556 |
557 | Tips:
558 | - Some Qwen distributions expose MCP configuration in a UI. Map fields as: transport = Streamable HTTP, URL = `http://<host>:8000/mcp`, header `Authorization: Bearer <key>`.
559 |
560 | ### Gemini
561 |
562 | Gemini-based IDE integrations (e.g., Gemini Code Assist in VS Code/JetBrains) typically support MCP via a config file or UI. Use either direct Streamable HTTP or `mcp-proxy`:
563 |
564 | - Direct Streamable HTTP (when supported):
565 | ```json
566 | {
567 | "mcpServers": {
568 | "memory": {
569 | "transport": "streamablehttp",
570 | "url": "https://your-server:8443/mcp",
571 | "headers": { "Authorization": "Bearer your-secure-api-key" }
572 | }
573 | }
574 | }
575 | ```
576 |
577 | - Via mcp-proxy (works everywhere):
578 | ```json
579 | {
580 | "mcpServers": {
581 | "memory": {
582 | "command": "mcp-proxy",
583 | "args": [
584 | "https://your-server:8443/mcp",
585 | "--transport=streamablehttp"
586 | ],
587 | "env": { "API_ACCESS_TOKEN": "your-secure-api-key" }
588 | }
589 | }
590 | }
591 | ```
592 |
593 | If your Gemini client expects a command-only entry, prefer the `mcp-proxy` form.
594 |
595 | ---
596 |
597 | Troubleshooting client connections:
598 | - Ensure you’re using `/mcp` (Streamable HTTP), not `/api/events` (SSE).
599 | - Verify server exports `MCP_API_KEY` and clients send `Authorization: Bearer ...`.
600 | - For remote setups, test reachability: `curl -i http://your-server:8000/api/health`.
601 | - If a client doesn’t support Streamable HTTP, use `mcp-proxy`.
602 |
```
--------------------------------------------------------------------------------
/scripts/quality/phase1_dead_code_analysis.md:
--------------------------------------------------------------------------------
```markdown
1 | # Phase 1: Dead Code Removal Analysis for Issue #240
2 |
3 | **Generated:** 2025-11-24
4 | **Based on:** pyscn report `analyze_20251123_214224.html`
5 | **Current Health Score:** 63/100 (Grade C)
6 | **Dead Code Score:** 70/100 (27 issues, 2 critical)
7 |
8 | ---
9 |
10 | ## Executive Summary
11 |
12 | Based on the pyscn analysis, this codebase has **27 dead code issues** that need to be addressed. After detailed analysis of the report, I've identified the following breakdown:
13 |
14 | - **Total Issues:** 27
15 | - **Critical:** 2 (1 critical unreachable code block in `scripts/installation/install.py`)
16 | - **Warnings:** 25 (unreachable branches in the same function)
17 | - **Safe to Remove Immediately:** 27 (all issues are in the same function with a clear root cause)
18 | - **Needs Investigation:** 0
19 | - **False Positives:** 0
20 |
21 | **Estimated Health Score Improvement:**
22 | - **Before:** Dead Code 70/100, Overall 63/100
23 | - **After:** Dead Code 85-90/100, Overall 68-72/100
24 | - **Confidence:** High (95%)
25 |
26 | ### Root Cause Analysis
27 |
28 | All 27 dead code issues stem from a **single premature return statement** in the `configure_paths()` function at line 1358 of `scripts/installation/install.py`. This return statement makes 77 lines of critical Claude Desktop configuration code unreachable.
29 |
30 | **Impact:**
31 | - Claude Desktop configuration is never applied during installation
32 | - Users must manually configure Claude Desktop after installation
33 | - Installation verification may fail silently
34 |
35 | ---
36 |
37 | ## Critical Dead Code (Priority 1)
38 |
39 | ### Issue 1: Unreachable Claude Desktop Configuration Block
40 |
41 | **File:** `scripts/installation/install.py`
42 | **Function:** `configure_paths`
43 | **Lines:** 1360-1436 (77 lines)
44 | **Type:** Unreachable code after return statement
45 | **Severity:** **CRITICAL**
46 | **References:** 0 (verified - this is genuine dead code)
47 |
48 | **Root Cause:**
49 | Line 1358 contains `return False` inside a try-except block, causing the entire Claude Desktop configuration logic to be unreachable.
50 |
51 | **Code Context (Lines 1350-1365):**
52 | ```python
53 | try:
54 | test_file = os.path.join(backups_path, '.write_test')
55 | with open(test_file, 'w') as f:
56 | f.write('test')
57 | os.remove(test_file)
58 | print_success("Storage directories created and are writable")
59 | except Exception as e:
60 | print_error(f"Failed to test backups directory: {e}")
61 | return False # ← PROBLEM: This return makes all code below unreachable
62 |
63 | # Configure Claude Desktop if available # ← UNREACHABLE
64 | claude_config_paths = [
65 | home_dir / 'Library' / 'Application Support' / 'Claude' / 'claude_desktop_config.json',
66 | home_dir / '.config' / 'Claude' / 'claude_desktop_config.json',
67 | Path('claude_config') / 'claude_desktop_config.json'
68 | ]
69 | ```
70 |
71 | **Unreachable Code Block (Lines 1360-1436):**
72 | ```python
73 | # Configure Claude Desktop if available
74 | claude_config_paths = [
75 | home_dir / 'Library' / 'Application Support' / 'Claude' / 'claude_desktop_config.json',
76 | home_dir / '.config' / 'Claude' / 'claude_desktop_config.json',
77 | Path('claude_config') / 'claude_desktop_config.json'
78 | ]
79 |
80 | for config_path in claude_config_paths:
81 | if config_path.exists():
82 | print_info(f"Found Claude Desktop config at {config_path}")
83 | try:
84 | import json
85 | with open(config_path, 'r') as f:
86 | config = json.load(f)
87 |
88 | # Update or add MCP Memory configuration
89 | if 'mcpServers' not in config:
90 | config['mcpServers'] = {}
91 |
92 | # Create environment configuration based on storage backend
93 | env_config = {
94 | "MCP_MEMORY_BACKUPS_PATH": str(backups_path),
95 | "MCP_MEMORY_STORAGE_BACKEND": storage_backend
96 | }
97 |
98 | if storage_backend in ['sqlite_vec', 'hybrid']:
99 | env_config["MCP_MEMORY_SQLITE_PATH"] = str(storage_path)
100 | env_config["MCP_MEMORY_SQLITE_PRAGMAS"] = "busy_timeout=15000,cache_size=20000"
101 |
102 | if storage_backend in ['hybrid', 'cloudflare']:
103 | cloudflare_env_vars = [
104 | 'CLOUDFLARE_API_TOKEN',
105 | 'CLOUDFLARE_ACCOUNT_ID',
106 | 'CLOUDFLARE_D1_DATABASE_ID',
107 | 'CLOUDFLARE_VECTORIZE_INDEX'
108 | ]
109 | for var in cloudflare_env_vars:
110 | value = os.environ.get(var)
111 | if value:
112 | env_config[var] = value
113 |
114 | if storage_backend == 'chromadb':
115 | env_config["MCP_MEMORY_CHROMA_PATH"] = str(storage_path)
116 |
117 | # Create or update the memory server configuration
118 | if system_info["is_windows"]:
119 | script_path = os.path.abspath("memory_wrapper.py")
120 | config['mcpServers']['memory'] = {
121 | "command": "python",
122 | "args": [script_path],
123 | "env": env_config
124 | }
125 | print_info("Configured Claude Desktop to use memory_wrapper.py for Windows")
126 | else:
127 | config['mcpServers']['memory'] = {
128 | "command": "uv",
129 | "args": [
130 | "--directory",
131 | os.path.abspath("."),
132 | "run",
133 | "memory"
134 | ],
135 | "env": env_config
136 | }
137 |
138 | with open(config_path, 'w') as f:
139 | json.dump(config, f, indent=2)
140 |
141 | print_success("Updated Claude Desktop configuration")
142 | except Exception as e:
143 | print_warning(f"Failed to update Claude Desktop configuration: {e}")
144 | break
145 |
146 | return True
147 | ```
148 |
149 | **Classification:** ✅ **Safe to Remove and Fix**
150 |
151 | **Recommended Fix:**
152 | The `return False` at line 1358 should NOT be removed - it's a valid error condition. Instead, the Claude Desktop configuration code (lines 1360-1436) needs to be **moved outside the try-except block** so it executes regardless of the write test result.
153 |
154 | **Fix Strategy:**
155 | 1. Remove lines 1360-1436 from current location (inside except block)
156 | 2. Dedent and move this code block to after line 1358 (after the except block closes)
157 | 3. Ensure proper indentation and flow control
158 |
159 | **Detailed Fix:**
160 |
161 | ```python
162 | # BEFORE (Current broken code):
163 | try:
164 | test_file = os.path.join(backups_path, '.write_test')
165 | with open(test_file, 'w') as f:
166 | f.write('test')
167 | os.remove(test_file)
168 | print_success("Storage directories created and are writable")
169 | except Exception as e:
170 | print_error(f"Failed to test backups directory: {e}")
171 | return False
172 |
173 | # Configure Claude Desktop if available # ← UNREACHABLE
174 | claude_config_paths = [...]
175 |
176 | # AFTER (Fixed code):
177 | try:
178 | test_file = os.path.join(backups_path, '.write_test')
179 | with open(test_file, 'w') as f:
180 | f.write('test')
181 | os.remove(test_file)
182 | print_success("Storage directories created and are writable")
183 | except Exception as e:
184 | print_error(f"Failed to test backups directory: {e}")
185 | # Don't return False here - we can still configure Claude Desktop
186 | print_warning("Continuing with Claude Desktop configuration despite write test failure")
187 |
188 | # Configure Claude Desktop if available # ← NOW REACHABLE
189 | claude_config_paths = [
190 | home_dir / 'Library' / 'Application Support' / 'Claude' / 'claude_desktop_config.json',
191 | home_dir / '.config' / 'Claude' / 'claude_desktop_config.json',
192 | Path('claude_config') / 'claude_desktop_config.json'
193 | ]
194 |
195 | for config_path in claude_config_paths:
196 | # ... rest of configuration logic ...
197 | break
198 |
199 | return True
200 | ```
201 |
202 | **Verification Command:**
203 | ```bash
204 | # After fix, verify with:
205 | python -m py_compile scripts/installation/install.py
206 | pyscn analyze scripts/installation/install.py --dead-code
207 | ```
208 |
209 | ---
210 |
211 | ## Detailed Issue Breakdown (All 27 Issues)
212 |
213 | All 27 issues are variations of the same root cause. Here's the complete list from pyscn:
214 |
215 | | # | Lines | Severity | Reason | Description |
216 | |---|-------|----------|--------|-------------|
217 | | 1 | 1361-1365 | Critical | unreachable_after_return | Comment and variable declarations |
218 | | 2 | 1367-1436 | Warning | unreachable_branch | Entire for loop and configuration logic |
219 | | 3 | 1368-1436 | Warning | unreachable_branch | For loop body |
220 | | 4 | 1369-1369 | Warning | unreachable_branch | If condition check |
221 | | 5 | 1371-1371 | Warning | unreachable_branch | Import statement |
222 | | 6 | 1372-1373 | Warning | unreachable_branch | File read |
223 | | 7 | 1373-1373 | Warning | unreachable_branch | JSON load |
224 | | 8 | 1376-1377 | Warning | unreachable_branch | Config check |
225 | | 9 | 1377-1377 | Warning | unreachable_branch | Dictionary assignment |
226 | | 10 | 1380-1388 | Warning | unreachable_branch | env_config creation |
227 | | ... | ... | ... | ... | (17 more warnings for nested code blocks) |
228 |
229 | **Note:** These are all sub-issues of the main critical issue. Fixing the root cause (moving the code block) will resolve all 27 issues simultaneously.
230 |
231 | ---
232 |
233 | ## Removal Script
234 |
235 | **Important:** This is not a simple removal - it's a **code restructuring** to make the unreachable code reachable.
236 |
237 | ### Manual Fix Script
238 |
239 | ```bash
240 | #!/bin/bash
241 | # scripts/quality/fix_dead_code_install.sh
242 | # Fix unreachable Claude Desktop configuration in install.py
243 |
244 | set -e
245 |
246 | PROJECT_ROOT="/Users/hkr/Documents/GitHub/mcp-memory-service"
247 | cd "$PROJECT_ROOT"
248 |
249 | INSTALL_FILE="scripts/installation/install.py"
250 |
251 | echo "=== Phase 1: Fix Dead Code in install.py ==="
252 | echo ""
253 |
254 | # Backup
255 | BRANCH_NAME="quality/fix-dead-code-install-$(date +%Y%m%d-%H%M%S)"
256 | git checkout -b "$BRANCH_NAME"
257 | echo "✓ Created branch: $BRANCH_NAME"
258 | echo ""
259 |
260 | # Create backup of original file
261 | cp "$INSTALL_FILE" "$INSTALL_FILE.backup"
262 | echo "✓ Backed up $INSTALL_FILE to $INSTALL_FILE.backup"
263 | echo ""
264 |
265 | echo "Manual fix required for this issue:"
266 | echo "1. Open $INSTALL_FILE"
267 | echo "2. Locate line 1358: 'return False'"
268 | echo "3. Change it to: print_warning('Continuing with Claude Desktop configuration despite write test failure')"
269 | echo "4. Cut lines 1360-1436 (Claude Desktop configuration)"
270 | echo "5. Paste them AFTER the except block (after current line 1358)"
271 | echo "6. Adjust indentation to match outer scope"
272 | echo "7. Save file"
273 | echo ""
274 |
275 | read -p "Press Enter after making the manual fix..."
276 |
277 | # Verify syntax
278 | echo "Verifying Python syntax..."
279 | if python -m py_compile "$INSTALL_FILE"; then
280 | echo "✓ Python syntax valid"
281 | else
282 | echo "✗ Python syntax error - reverting"
283 | mv "$INSTALL_FILE.backup" "$INSTALL_FILE"
284 | exit 1
285 | fi
286 |
287 | # Run pyscn to verify fix
288 | echo ""
289 | echo "Running pyscn to verify fix..."
290 | pyscn analyze "$INSTALL_FILE" --dead-code --output .pyscn/reports/
291 |
292 | # Run tests
293 | echo ""
294 | echo "Running installation tests..."
295 | if pytest tests/unit/test_installation.py -v; then
296 | echo "✓ Installation tests passed"
297 | else
298 | echo "⚠ Some tests failed - review manually"
299 | fi
300 |
301 | # Summary
302 | echo ""
303 | echo "=== Summary ==="
304 | git diff --stat "$INSTALL_FILE"
305 | echo ""
306 | echo "✓ Dead code fix applied"
307 | echo ""
308 | echo "Next steps:"
309 | echo "1. Review changes: git diff $INSTALL_FILE"
310 | echo "2. Test installation: python scripts/installation/install.py --storage-backend sqlite_vec"
311 | echo "3. Verify Claude Desktop config is created"
312 | echo "4. Commit: git commit -m 'fix: move Claude Desktop configuration out of unreachable code block (issue #240 Phase 1)'"
313 | echo "5. Re-run pyscn: pyscn analyze . --output .pyscn/reports/"
314 | ```
315 |
316 | ### Automated Fix Script (Using sed)
317 |
318 | **Warning:** This is complex due to the need to move and re-indent code. Manual fix is recommended.
319 |
320 | ---
321 |
322 | ## Risk Assessment Matrix
323 |
324 | | Item | Risk | Impact | Testing | Rollback | Priority |
325 | |------|------|--------|---------|----------|----------|
326 | | Move Claude Desktop config code | **Low** | **High** - Fixes installation for all users | `pytest tests/unit/test_installation.py` | `git revert` or restore from backup | **P1** |
327 | | Change `return False` to warning | **Low** | Medium - Changes error handling behavior | Manual installation test | `git revert` | **P1** |
328 | | Indentation adjustment | **Very Low** | High - Code won't run if wrong | `python -m py_compile` | `git revert` | **P1** |
329 |
330 | **Overall Risk Level:** Low
331 | **Reason:** This is a straightforward code movement with clear intent. The original code was never executing, so we're not changing existing behavior - we're enabling intended behavior.
332 |
333 | ---
334 |
335 | ## Expected Impact
336 |
337 | ### Before Fix
338 | ```
339 | Health Score: 63/100
340 | ├─ Complexity: 40/100
341 | ├─ Dead Code: 70/100 (27 issues, 2 critical)
342 | ├─ Duplication: 30/100
343 | ├─ Coupling: 100/100
344 | ├─ Dependencies: 85/100
345 | └─ Architecture: 75/100
346 | ```
347 |
348 | ### After Fix (Estimated)
349 | ```
350 | Health Score: 68-72/100 (+5 to +9)
351 | ├─ Complexity: 40/100 (unchanged)
352 | ├─ Dead Code: 85-90/100 (0 issues, 0 critical) [+15 to +20]
353 | ├─ Duplication: 30/100 (unchanged)
354 | ├─ Coupling: 100/100 (unchanged)
355 | ├─ Dependencies: 85/100 (unchanged)
356 | └─ Architecture: 75/100 (unchanged)
357 | ```
358 |
359 | **Confidence:** High (95%)
360 |
361 | **Rationale:**
362 | - Fixing all 27 dead code issues simultaneously by addressing the root cause
363 | - Dead code score expected to improve by 15-20 points (from 70 to 85-90)
364 | - Overall health score improvement of 5-9 points (from 63 to 68-72)
365 | - This is a conservative estimate - could be higher if pyscn weighs critical issues heavily
366 |
367 | **Additional Benefits:**
368 | - Installation process will work correctly for Claude Desktop configuration
369 | - Users won't need manual post-installation configuration
370 | - Improved user experience and reduced support requests
371 |
372 | ---
373 |
374 | ## Testing Strategy
375 |
376 | ### Pre-Fix Verification
377 | 1. **Confirm current behavior:**
378 | ```bash
379 | # Run installer and verify Claude Desktop config is NOT created
380 | python scripts/installation/install.py --storage-backend sqlite_vec
381 | # Check: Is ~/.claude/claude_desktop_config.json updated? (Should be NO)
382 | ```
383 |
384 | ### Post-Fix Verification
385 | 1. **Syntax Check:**
386 | ```bash
387 | python -m py_compile scripts/installation/install.py
388 | ```
389 |
390 | 2. **Unit Tests:**
391 | ```bash
392 | pytest tests/unit/test_installation.py -v
393 | ```
394 |
395 | 3. **Integration Test:**
396 | ```bash
397 | # Test full installation flow
398 | python scripts/installation/install.py --storage-backend sqlite_vec
399 | # Verify Claude Desktop config IS created
400 | cat ~/.claude/claude_desktop_config.json | grep "mcp-memory-service"
401 | ```
402 |
403 | 4. **pyscn Re-analysis:**
404 | ```bash
405 | pyscn analyze . --output .pyscn/reports/
406 | # Verify dead code issues reduced from 27 to 0
407 | ```
408 |
409 | 5. **Edge Case Testing:**
410 | ```bash
411 | # Test with different storage backends
412 | python scripts/installation/install.py --storage-backend hybrid
413 | python scripts/installation/install.py --storage-backend cloudflare
414 | ```
415 |
416 | ---
417 |
418 | ## Next Steps
419 |
420 | ### Immediate Actions (Phase 1)
421 | 1. ✅ **Review this analysis** - Confirm the root cause and fix strategy
422 | 2. ⏳ **Apply the fix manually** - Edit `scripts/installation/install.py`
423 | 3. ⏳ **Run tests** - Verify no regressions: `pytest tests/unit/test_installation.py`
424 | 4. ⏳ **Test installation** - Run full installer and verify Claude config created
425 | 5. ⏳ **Commit changes** - Use semantic commit message
426 | 6. ⏳ **Re-run pyscn** - Verify health score improvement
427 |
428 | ### Commit Message Template
429 | ```
430 | fix: move Claude Desktop configuration out of unreachable code block
431 |
432 | Fixes issue #240 Phase 1 - Dead Code Removal
433 |
434 | The configure_paths() function had a 'return False' statement inside
435 | an exception handler that made 77 lines of Claude Desktop configuration
436 | code unreachable. This caused installations to skip Claude Desktop setup.
437 |
438 | Changes:
439 | - Move Claude Desktop config code (lines 1360-1436) outside except block
440 | - Replace premature 'return False' with warning message
441 | - Ensure config runs regardless of write test result
442 |
443 | Impact:
444 | - Resolves all 27 dead code issues identified by pyscn
445 | - Claude Desktop now configured automatically during installation
446 | - Dead code score: 70 → 85-90 (+15 to +20 points)
447 | - Overall health score: 63 → 68-72 (+5 to +9 points)
448 |
449 | Testing:
450 | - Syntax validated with py_compile
451 | - Unit tests pass: pytest tests/unit/test_installation.py
452 | - Manual installation tested with sqlite_vec backend
453 | - pyscn re-analysis confirms 0 dead code issues
454 |
455 | Co-authored-by: pyscn analysis tool
456 | ```
457 |
458 | ### Follow-up Actions (Phase 2)
459 | After Phase 1 is complete and merged:
460 | 1. **Run pyscn again** - Get updated health score
461 | 2. **Analyze complexity issues** - Address complexity score of 40/100
462 | 3. **Review duplication** - Address duplication score of 30/100
463 | 4. **Create Phase 2 plan** - Target low-hanging complexity reductions
464 |
465 | ---
466 |
467 | ## Appendix: pyscn Report Metadata
468 |
469 | **Report File:** `.pyscn/reports/analyze_20251123_214224.html`
470 | **Generated:** 2025-11-23 21:42:24
471 | **Total Files Analyzed:** 252
472 | **Total Functions:** 567
473 | **Average Complexity:** 9.52
474 |
475 | **Health Score Breakdown:**
476 | - Overall: 63/100 (Grade C)
477 | - Complexity: 40/100 (28 high-risk functions)
478 | - Dead Code: 70/100 (27 issues, 2 critical)
479 | - Duplication: 30/100 (6.0% duplication, 18 groups)
480 | - Coupling (CBO): 100/100 (excellent)
481 | - Dependencies: 85/100 (no cycles)
482 | - Architecture: 75/100 (75.5% compliant)
483 |
484 | ---
485 |
486 | ## Conclusion
487 |
488 | This Phase 1 analysis identifies a single root cause affecting all 27 dead code issues: a premature `return False` statement in the `configure_paths()` function. By moving 77 lines of Claude Desktop configuration code outside the exception handler, we can:
489 |
490 | 1. **Eliminate all 27 dead code issues** identified by pyscn
491 | 2. **Fix a critical installation bug** where Claude Desktop is never configured
492 | 3. **Improve overall health score by 5-9 points** (from 63 to 68-72)
493 | 4. **Improve dead code score by 15-20 points** (from 70 to 85-90)
494 |
495 | The fix is straightforward, low-risk, and has high impact. This sets the stage for Phase 2, where we can tackle complexity and duplication issues with a cleaner codebase.
496 |
497 | **Recommendation:** Proceed with manual fix using the strategy outlined above. Automated sed script is possible but manual fix is safer given the code movement and indentation requirements.
498 |
```
--------------------------------------------------------------------------------
/claude-hooks/utilities/dynamic-context-updater.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Dynamic Context Updater
3 | * Orchestrates intelligent context updates during active conversations
4 | * Phase 2: Intelligent Context Updates
5 | */
6 |
7 | const { analyzeConversation, detectTopicChanges } = require('./conversation-analyzer');
8 | const { scoreMemoryRelevance } = require('./memory-scorer');
9 | const { formatMemoriesForContext } = require('./context-formatter');
10 | const { getSessionTracker } = require('./session-tracker');
11 |
12 | /**
13 | * Dynamic Context Update Manager
14 | * Coordinates between conversation analysis, memory retrieval, and context injection
15 | */
16 | class DynamicContextUpdater {
17 | constructor(options = {}) {
18 | this.options = {
19 | updateThreshold: 0.3, // Minimum significance score to trigger update
20 | maxMemoriesPerUpdate: 3, // Maximum memories to inject per update
21 | updateCooldownMs: 30000, // Minimum time between updates (30 seconds)
22 | maxUpdatesPerSession: 10, // Maximum updates per session
23 | debounceMs: 5000, // Debounce rapid conversation changes
24 | enableCrossSessionContext: true, // Include cross-session intelligence
25 | ...options
26 | };
27 |
28 | this.lastUpdateTime = 0;
29 | this.updateCount = 0;
30 | this.conversationBuffer = '';
31 | this.lastAnalysis = null;
32 | this.loadedMemoryHashes = new Set();
33 | this.sessionTracker = null;
34 | this.debounceTimer = null;
35 | }
36 |
37 | /**
38 | * Initialize the dynamic context updater
39 | */
40 | async initialize(sessionContext = {}) {
41 | console.log('[Dynamic Context] Initializing dynamic context updater...');
42 |
43 | this.sessionContext = sessionContext;
44 | this.updateCount = 0;
45 | this.loadedMemoryHashes.clear();
46 |
47 | if (this.options.enableCrossSessionContext) {
48 | this.sessionTracker = getSessionTracker();
49 | await this.sessionTracker.initialize();
50 | }
51 |
52 | console.log('[Dynamic Context] Dynamic context updater initialized');
53 | }
54 |
55 | /**
56 | * Process conversation update and potentially inject new context
57 | * @param {string} conversationText - Current conversation content
58 | * @param {object} memoryServiceConfig - Memory service configuration
59 | * @param {function} contextInjector - Function to inject context into conversation
60 | */
61 | async processConversationUpdate(conversationText, memoryServiceConfig, contextInjector) {
62 | try {
63 | // Check rate limiting
64 | if (!this.shouldProcessUpdate()) {
65 | return { processed: false, reason: 'rate_limited' };
66 | }
67 |
68 | // Debounce rapid updates
69 | if (this.debounceTimer) {
70 | clearTimeout(this.debounceTimer);
71 | }
72 |
73 | return new Promise((resolve) => {
74 | this.debounceTimer = setTimeout(async () => {
75 | const result = await this.performContextUpdate(
76 | conversationText,
77 | memoryServiceConfig,
78 | contextInjector
79 | );
80 | resolve(result);
81 | }, this.options.debounceMs);
82 | });
83 |
84 | } catch (error) {
85 | console.error('[Dynamic Context] Error processing conversation update:', error.message);
86 | return { processed: false, error: error.message };
87 | }
88 | }
89 |
90 | /**
91 | * Perform the actual context update
92 | */
93 | async performContextUpdate(conversationText, memoryServiceConfig, contextInjector) {
94 | console.log('[Dynamic Context] Processing conversation update...');
95 |
96 | // Analyze current conversation
97 | const currentAnalysis = analyzeConversation(conversationText, {
98 | extractTopics: true,
99 | extractEntities: true,
100 | detectIntent: true,
101 | detectCodeContext: true,
102 | minTopicConfidence: 0.3
103 | });
104 |
105 | // Detect significant changes
106 | const changes = detectTopicChanges(this.lastAnalysis, currentAnalysis);
107 |
108 | if (!changes.hasTopicShift || changes.significanceScore < this.options.updateThreshold) {
109 | console.log(`[Dynamic Context] No significant changes detected (score: ${changes.significanceScore.toFixed(2)})`);
110 | this.lastAnalysis = currentAnalysis;
111 | return { processed: false, reason: 'insufficient_change', significanceScore: changes.significanceScore };
112 | }
113 |
114 | console.log(`[Dynamic Context] Significant conversation change detected (score: ${changes.significanceScore.toFixed(2)})`);
115 | console.log(`[Dynamic Context] New topics: ${changes.newTopics.map(t => t.name).join(', ')}`);
116 |
117 | // Generate memory queries based on conversation changes
118 | const queries = this.generateMemoryQueries(currentAnalysis, changes);
119 |
120 | if (queries.length === 0) {
121 | this.lastAnalysis = currentAnalysis;
122 | return { processed: false, reason: 'no_actionable_queries' };
123 | }
124 |
125 | // Retrieve memories from memory service
126 | const memories = await this.retrieveRelevantMemories(queries, memoryServiceConfig);
127 |
128 | if (memories.length === 0) {
129 | this.lastAnalysis = currentAnalysis;
130 | return { processed: false, reason: 'no_relevant_memories' };
131 | }
132 |
133 | // Score memories with conversation context
134 | const scoredMemories = this.scoreMemoriesWithContext(memories, currentAnalysis);
135 |
136 | // Select top memories for injection
137 | const selectedMemories = scoredMemories
138 | .filter(memory => memory.relevanceScore > 0.3)
139 | .slice(0, this.options.maxMemoriesPerUpdate);
140 |
141 | if (selectedMemories.length === 0) {
142 | this.lastAnalysis = currentAnalysis;
143 | return { processed: false, reason: 'no_high_relevance_memories' };
144 | }
145 |
146 | // Track loaded memories to avoid duplicates
147 | selectedMemories.forEach(memory => {
148 | this.loadedMemoryHashes.add(memory.content_hash);
149 | });
150 |
151 | // Include cross-session context if enabled
152 | let crossSessionContext = null;
153 | if (this.options.enableCrossSessionContext && this.sessionTracker) {
154 | crossSessionContext = await this.sessionTracker.getConversationContext(
155 | this.sessionContext.projectContext,
156 | { maxPreviousSessions: 2, maxDaysBack: 3 }
157 | );
158 | }
159 |
160 | // Format context update
161 | const contextUpdate = this.formatContextUpdate(
162 | selectedMemories,
163 | currentAnalysis,
164 | changes,
165 | crossSessionContext
166 | );
167 |
168 | // Inject context into conversation
169 | if (contextInjector && typeof contextInjector === 'function') {
170 | await contextInjector(contextUpdate);
171 | }
172 |
173 | // Update state
174 | this.lastAnalysis = currentAnalysis;
175 | this.lastUpdateTime = Date.now();
176 | this.updateCount++;
177 |
178 | console.log(`[Dynamic Context] Context update completed (update #${this.updateCount})`);
179 | console.log(`[Dynamic Context] Injected ${selectedMemories.length} memories`);
180 |
181 | return {
182 | processed: true,
183 | updateCount: this.updateCount,
184 | memoriesInjected: selectedMemories.length,
185 | significanceScore: changes.significanceScore,
186 | topics: changes.newTopics.map(t => t.name),
187 | hasConversationContext: true,
188 | hasCrossSessionContext: !!crossSessionContext
189 | };
190 | }
191 |
192 | /**
193 | * Check if we should process an update based on rate limiting
194 | */
195 | shouldProcessUpdate() {
196 | const now = Date.now();
197 |
198 | // Check cooldown period
199 | if (now - this.lastUpdateTime < this.options.updateCooldownMs) {
200 | return false;
201 | }
202 |
203 | // Check maximum updates per session
204 | if (this.updateCount >= this.options.maxUpdatesPerSession) {
205 | return false;
206 | }
207 |
208 | return true;
209 | }
210 |
211 | /**
212 | * Generate memory queries from conversation analysis
213 | */
214 | generateMemoryQueries(analysis, changes) {
215 | const queries = [];
216 |
217 | // Query for new topics
218 | changes.newTopics.forEach(topic => {
219 | if (topic.confidence > 0.4) {
220 | queries.push({
221 | query: topic.name,
222 | type: 'topic',
223 | weight: topic.confidence,
224 | limit: 2
225 | });
226 | }
227 | });
228 |
229 | // Query for changed intent
230 | if (changes.changedIntents && analysis.intent && analysis.intent.confidence > 0.5) {
231 | queries.push({
232 | query: `${analysis.intent.name} ${this.sessionContext.projectContext?.name || ''}`,
233 | type: 'intent',
234 | weight: analysis.intent.confidence,
235 | limit: 1
236 | });
237 | }
238 |
239 | // Query for high-confidence entities
240 | analysis.entities
241 | .filter(entity => entity.confidence > 0.7)
242 | .slice(0, 2)
243 | .forEach(entity => {
244 | queries.push({
245 | query: `${entity.name} ${entity.type}`,
246 | type: 'entity',
247 | weight: entity.confidence,
248 | limit: 1
249 | });
250 | });
251 |
252 | // Sort by weight and limit total queries
253 | return queries
254 | .sort((a, b) => b.weight - a.weight)
255 | .slice(0, 4); // Maximum 4 queries per update
256 | }
257 |
258 | /**
259 | * Retrieve memories from memory service for multiple queries
260 | */
261 | async retrieveRelevantMemories(queries, memoryServiceConfig) {
262 | const allMemories = [];
263 |
264 | // Import the query function from topic-change hook
265 | const { queryMemoryService } = require('../core/topic-change');
266 |
267 | for (const queryObj of queries) {
268 | try {
269 | const memories = await this.queryMemoryService(
270 | memoryServiceConfig.endpoint,
271 | memoryServiceConfig.apiKey,
272 | queryObj.query,
273 | {
274 | limit: queryObj.limit,
275 | excludeHashes: Array.from(this.loadedMemoryHashes)
276 | }
277 | );
278 |
279 | // Add query context to memories
280 | memories.forEach(memory => {
281 | memory.queryContext = queryObj;
282 | });
283 |
284 | allMemories.push(...memories);
285 |
286 | } catch (error) {
287 | console.error(`[Dynamic Context] Failed to query memories for "${queryObj.query}":`, error.message);
288 | }
289 | }
290 |
291 | return allMemories;
292 | }
293 |
294 | /**
295 | * Simplified memory service query (extracted from topic-change.js pattern)
296 | */
297 | async queryMemoryService(endpoint, apiKey, query, options = {}) {
298 | const https = require('https');
299 |
300 | return new Promise((resolve, reject) => {
301 | const { limit = 3, excludeHashes = [] } = options;
302 |
303 | const postData = JSON.stringify({
304 | jsonrpc: '2.0',
305 | id: Date.now(),
306 | method: 'tools/call',
307 | params: {
308 | name: 'retrieve_memory',
309 | arguments: { query: query, limit: limit }
310 | }
311 | });
312 |
313 | const url = new URL('/mcp', endpoint);
314 | const requestOptions = {
315 | hostname: url.hostname,
316 | port: url.port,
317 | path: url.pathname,
318 | method: 'POST',
319 | headers: {
320 | 'Content-Type': 'application/json',
321 | 'Authorization': `Bearer ${apiKey}`,
322 | 'Content-Length': Buffer.byteLength(postData)
323 | },
324 | rejectUnauthorized: false,
325 | timeout: 5000
326 | };
327 |
328 | const req = https.request(requestOptions, (res) => {
329 | let data = '';
330 | res.on('data', (chunk) => { data += chunk; });
331 | res.on('end', () => {
332 | try {
333 | const response = JSON.parse(data);
334 | if (response.error) {
335 | console.error('[Dynamic Context] Memory service error:', response.error);
336 | resolve([]);
337 | return;
338 | }
339 |
340 | const memories = this.parseMemoryResults(response.result);
341 | const filteredMemories = memories.filter(memory =>
342 | !excludeHashes.includes(memory.content_hash)
343 | );
344 |
345 | resolve(filteredMemories);
346 | } catch (parseError) {
347 | console.error('[Dynamic Context] Failed to parse memory response:', parseError.message);
348 | resolve([]);
349 | }
350 | });
351 | });
352 |
353 | req.on('error', (error) => {
354 | console.error('[Dynamic Context] Memory service request failed:', error.message);
355 | resolve([]);
356 | });
357 |
358 | req.on('timeout', () => {
359 | req.destroy();
360 | resolve([]);
361 | });
362 |
363 | req.write(postData);
364 | req.end();
365 | });
366 | }
367 |
368 | /**
369 | * Parse memory results from MCP response
370 | */
371 | parseMemoryResults(result) {
372 | try {
373 | if (result && result.content && result.content[0] && result.content[0].text) {
374 | const text = result.content[0].text;
375 | const resultsMatch = text.match(/'results':\s*(\[[\s\S]*?\])/);
376 | if (resultsMatch) {
377 | return eval(resultsMatch[1]) || [];
378 | }
379 | }
380 | return [];
381 | } catch (error) {
382 | console.error('[Dynamic Context] Error parsing memory results:', error.message);
383 | return [];
384 | }
385 | }
386 |
387 | /**
388 | * Score memories with enhanced conversation context
389 | */
390 | scoreMemoriesWithContext(memories, conversationAnalysis) {
391 | return scoreMemoryRelevance(memories, this.sessionContext.projectContext || {}, {
392 | includeConversationContext: true,
393 | conversationAnalysis: conversationAnalysis,
394 | weights: {
395 | timeDecay: 0.2,
396 | tagRelevance: 0.3,
397 | contentRelevance: 0.15,
398 | conversationRelevance: 0.35 // High weight for conversation context
399 | }
400 | });
401 | }
402 |
403 | /**
404 | * Format the context update message
405 | */
406 | formatContextUpdate(memories, analysis, changes, crossSessionContext) {
407 | let updateMessage = '\n🧠 **Dynamic Context Update**\n\n';
408 |
409 | // Explain the trigger
410 | if (changes.newTopics.length > 0) {
411 | updateMessage += `**New topics detected**: ${changes.newTopics.map(t => t.name).join(', ')}\n`;
412 | }
413 | if (changes.changedIntents && analysis.intent) {
414 | updateMessage += `**Focus shifted to**: ${analysis.intent.name}\n`;
415 | }
416 | updateMessage += '\n';
417 |
418 | // Add cross-session context if available
419 | if (crossSessionContext && crossSessionContext.recentSessions.length > 0) {
420 | updateMessage += '**Recent session context**:\n';
421 | crossSessionContext.recentSessions.slice(0, 2).forEach(session => {
422 | const timeAgo = this.formatTimeAgo(session.endTime);
423 | updateMessage += `• ${session.outcome?.type || 'Session'} completed ${timeAgo}\n`;
424 | });
425 | updateMessage += '\n';
426 | }
427 |
428 | // Add relevant memories
429 | updateMessage += '**Relevant context**:\n';
430 | memories.slice(0, 3).forEach((memory, index) => {
431 | const content = memory.content.length > 100 ?
432 | memory.content.substring(0, 100) + '...' :
433 | memory.content;
434 |
435 | const relevanceIndicator = memory.relevanceScore > 0.7 ? '🔥' :
436 | memory.relevanceScore > 0.5 ? '⭐' : '💡';
437 |
438 | updateMessage += `${relevanceIndicator} ${content}\n`;
439 |
440 | if (memory.tags && memory.tags.length > 0) {
441 | updateMessage += ` *${memory.tags.slice(0, 3).join(', ')}*\n`;
442 | }
443 | updateMessage += '\n';
444 | });
445 |
446 | updateMessage += '---\n';
447 | return updateMessage;
448 | }
449 |
450 | /**
451 | * Format time ago for human readability
452 | */
453 | formatTimeAgo(timestamp) {
454 | const now = new Date();
455 | const time = new Date(timestamp);
456 | const diffMs = now - time;
457 | const diffMins = Math.floor(diffMs / 60000);
458 | const diffHours = Math.floor(diffMs / 3600000);
459 | const diffDays = Math.floor(diffMs / 86400000);
460 |
461 | if (diffMins < 60) return `${diffMins} minutes ago`;
462 | if (diffHours < 24) return `${diffHours} hours ago`;
463 | if (diffDays < 7) return `${diffDays} days ago`;
464 | return time.toLocaleDateString();
465 | }
466 |
467 | /**
468 | * Get statistics about dynamic context updates
469 | */
470 | getStats() {
471 | return {
472 | updateCount: this.updateCount,
473 | loadedMemoriesCount: this.loadedMemoryHashes.size,
474 | lastUpdateTime: this.lastUpdateTime,
475 | hasSessionTracker: !!this.sessionTracker,
476 | isInitialized: !!this.sessionContext
477 | };
478 | }
479 |
480 | /**
481 | * Reset the updater state for a new conversation
482 | */
483 | reset() {
484 | console.log('[Dynamic Context] Resetting dynamic context updater');
485 |
486 | this.lastUpdateTime = 0;
487 | this.updateCount = 0;
488 | this.conversationBuffer = '';
489 | this.lastAnalysis = null;
490 | this.loadedMemoryHashes.clear();
491 |
492 | if (this.debounceTimer) {
493 | clearTimeout(this.debounceTimer);
494 | this.debounceTimer = null;
495 | }
496 | }
497 | }
498 |
499 | module.exports = {
500 | DynamicContextUpdater
501 | };
```
--------------------------------------------------------------------------------
/src/mcp_memory_service/storage/http_client.py:
--------------------------------------------------------------------------------
```python
1 | # Copyright 2024 Heinrich Krupp
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """
16 | HTTP client storage adapter for MCP Memory Service.
17 | Implements the MemoryStorage interface by forwarding requests to a remote HTTP server.
18 | """
19 |
20 | import aiohttp
21 | import asyncio
22 | import json
23 | import logging
24 | from typing import List, Dict, Any, Tuple, Optional
25 | from datetime import datetime, timezone
26 |
27 | from .base import MemoryStorage
28 | from ..models.memory import Memory, MemoryQueryResult
29 | from ..config import HTTP_HOST, HTTP_PORT
30 |
31 | logger = logging.getLogger(__name__)
32 |
33 |
34 | class HTTPClientStorage(MemoryStorage):
35 | """
36 | HTTP client storage implementation.
37 |
38 | This adapter forwards all storage operations to a remote MCP Memory Service
39 | HTTP server, enabling multiple clients to coordinate through a shared server.
40 | """
41 |
42 | def __init__(self, base_url: Optional[str] = None, timeout: float = 30.0):
43 | """
44 | Initialize HTTP client storage.
45 |
46 | Args:
47 | base_url: Base URL of the MCP Memory Service HTTP server
48 | timeout: Request timeout in seconds
49 | """
50 | if base_url:
51 | self.base_url = base_url.rstrip('/')
52 | else:
53 | # Use default from config
54 | host = HTTP_HOST if HTTP_HOST != '0.0.0.0' else 'localhost'
55 | self.base_url = f"http://{host}:{HTTP_PORT}"
56 |
57 | self.timeout = aiohttp.ClientTimeout(total=timeout)
58 | self.session = None
59 | self._initialized = False
60 |
61 | logger.info(f"Initialized HTTP client storage for: {self.base_url}")
62 |
63 | def _handle_http_error(self, e: Exception, operation: str, return_empty_list: bool = False):
64 | """Centralized HTTP error handling with context-specific logging."""
65 | if isinstance(e, aiohttp.ClientError):
66 | error_msg = f"HTTP client connection error during {operation}: {str(e)}"
67 | elif isinstance(e, aiohttp.ServerTimeoutError):
68 | error_msg = f"HTTP server timeout during {operation}: {str(e)}"
69 | elif isinstance(e, asyncio.TimeoutError):
70 | error_msg = f"{operation.capitalize()} operation timeout: {str(e)}"
71 | elif isinstance(e, json.JSONDecodeError):
72 | error_msg = f"Invalid JSON response during {operation}: {str(e)}"
73 | else:
74 | error_msg = f"Unexpected {operation} error: {type(e).__name__}: {str(e)}"
75 |
76 | logger.error(error_msg)
77 |
78 | if return_empty_list:
79 | return []
80 | else:
81 | return False, error_msg
82 |
83 | async def initialize(self):
84 | """Initialize the HTTP client session."""
85 | try:
86 | self.session = aiohttp.ClientSession(timeout=self.timeout)
87 |
88 | # Test connection to the server
89 | health_url = f"{self.base_url}/health"
90 | async with self.session.get(health_url) as response:
91 | if response.status == 200:
92 | health_data = await response.json()
93 | logger.info(f"Connected to MCP Memory Service: {health_data.get('service', 'unknown')} v{health_data.get('version', 'unknown')}")
94 | self._initialized = True
95 | else:
96 | raise RuntimeError(f"Health check failed: HTTP {response.status}")
97 | except Exception as e:
98 | if isinstance(e, aiohttp.ClientError):
99 | error_msg = f"HTTP client connection error during initialization: {str(e)}"
100 | elif isinstance(e, aiohttp.ServerTimeoutError):
101 | error_msg = f"HTTP server timeout during initialization: {str(e)}"
102 | elif isinstance(e, asyncio.TimeoutError):
103 | error_msg = f"Initialization timeout: {str(e)}"
104 | else:
105 | error_msg = f"Unexpected error during HTTP client initialization: {type(e).__name__}: {str(e)}"
106 |
107 | logger.error(error_msg)
108 | if self.session:
109 | await self.session.close()
110 | self.session = None
111 | raise RuntimeError(error_msg)
112 |
113 | async def store(self, memory: Memory) -> Tuple[bool, str]:
114 | """Store a memory via HTTP API."""
115 | if not self._initialized or not self.session:
116 | return False, "HTTP client not initialized"
117 |
118 | try:
119 | store_url = f"{self.base_url}/api/memories"
120 | payload = {
121 | "content": memory.content,
122 | "tags": memory.tags or [],
123 | "memory_type": memory.memory_type,
124 | "metadata": memory.metadata or {}
125 | }
126 |
127 | async with self.session.post(store_url, json=payload) as response:
128 | if response.status == 201:
129 | result = await response.json()
130 | logger.info(f"Successfully stored memory via HTTP: {result.get('content_hash')}")
131 | return True, f"Memory stored successfully: {result.get('content_hash')}"
132 | else:
133 | error_data = await response.json()
134 | error_msg = error_data.get('detail', f'HTTP {response.status}')
135 | logger.error(f"Failed to store memory via HTTP: {error_msg}")
136 | return False, error_msg
137 |
138 | except Exception as e:
139 | return self._handle_http_error(e, "store")
140 |
141 | async def retrieve(self, query: str, n_results: int = 5) -> List[MemoryQueryResult]:
142 | """Retrieve memories using semantic search via HTTP API."""
143 | if not self._initialized or not self.session:
144 | logger.error("HTTP client not initialized")
145 | return []
146 |
147 | try:
148 | search_url = f"{self.base_url}/api/search/semantic"
149 | payload = {
150 | "query": query,
151 | "n_results": n_results
152 | }
153 |
154 | async with self.session.post(search_url, json=payload) as response:
155 | if response.status == 200:
156 | data = await response.json()
157 | results = []
158 |
159 | for item in data.get("results", []):
160 | memory_data = item.get("memory", {})
161 | memory = Memory(
162 | content=memory_data.get("content", ""),
163 | content_hash=memory_data.get("content_hash", ""),
164 | tags=memory_data.get("tags", []),
165 | memory_type=memory_data.get("memory_type"),
166 | metadata=memory_data.get("metadata", {}),
167 | created_at=memory_data.get("created_at"),
168 | updated_at=memory_data.get("updated_at"),
169 | created_at_iso=memory_data.get("created_at_iso"),
170 | updated_at_iso=memory_data.get("updated_at_iso")
171 | )
172 |
173 | result = MemoryQueryResult(
174 | memory=memory,
175 | relevance_score=item.get("similarity_score"),
176 | debug_info={"backend": "http_client", "server": self.base_url}
177 | )
178 | results.append(result)
179 |
180 | logger.info(f"Retrieved {len(results)} memories via HTTP for query: {query}")
181 | return results
182 | else:
183 | logger.error(f"HTTP retrieve error: {response.status}")
184 | return []
185 |
186 | except Exception as e:
187 | return self._handle_http_error(e, "retrieve", return_empty_list=True)
188 |
189 | async def search_by_tag(self, tags: List[str], time_start: Optional[float] = None) -> List[Memory]:
190 | """Search memories by tags via HTTP API with optional time filtering."""
191 | return await self._execute_tag_search(
192 | tags=tags,
193 | match_all=False,
194 | time_start=time_start,
195 | time_end=None
196 | )
197 |
198 | async def search_by_tags(
199 | self,
200 | tags: List[str],
201 | operation: str = "AND",
202 | time_start: Optional[float] = None,
203 | time_end: Optional[float] = None
204 | ) -> List[Memory]:
205 | """Search memories by tags with AND/OR semantics via HTTP API."""
206 | normalized_operation = operation.strip().upper() if isinstance(operation, str) else "AND"
207 | if normalized_operation not in {"AND", "OR"}:
208 | logger.warning("Unsupported tag operation %s; defaulting to AND", operation)
209 | normalized_operation = "AND"
210 |
211 | match_all = normalized_operation == "AND"
212 | return await self._execute_tag_search(
213 | tags=tags,
214 | match_all=match_all,
215 | time_start=time_start,
216 | time_end=time_end
217 | )
218 |
219 | async def _execute_tag_search(
220 | self,
221 | tags: List[str],
222 | match_all: bool,
223 | time_start: Optional[float],
224 | time_end: Optional[float]
225 | ) -> List[Memory]:
226 | """Internal helper to execute HTTP tag-based searches."""
227 | if not self._initialized or not self.session:
228 | logger.error("HTTP client not initialized")
229 | return []
230 |
231 | try:
232 | search_url = f"{self.base_url}/api/search/by-tag"
233 | payload: Dict[str, Any] = {
234 | "tags": tags,
235 | "match_all": match_all
236 | }
237 |
238 | time_filter = self._build_time_filter(time_start, time_end)
239 | if time_filter:
240 | payload["time_filter"] = time_filter
241 |
242 | async with self.session.post(search_url, json=payload) as response:
243 | if response.status == 200:
244 | data = await response.json()
245 | results: List[Memory] = []
246 |
247 | for result_item in data.get("results", []):
248 | memory_data = result_item.get("memory", {})
249 | memory = Memory(
250 | content=memory_data.get("content", ""),
251 | content_hash=memory_data.get("content_hash", ""),
252 | tags=memory_data.get("tags", []),
253 | memory_type=memory_data.get("memory_type"),
254 | metadata=memory_data.get("metadata", {}),
255 | created_at=memory_data.get("created_at"),
256 | updated_at=memory_data.get("updated_at"),
257 | created_at_iso=memory_data.get("created_at_iso"),
258 | updated_at_iso=memory_data.get("updated_at_iso")
259 | )
260 | results.append(memory)
261 |
262 | logger.info(
263 | "Found %d memories via HTTP with tags %s (match_all=%s)",
264 | len(results),
265 | tags,
266 | match_all
267 | )
268 | return results
269 |
270 | logger.error(f"HTTP tag search error: {response.status}")
271 | return []
272 |
273 | except Exception as e:
274 | return self._handle_http_error(e, "tag search", return_empty_list=True)
275 |
276 | @staticmethod
277 | def _build_time_filter(time_start: Optional[float], time_end: Optional[float]) -> Optional[str]:
278 | """Convert timestamps to the natural language format expected by the HTTP API."""
279 | if time_start is None and time_end is None:
280 | return None
281 |
282 | def _to_date(ts: float) -> str:
283 | return datetime.fromtimestamp(ts, tz=timezone.utc).date().isoformat()
284 |
285 | if time_start is not None and time_end is not None:
286 | return f"between {_to_date(time_start)} and {_to_date(time_end)}"
287 | if time_start is not None:
288 | return _to_date(time_start)
289 | return _to_date(time_end)
290 |
291 | async def delete(self, content_hash: str) -> Tuple[bool, str]:
292 | """Delete a memory by content hash via HTTP API."""
293 | if not self._initialized or not self.session:
294 | return False, "HTTP client not initialized"
295 |
296 | try:
297 | delete_url = f"{self.base_url}/api/memories/{content_hash}"
298 |
299 | async with self.session.delete(delete_url) as response:
300 | if response.status == 200:
301 | result = await response.json()
302 | logger.info(f"Successfully deleted memory via HTTP: {content_hash}")
303 | return True, result.get("message", "Memory deleted successfully")
304 | elif response.status == 404:
305 | return False, f"Memory with hash {content_hash} not found"
306 | else:
307 | error_data = await response.json()
308 | error_msg = error_data.get('detail', f'HTTP {response.status}')
309 | logger.error(f"Failed to delete memory via HTTP: {error_msg}")
310 | return False, error_msg
311 |
312 | except Exception as e:
313 | return self._handle_http_error(e, "delete")
314 |
315 | async def delete_by_tag(self, tag: str) -> Tuple[int, str]:
316 | """Delete memories by tag (not implemented via HTTP - would be dangerous)."""
317 | logger.warning("Bulk delete by tag not supported via HTTP client for safety")
318 | return 0, "Bulk delete by tag not supported via HTTP client for safety reasons"
319 |
320 | async def cleanup_duplicates(self) -> Tuple[int, str]:
321 | """Cleanup duplicates (not implemented via HTTP - server-side operation)."""
322 | logger.warning("Cleanup duplicates not supported via HTTP client")
323 | return 0, "Cleanup duplicates should be performed on the server side"
324 |
325 | async def update_memory_metadata(self, content_hash: str, updates: Dict[str, Any], preserve_timestamps: bool = True) -> Tuple[bool, str]:
326 | """Update memory metadata (not implemented - would need PUT endpoint)."""
327 | logger.warning("Update memory metadata not supported via HTTP client yet")
328 | return False, "Update memory metadata not supported via HTTP client yet"
329 |
330 | async def recall(self, query: Optional[str] = None, n_results: int = 5, start_timestamp: Optional[float] = None, end_timestamp: Optional[float] = None) -> List[MemoryQueryResult]:
331 | """
332 | Retrieve memories with time filtering and optional semantic search via HTTP API.
333 | """
334 | if not self._initialized or not self.session:
335 | logger.error("HTTP client not initialized")
336 | return []
337 |
338 | try:
339 | recall_url = f"{self.base_url}/api/search/time"
340 | payload = {
341 | "query": query or f"memories from {datetime.fromtimestamp(start_timestamp).isoformat() if start_timestamp else 'beginning'} to {datetime.fromtimestamp(end_timestamp).isoformat() if end_timestamp else 'now'}",
342 | "n_results": n_results
343 | }
344 |
345 | async with self.session.post(recall_url, json=payload) as response:
346 | if response.status == 200:
347 | data = await response.json()
348 | results = []
349 |
350 | for item in data.get("results", []):
351 | memory_data = item.get("memory", {})
352 | memory = Memory(
353 | content=memory_data.get("content", ""),
354 | content_hash=memory_data.get("content_hash", ""),
355 | tags=memory_data.get("tags", []),
356 | memory_type=memory_data.get("memory_type"),
357 | metadata=memory_data.get("metadata", {}),
358 | created_at=memory_data.get("created_at"),
359 | updated_at=memory_data.get("updated_at"),
360 | created_at_iso=memory_data.get("created_at_iso"),
361 | updated_at_iso=memory_data.get("updated_at_iso")
362 | )
363 |
364 | result = MemoryQueryResult(
365 | memory=memory,
366 | relevance_score=item.get("similarity_score"),
367 | debug_info={"backend": "http_client", "server": self.base_url, "time_filtered": True}
368 | )
369 | results.append(result)
370 |
371 | logger.info(f"Retrieved {len(results)} memories via HTTP recall")
372 | return results
373 | else:
374 | logger.error(f"HTTP recall error: {response.status}")
375 | return []
376 |
377 | except Exception as e:
378 | return self._handle_http_error(e, "recall", return_empty_list=True)
379 |
380 | def get_stats(self) -> Dict[str, Any]:
381 | """Get storage statistics (placeholder - could call stats endpoint)."""
382 | return {
383 | "backend": "http_client",
384 | "server": self.base_url,
385 | "initialized": self._initialized,
386 | "note": "Statistics from remote server not implemented yet"
387 | }
388 |
389 | async def close(self):
390 | """Close the HTTP client session."""
391 | if self.session:
392 | await self.session.close()
393 | self.session = None
394 | self._initialized = False
395 | logger.info("HTTP client storage connection closed")
396 |
397 | async def update_memory(self, memory: Memory) -> bool:
398 | """Update an existing memory (not implemented via HTTP client yet)."""
399 | logger.warning("Update memory not supported via HTTP client yet")
400 | return False
401 |
```