#
tokens: 48182/50000 8/625 files (page 28/47)
lines: on (toggle) GitHub
raw markdown copy reset
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 | 
```
Page 28/47FirstPrevNextLast