#
tokens: 45917/50000 9/625 files (page 22/35)
lines: off (toggle) GitHub
raw markdown copy
This is page 22 of 35. Use http://codebase.md/doobidoo/mcp-memory-service?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

--------------------------------------------------------------------------------
/docs/pr-graphql-integration.md:
--------------------------------------------------------------------------------

```markdown
# PR Review Thread Management with GraphQL

**Status:** ✅ Implemented in v8.20.0
**Motivation:** Eliminate manual "mark as resolved" clicks, reduce PR review friction
**Key Benefit:** Automatic thread resolution when code is fixed

---

## Table of Contents

1. [Overview](#overview)
2. [Why GraphQL?](#why-graphql)
3. [Components](#components)
4. [Usage Guide](#usage-guide)
5. [Integration with Automation](#integration-with-automation)
6. [Troubleshooting](#troubleshooting)
7. [API Reference](#api-reference)

---

## Overview

This system provides **automated PR review thread management** using GitHub's GraphQL API. It eliminates the manual work of resolving review threads by:

1. **Detecting** which code changes address review comments
2. **Automatically resolving** threads for fixed code
3. **Adding explanatory comments** with commit references
4. **Tracking thread status** across review iterations

### Problem Solved

**Before:**
```bash
# Manual workflow (time-consuming, error-prone)
1. Gemini reviews code → creates 30 inline comments
2. You fix all issues → push commit
3. Manually click "Resolve" 30 times on GitHub web UI
4. Trigger new review: gh pr comment $PR --body "/gemini review"
5. Repeat...
```

**After:**
```bash
# Automated workflow (zero manual clicks)
1. Gemini reviews code → creates 30 inline comments
2. You fix all issues → push commit
3. Auto-resolve: bash scripts/pr/resolve_threads.sh $PR HEAD --auto
4. Trigger new review: gh pr comment $PR --body "/gemini review"
5. Repeat...
```

Even better with `auto_review.sh` - it auto-resolves threads after each fix iteration!

---

## Why GraphQL?

### GitHub API Limitation

**Critical discovery:** GitHub's REST API **cannot** resolve PR review threads.

```bash
# ❌ REST API - No thread resolution endpoint
gh api repos/OWNER/REPO/pulls/PR_NUMBER/comments
# Can list comments, but cannot resolve threads

# ✅ GraphQL API - Full thread management
gh api graphql -f query='mutation { resolveReviewThread(...) }'
# Can query threads, resolve them, add replies
```

### GraphQL Advantages

| Feature | REST API | GraphQL API |
|---------|----------|-------------|
| **List review comments** | ✅ Yes | ✅ Yes |
| **Get thread status** | ❌ No | ✅ Yes (`isResolved`, `isOutdated`) |
| **Resolve threads** | ❌ No | ✅ Yes (`resolveReviewThread` mutation) |
| **Add thread replies** | ❌ Limited | ✅ Yes (`addPullRequestReviewThreadReply`) |
| **Thread metadata** | ❌ No | ✅ Yes (line, path, diffSide) |

---

## Components

### 1. GraphQL Helpers Library

**File:** `scripts/pr/lib/graphql_helpers.sh`
**Purpose:** Reusable GraphQL operations for PR review threads

**Key Functions:**

```bash
# Get all review threads for a PR
get_review_threads <PR_NUMBER>

# Resolve a thread (with optional comment)
resolve_review_thread <THREAD_ID> [COMMENT]

# Add a reply to a thread
add_thread_reply <THREAD_ID> <COMMENT>

# Check if a line was modified in a commit
was_line_modified <FILE_PATH> <LINE_NUMBER> <COMMIT_SHA>

# Get thread statistics
get_thread_stats <PR_NUMBER>
count_unresolved_threads <PR_NUMBER>

# Verify gh CLI supports GraphQL
check_graphql_support
```

**GraphQL Queries Used:**

1. **Query review threads:**
   ```graphql
   query($pr: Int!, $owner: String!, $repo: String!) {
     repository(owner: $owner, name: $repo) {
       pullRequest(number: $pr) {
         reviewThreads(first: 100) {
           nodes {
             id
             isResolved
             isOutdated
             path
             line
             comments(first: 10) {
               nodes {
                 id
                 author { login }
                 body
                 createdAt
               }
             }
           }
         }
       }
     }
   }
   ```

2. **Resolve a thread:**
   ```graphql
   mutation($threadId: ID!) {
     resolveReviewThread(input: {threadId: $threadId}) {
       thread {
         id
         isResolved
       }
     }
   }
   ```

3. **Add thread reply:**
   ```graphql
   mutation($threadId: ID!, $body: String!) {
     addPullRequestReviewThreadReply(input: {
       pullRequestReviewThreadId: $threadId
       body: $body
     }) {
       comment { id }
     }
   }
   ```

### 2. Smart Thread Resolution Tool

**File:** `scripts/pr/resolve_threads.sh`
**Purpose:** Automatically resolve threads when code is fixed

**Usage:**

```bash
# Interactive mode (prompts for each thread)
bash scripts/pr/resolve_threads.sh <PR_NUMBER> [COMMIT_SHA]

# Automatic mode (no prompts)
bash scripts/pr/resolve_threads.sh <PR_NUMBER> HEAD --auto

# Example
bash scripts/pr/resolve_threads.sh 212 HEAD --auto
```

**Decision Logic:**

```bash
For each unresolved thread:
  1. Is the file modified in this commit?
     → Yes: Check if the specific line was changed
        → Yes: Resolve with "Line X modified in commit ABC"
        → No: Skip
     → No: Check if thread is marked "outdated" by GitHub
        → Yes: Resolve with "Thread outdated by subsequent commits"
        → No: Skip
```

**Resolution Comment Format:**

```markdown
✅ Resolved: Line 123 in file.py was modified in commit abc1234

Verified by automated thread resolution script.
```

### 3. Thread Status Display

**File:** `scripts/pr/thread_status.sh`
**Purpose:** Display comprehensive thread status with filtering

**Usage:**

```bash
# Show all threads with summary
bash scripts/pr/thread_status.sh <PR_NUMBER>

# Show only unresolved threads
bash scripts/pr/thread_status.sh <PR_NUMBER> --unresolved

# Show only resolved threads
bash scripts/pr/thread_status.sh <PR_NUMBER> --resolved

# Show only outdated threads
bash scripts/pr/thread_status.sh <PR_NUMBER> --outdated

# Example
bash scripts/pr/thread_status.sh 212 --unresolved
```

**Output Format:**

```
========================================
  PR Review Thread Status
========================================
PR Number: #212
Filter: unresolved

========================================
  Summary
========================================
Total Threads:      45
Resolved:           39
Unresolved:         6
Outdated:           12

========================================
  Thread Details
========================================

○ Thread #1
  Status: UNRESOLVED | OUTDATED
  File: scripts/pr/auto_review.sh:89
  Side: RIGHT
  Author: gemini-code-assist[bot]
  Created: 2025-11-08T12:30:45Z
  Comments: 1
  "Variable $review_comments is undefined. Define it before use..."
  Thread ID: MDEyOlB1bGxSZXF1ZXN...

...
```

### 4. Integration with Auto-Review

**File:** `scripts/pr/auto_review.sh` (enhanced)
**Added functionality:**

1. **Startup:** Check GraphQL availability
   ```bash
   GraphQL Thread Resolution: Enabled
   ```

2. **Per-iteration:** Display thread stats
   ```bash
   Review Threads: 45 total, 30 resolved, 15 unresolved
   ```

3. **After pushing fixes:** Auto-resolve threads
   ```bash
   Resolving review threads for fixed code...
   ✅ Review threads auto-resolved (8 threads)
   ```

### 5. Integration with Watch Mode

**File:** `scripts/pr/watch_reviews.sh` (enhanced)
**Added functionality:**

1. **Startup:** Check GraphQL availability
   ```bash
   GraphQL Thread Tracking: Enabled
   ```

2. **Per-check:** Display thread stats
   ```bash
   Review Threads: 45 total, 30 resolved, 15 unresolved
   ```

3. **On new review:** Show unresolved thread details
   ```bash
   Thread Status:
   [Displays thread_status.sh --unresolved output]

   Options:
     1. View detailed thread status:
        bash scripts/pr/thread_status.sh 212
     ...
   ```

---

## Usage Guide

### Basic Workflow

**1. Check thread status:**

```bash
bash scripts/pr/thread_status.sh 212
```

**2. Fix issues and push:**

```bash
# Fix code based on review comments
git add .
git commit -m "fix: address review feedback"
git push
```

**3. Resolve threads for fixed code:**

```bash
# Automatic resolution
bash scripts/pr/resolve_threads.sh 212 HEAD --auto

# Interactive resolution (with prompts)
bash scripts/pr/resolve_threads.sh 212 HEAD
```

**4. Trigger new review:**

```bash
gh pr comment 212 --body "/gemini review"
```

### Integrated Workflow (Recommended)

**Use auto_review.sh - it handles everything:**

```bash
bash scripts/pr/auto_review.sh 212 5 true
```

This will:
- Fetch review feedback
- Categorize issues
- Generate fixes
- Apply and push fixes
- **Auto-resolve threads** ← New!
- Wait for next review
- Repeat

**Use watch_reviews.sh for monitoring:**

```bash
bash scripts/pr/watch_reviews.sh 212 120
```

This will:
- Check for new reviews every 120s
- **Display thread status** ← New!
- Show unresolved threads when reviews arrive
- Optionally trigger auto_review.sh

### Advanced Usage

**Manual thread resolution with custom comment:**

```bash
# Interactive mode allows custom comments
bash scripts/pr/resolve_threads.sh 212 HEAD

# When prompted:
Resolve this thread? (y/N): y
Add custom comment? (leave empty for auto): Fixed by refactoring storage backend

# Result:
✅ Fixed by refactoring storage backend
```

**Query thread info programmatically:**

```bash
# Source the helpers
source scripts/pr/lib/graphql_helpers.sh

# Get all threads as JSON
threads=$(get_review_threads 212)

# Extract specific data
echo "$threads" | jq '.data.repository.pullRequest.reviewThreads.nodes[] |
  select(.isResolved == false) |
  {file: .path, line: .line, comment: .comments.nodes[0].body}'
```

**Check specific file's threads:**

```bash
source scripts/pr/lib/graphql_helpers.sh

# Get threads for specific file
get_unresolved_threads_for_file 212 "scripts/pr/auto_review.sh"
```

---

## Integration with Automation

### Gemini PR Automator Agent

The gemini-pr-automator agent (`.claude/agents/gemini-pr-automator.md`) now includes GraphQL thread management:

**Phase 1: Initial PR Creation**
```bash
# After creating PR, start watch mode with GraphQL tracking
bash scripts/pr/watch_reviews.sh $PR_NUMBER 180 &
```

**Phase 2: Review Iteration**
```bash
# Auto-review now auto-resolves threads
bash scripts/pr/auto_review.sh $PR_NUMBER 5 true
# Includes:
# - Fix issues
# - Push commits
# - Resolve threads  ← Automatic!
# - Trigger new review
```

**Phase 3: Manual Fixes**
```bash
# After manual fixes
git push
bash scripts/pr/resolve_threads.sh $PR_NUMBER HEAD --auto
gh pr comment $PR_NUMBER --body "/gemini review"
```

### Pre-commit Integration (Future)

**Potential enhancement:** Warn about unresolved threads before allowing new commits

```bash
# In .git/hooks/pre-commit
if [ -n "$PR_BRANCH" ]; then
  unresolved=$(bash scripts/pr/thread_status.sh $PR_NUMBER --unresolved | grep "Unresolved:" | awk '{print $2}')

  if [ "$unresolved" -gt 0 ]; then
    echo "⚠️  Warning: $unresolved unresolved review threads"
    echo "Consider resolving before committing new changes"
  fi
fi
```

---

## Troubleshooting

### Issue 1: GraphQL helpers not found

**Symptom:**
```
Warning: GraphQL helpers not available, thread auto-resolution disabled
```

**Cause:** `scripts/pr/lib/graphql_helpers.sh` not found

**Fix:**
```bash
# Verify file exists
ls -la scripts/pr/lib/graphql_helpers.sh

# If missing, re-pull from main branch
git checkout main -- scripts/pr/lib/
```

### Issue 2: gh CLI doesn't support GraphQL

**Symptom:**
```
Error: GitHub CLI version X.Y.Z is too old
GraphQL support requires v2.20.0 or later
```

**Fix:**
```bash
# Update gh CLI
gh upgrade

# Or install latest from https://cli.github.com/
```

### Issue 3: Thread resolution fails

**Symptom:**
```
❌ Failed to resolve
```

**Causes and fixes:**

1. **Invalid thread ID:**
   ```bash
   # Verify thread exists
   bash scripts/pr/thread_status.sh $PR_NUMBER
   ```

2. **Network issues:**
   ```bash
   # Check GitHub connectivity
   gh auth status
   gh api graphql -f query='query { viewer { login } }'
   ```

3. **Permissions:**
   ```bash
   # Ensure you have write access to the repository
   gh repo view --json viewerPermission
   ```

### Issue 4: Threads not auto-resolving during auto_review

**Symptom:**
Auto-review runs but threads remain unresolved

**Debug steps:**

1. **Check GraphQL availability:**
   ```bash
   bash scripts/pr/auto_review.sh 212 1 true 2>&1 | grep "GraphQL"
   # Should show: GraphQL Thread Resolution: Enabled
   ```

2. **Verify thread resolution script works:**
   ```bash
   bash scripts/pr/resolve_threads.sh 212 HEAD --auto
   # Should resolve threads if any changes match
   ```

3. **Check commit SHA detection:**
   ```bash
   git rev-parse HEAD
   # Should return valid SHA
   ```

### Issue 5: "No threads needed resolution" when threads exist

**Symptom:**
```
ℹ️  No threads needed resolution
```

**Cause:** Threads reference lines that weren't modified in the commit

**Explanation:**

The tool only resolves threads for **code that was actually changed**:

```bash
# Thread on line 89 of file.py
# Your commit modified lines 100-120
# → Thread NOT resolved (line 89 unchanged)

# Thread on line 105 of file.py
# Your commit modified lines 100-120
# → Thread RESOLVED (line 105 changed)
```

**Fix:** Either:
1. Modify the code that the thread references
2. Manually resolve via GitHub web UI if thread is no longer relevant
3. Wait for thread to become "outdated" (GitHub marks it automatically after subsequent commits)

---

## API Reference

### GraphQL Helper Functions

#### `get_review_threads <PR_NUMBER>`

**Description:** Fetch all review threads for a PR

**Returns:** JSON with thread data

**Example:**
```bash
source scripts/pr/lib/graphql_helpers.sh
threads=$(get_review_threads 212)
echo "$threads" | jq '.data.repository.pullRequest.reviewThreads.nodes | length'
# Output: 45
```

#### `resolve_review_thread <THREAD_ID> [COMMENT]`

**Description:** Resolve a review thread with optional comment

**Parameters:**
- `THREAD_ID`: GraphQL node ID (e.g., `MDEyOlB1bGxSZXF1ZXN...`)
- `COMMENT`: Optional explanatory comment

**Returns:** 0 on success, 1 on failure

**Example:**
```bash
resolve_review_thread "MDEyOlB1bGxSZXF1ZXN..." "Fixed in commit abc1234"
```

#### `add_thread_reply <THREAD_ID> <COMMENT>`

**Description:** Add a reply to a thread without resolving

**Parameters:**
- `THREAD_ID`: GraphQL node ID
- `COMMENT`: Reply text (required)

**Returns:** 0 on success, 1 on failure

**Example:**
```bash
add_thread_reply "MDEyOlB1bGxSZXF1ZXN..." "Working on this now, will fix in next commit"
```

#### `was_line_modified <FILE_PATH> <LINE_NUMBER> <COMMIT_SHA>`

**Description:** Check if a specific line was modified in a commit

**Parameters:**
- `FILE_PATH`: Relative path to file
- `LINE_NUMBER`: Line number to check
- `COMMIT_SHA`: Commit to check (e.g., `HEAD`, `abc1234`)

**Returns:** 0 if modified, 1 if not

**Example:**
```bash
if was_line_modified "scripts/pr/auto_review.sh" 89 "HEAD"; then
  echo "Line 89 was modified"
fi
```

#### `get_thread_stats <PR_NUMBER>`

**Description:** Get summary statistics for PR review threads

**Returns:** JSON with counts

**Example:**
```bash
stats=$(get_thread_stats 212)
echo "$stats" | jq '.unresolved'
# Output: 6
```

#### `count_unresolved_threads <PR_NUMBER>`

**Description:** Get count of unresolved threads

**Returns:** Integer count

**Example:**
```bash
count=$(count_unresolved_threads 212)
echo "Unresolved threads: $count"
# Output: Unresolved threads: 6
```

---

## Best Practices

### 1. Use Auto-Resolution Conservatively

**Do auto-resolve when:**
- ✅ You fixed the exact code mentioned in the comment
- ✅ The commit directly addresses the review feedback
- ✅ Tests pass after the fix

**Don't auto-resolve when:**
- ❌ Unsure if the fix fully addresses the concern
- ❌ The review comment asks a question (not a fix request)
- ❌ Breaking changes involved (needs discussion)

### 2. Add Meaningful Comments

**Good resolution comments:**
```
✅ Fixed: Refactored using async/await pattern as suggested
✅ Resolved: Added type hints for all parameters
✅ Addressed: Extracted helper function to reduce complexity
```

**Bad resolution comments:**
```
❌ Done
❌ Fixed
❌ OK
```

### 3. Verify Before Auto-Resolving

```bash
# 1. Check what will be resolved
bash scripts/pr/resolve_threads.sh 212 HEAD

# Review the prompts, then run in auto mode
bash scripts/pr/resolve_threads.sh 212 HEAD --auto
```

### 4. Monitor Thread Status

```bash
# Regular check during review cycle
bash scripts/pr/thread_status.sh 212 --unresolved

# Track progress
bash scripts/pr/thread_status.sh 212
# Shows: 45 total, 39 resolved, 6 unresolved
```

---

## Performance Considerations

### API Rate Limits

GitHub GraphQL API has rate limits:
- **Authenticated:** 5,000 points per hour
- **Points per query:** ~1 point for simple queries, ~10 for complex

**Our usage:**
- `get_review_threads`: ~5 points (fetches 100 threads with comments)
- `resolve_review_thread`: ~1 point
- `get_thread_stats`: ~5 points

**Typical PR with 30 threads:**
- Initial status check: 5 points
- Resolve 30 threads: 30 points
- Final status check: 5 points
- **Total: ~40 points** (0.8% of hourly limit)

**Conclusion:** Rate limits not a concern for typical PR workflows.

### Network Latency

- GraphQL API calls: ~200-500ms each
- Auto-resolving 30 threads: ~1-2 seconds total
- Minimal impact on review cycle time

---

## Future Enhancements

### 1. Bulk Thread Operations

**Idea:** Resolve all threads for a file in one mutation

```bash
# Current: 30 API calls for 30 threads
for thread in $threads; do
  resolve_review_thread "$thread"
done

# Future: 1 API call for 30 threads
resolve_threads_batch "${thread_ids[@]}"
```

### 2. Smart Thread Filtering

**Idea:** Only show threads relevant to recent commits

```bash
bash scripts/pr/thread_status.sh 212 --since="2 hours ago"
bash scripts/pr/thread_status.sh 212 --author="gemini-code-assist[bot]"
```

### 3. Thread Diff View

**Idea:** Show what changed for each thread

```bash
bash scripts/pr/thread_diff.sh 212
# Shows:
# Thread #1: scripts/pr/auto_review.sh:89
#   Before: review_comments=$(undefined)
#   After:  review_comments=$(gh api "repos/$REPO/pulls/$PR_NUMBER/comments" | ...)
#   Status: Fixed ✅
```

### 4. Pre-Push Hook Integration

**Idea:** Warn before pushing if unresolved threads exist

```bash
# .git/hooks/pre-push
unresolved=$(count_unresolved_threads $PR_NUMBER)
if [ "$unresolved" -gt 0 ]; then
  echo "⚠️  $unresolved unresolved threads"
  read -p "Continue? (y/N): " response
fi
```

---

## Related Documentation

- **Gemini PR Automator:** `.claude/agents/gemini-pr-automator.md`
- **Code Quality Guard:** `.claude/agents/code-quality-guard.md`
- **Auto-Review Script:** `scripts/pr/auto_review.sh`
- **Watch Mode Script:** `scripts/pr/watch_reviews.sh`
- **GitHub GraphQL API:** https://docs.github.com/en/graphql

---

**Last Updated:** 2025-11-08
**Version:** 8.20.0
**Maintainer:** MCP Memory Service Team

```

--------------------------------------------------------------------------------
/.claude/agents/github-release-manager.md:
--------------------------------------------------------------------------------

```markdown
---
name: github-release-manager
description: Use this agent when you need to manage the complete GitHub release workflow, including version management, documentation updates, branch management, PR creation, issue tracking, and post-release communication. This agent should be invoked proactively in the following scenarios:\n\n<example>\nContext: User has just completed a significant feature implementation or bug fix.\nuser: "I've finished implementing the new caching system for the memory service"\nassistant: "Great work! Let me use the github-release-manager agent to handle the complete release workflow for this change."\n<commentary>\nThe agent will analyze the changes, determine appropriate version bump, update CHANGELOG.md and README.md, create a feature branch if needed, push changes, and create a PR for Gemini Code Assist review.\n</commentary>\n</example>\n\n<example>\nContext: Multiple commits have been merged to the main branch since the last release.\nuser: "What's the status of our pending changes?"\nassistant: "Let me check the release status using the github-release-manager agent."\n<commentary>\nThe agent will review commits since last release, assess if a new release is warranted, determine version bump type, and initiate the release workflow if appropriate.\n</commentary>\n</example>\n\n<example>\nContext: An issue has been marked as fixed in a recent commit.\nuser: "I've pushed the fix for issue #123"\nassistant: "Excellent! I'll use the github-release-manager agent to track this fix through the release process and handle issue closure."\n<commentary>\nThe agent will monitor the fix through PR review, release tagging, and automatically close the issue with a grateful comment once released.\n</commentary>\n</example>\n\n<example>\nContext: User mentions planning or refactoring work.\nuser: "We should refactor the storage backend architecture"\nassistant: "I'll use the github-release-manager agent to analyze open issues and create a refactoring plan."\n<commentary>\nThe agent will review related issues, assess technical debt, and create a structured plan for the refactoring work including branch strategy and milestone planning.\n</commentary>\n</example>\n\n<example>\nContext: Proactive monitoring after a work session.\nuser: "I think we're done for today"\nassistant: "Before we wrap up, let me use the github-release-manager agent to check if any release actions are needed based on today's work."\n<commentary>\nThe agent proactively reviews the session's commits, determines if version bumps or documentation updates are needed, and can initiate the release workflow automatically.\n</commentary>\n</example>
model: sonnet
color: purple
---

You are an elite GitHub Release Manager, a specialized AI agent with deep expertise in semantic versioning, release engineering, documentation management, and issue lifecycle management. Your mission is to orchestrate the complete publishing workflow for the MCP Memory Service project with precision, consistency, and professionalism.

## Core Responsibilities

You are responsible for the entire release lifecycle:

1. **Version Management**: Analyze commits and changes to determine appropriate semantic version bumps (major.minor.patch) following semver principles strictly
2. **Documentation Curation**: Update CHANGELOG.md with detailed, well-formatted entries and update README.md when features affect user-facing functionality
3. **Branch Strategy**: Decide when to create feature/fix branches vs. working directly on main/develop, following the project's git workflow
4. **Release Orchestration**: Create git tags, GitHub releases with comprehensive release notes, and ensure all artifacts are properly published
5. **PR Management**: Create pull requests with detailed descriptions and coordinate with Gemini Code Assist for automated reviews
6. **Issue Lifecycle**: Monitor issues, plan refactoring work, provide grateful closure comments with context, and maintain issue hygiene

## Decision-Making Framework

### Version Bump Determination

Analyze changes using these criteria:

- **MAJOR (x.0.0)**: Breaking API changes, removed features, incompatible architecture changes
- **MINOR (0.x.0)**: New features, significant enhancements, new capabilities (backward compatible)
- **PATCH (0.0.x)**: Bug fixes, performance improvements, documentation updates, minor tweaks

Consider the project context from CLAUDE.md:
- Storage backend changes may warrant MINOR bumps
- MCP protocol changes may warrant MAJOR bumps
- Hook system changes should be evaluated for breaking changes
- Performance improvements >20% may warrant MINOR bumps

### Branch Strategy Decision Matrix

**Create a new branch when:**
- Feature development will take multiple commits
- Changes are experimental or require review before merge
- Working on a fix for a specific issue that needs isolated testing
- Multiple developers might work on related changes
- Changes affect critical systems (storage backends, MCP protocol)

**Work directly on main/develop when:**
- Hot fixes for critical bugs
- Documentation-only updates
- Version bump commits
- Single-commit changes that are well-tested

### Documentation Update Strategy

Follow the project's Documentation Decision Matrix from CLAUDE.md:

**CHANGELOG.md** (Always update for):
- Bug fixes with issue references
- New features with usage examples
- Performance improvements with metrics
- Configuration changes with migration notes
- Breaking changes with upgrade guides

**README.md** (Update when):
- New features affect installation or setup
- Command-line interface changes
- New environment variables or configuration options
- Architecture changes affect user understanding

**CLAUDE.md** (Update when):
- New commands or workflows are introduced
- Development guidelines change
- Troubleshooting procedures are discovered

### PR Creation and Review Workflow

When creating pull requests:

1. **Title Format**: Use conventional commits format (feat:, fix:, docs:, refactor:, perf:, test:)
2. **Description Template**:
   ```markdown
   ## Changes
   - Detailed list of changes
   
   ## Motivation
   - Why these changes are needed
   
   ## Testing
   - How changes were tested
   
   ## Related Issues
   - Fixes #123, Closes #456
   
   ## Checklist
   - [ ] Version bumped in __init__.py and pyproject.toml
   - [ ] CHANGELOG.md updated
   - [ ] README.md updated (if needed)
   - [ ] Tests added/updated
   - [ ] Documentation updated
   ```

3. **Gemini Review Coordination**: After PR creation, wait for Gemini Code Assist review, address feedback iteratively (Fix → Comment → /gemini review → Wait 1min → Repeat)

### Issue Management Protocol

**Issue Tracking**:
- Monitor commits for patterns: "fixes #", "closes #", "resolves #"
- Auto-categorize issues: bug, feature, docs, performance, refactoring
- Track issue-PR relationships for post-release closure

**Refactoring Planning**:
- Review open issues tagged with "refactoring" or "technical-debt"
- Assess impact and priority based on:
  - Code complexity metrics
  - Frequency of related bugs
  - Developer pain points mentioned in issues
  - Performance implications
- Create structured refactoring plans with milestones

**Issue Closure**:
- Wait until fix is released (not just merged)
- Generate grateful, context-rich closure comments:
  ```markdown
  🎉 This issue has been resolved in v{version}!
  
  **Fix Details:**
  - PR: #{pr_number}
  - Commit: {commit_hash}
  - CHANGELOG: [View entry](link)
  
  **What Changed:**
  {brief description of the fix}
  
  Thank you for reporting this issue and helping improve the MCP Memory Service!
  ```

## Operational Workflow

### Complete Release Procedure

1. **Pre-Release Analysis**:
   - Review commits since last release
   - Identify breaking changes, new features, bug fixes
   - Determine appropriate version bump
   - Check for open issues that will be resolved

2. **Four-File Version Bump Procedure**:
   - Update `src/mcp_memory_service/__init__.py` (line 50: `__version__ = "X.Y.Z"`)
   - Update `pyproject.toml` (line 7: `version = "X.Y.Z"`)
   - Update `README.md` "Latest Release" section (documented in step 3b below)
   - Run `uv lock` to update dependency lock file
   - Commit ALL FOUR files together: `git commit -m "chore: release vX.Y.Z"`

   **CRITICAL**: All four files must be updated in single commit for version consistency

3. **Documentation Updates** (CRITICAL - Must be done in correct order):

   a. **CHANGELOG.md Validation** (FIRST - Before any edits):
      - Run: `grep -n "^## \[" CHANGELOG.md | head -10`
      - Verify no duplicate version sections
      - Confirm newest version will be at top (after [Unreleased])
      - If PR merged with incorrect CHANGELOG:
        - FIX IMMEDIATELY before proceeding
        - Create separate commit: "docs: fix CHANGELOG structure"
        - DO NOT include fixes in release commit
      - See "CHANGELOG Validation Protocol" section for full validation commands

   b. **CHANGELOG.md Content**:
      - **FIRST**: Check for `## [Unreleased]` section
      - If found, move ALL unreleased entries into the new version section
      - Add new version entry following project format: `## [x.y.z] - YYYY-MM-DD`
      - Ensure empty `## [Unreleased]` section remains at top
      - Verify all changes from commits are documented
      - **VERIFY**: New version positioned immediately after [Unreleased]
      - **VERIFY**: No duplicate content from previous versions

   c. **README.md**:
      - **ALWAYS update** the "Latest Release" section near top of file
      - Update version number: `### 🆕 Latest Release: **vX.Y.Z** (Mon DD, YYYY)`
      - Update "What's New" bullet points with CHANGELOG highlights
      - Keep list concise (4-6 key items with emojis)
      - Match tone and format of existing entries
      - **CRITICAL**: Add the PREVIOUS version to "Previous Releases" section
        - Extract one-line summary from the old "Latest Release" content
        - Insert at TOP of Previous Releases list (reverse chronological order)
        - Format: `- **vX.Y.Z** - Brief description (key metric/feature)`
        - Maintain 5-6 most recent releases, remove oldest if list gets long
        - Example: `- **v8.24.1** - Test Infrastructure Improvements (27 test failures resolved, 63% → 71% pass rate)`

   d. **CLAUDE.md**:
      - **ALWAYS update** version reference in Overview section (line ~13): `> **vX.Y.Z**: Brief description...`
      - Add version callout in Overview section if significant changes
      - Update "Essential Commands" if new scripts/commands added
      - Update "Database Maintenance" section for new maintenance utilities
      - Update any workflow documentation affected by changes

   e. **Commit**:
      - Commit message: "docs: update CHANGELOG, README, and CLAUDE.md for v{version}"

4. **Branch and PR Management**:
   - Create feature branch if needed: `git checkout -b release/v{version}`
   - Push changes: `git push origin release/v{version}`
   - Create PR with comprehensive description
   - Tag PR for Gemini Code Assist review
   - Monitor review feedback and iterate

5. **Release Creation** (CRITICAL - Follow this exact sequence):
   - **Step 1**: Merge PR to develop branch
   - **Step 2**: Merge develop into main branch
   - **Step 3**: Switch to main branch: `git checkout main`
   - **Step 4**: Pull latest: `git pull origin main`
   - **Step 5**: NOW create annotated git tag on main: `git tag -a v{version} -m "Release v{version}"`
   - **Step 6**: Push tag: `git push origin v{version}`
   - **Step 7**: Create GitHub release with:
     - Tag: v{version}
     - Title: "v{version} - {brief description}"
     - Body: CHANGELOG entry + highlights

   **WARNING**: Do NOT create the tag before merging to main. Tags must point to main branch commits, not develop branch commits. Creating the tag on develop and then merging causes tag conflicts and incorrect release points.

6. **Post-Merge Validation** (CRITICAL - Before creating tag):
   - **Validate CHANGELOG Structure**:
     - Run: `grep -n "^## \[" CHANGELOG.md | head -10`
     - Verify each version appears exactly once
     - Confirm newest version at top (after [Unreleased])
     - Check no duplicate content between versions
   - **If CHANGELOG Issues Found**:
     - Create hotfix commit: `git commit -m "docs: fix CHANGELOG structure"`
     - Push fix: `git push origin main`
     - DO NOT proceed with tag creation until CHANGELOG is correct
   - **Verify Version Consistency**:
     - Check all four files have matching version (init.py, pyproject.toml, README.md, uv.lock)
     - Confirm git history shows clean merge to main
   - **Only After Validation**: Proceed to create tag in step 5 above

7. **Post-Release Actions**:
   - Verify GitHub Actions workflows (Docker Publish, Publish and Test, HTTP-MCP Bridge)
   - Retrieve related issues using memory service
   - Close resolved issues with grateful comments
   - Update project board/milestones
   - **Update Wiki Roadmap** (if release includes major milestones):
     - **When to update**: Major versions (x.0.0), significant features, architecture changes, performance breakthroughs
     - **How to update**: Edit [13-Development-Roadmap](https://github.com/doobidoo/mcp-memory-service/wiki/13-Development-Roadmap) directly (no PR needed)
     - **What to update**:
       - Move completed items from "Current Focus" to "Completed Milestones"
       - Update "Project Status" with new version number
       - Add notable achievements to "Recent Achievements" section
       - Adjust timelines if delays or accelerations occurred
     - **Examples of roadmap-worthy changes**:
       - Major version bumps (v8.x → v9.0)
       - New storage backends or significant backend improvements
       - Memory consolidation system milestones
       - Performance improvements >20% (page load, search, sync)
       - New user-facing features (dashboard, document ingestion, etc.)
     - **Note**: Routine patches/hotfixes don't require roadmap updates

## CHANGELOG Validation Protocol (CRITICAL)

Before ANY release or documentation commit, ALWAYS validate CHANGELOG.md structure:

**Validation Commands**:
```bash
# 1. Check for duplicate version headers
grep -n "^## \[8\." CHANGELOG.md | sort
# Should show each version EXACTLY ONCE

# 2. Verify chronological order (newest first)
grep "^## \[" CHANGELOG.md | head -10
# First should be [Unreleased], second should be highest version number

# 3. Detect content duplication across versions
grep -c "Hybrid Storage Sync" CHANGELOG.md
# Count should match number of versions that include this feature
```

**Validation Rules**:
- [ ] Each version appears EXACTLY ONCE
- [ ] Newest version immediately after `## [Unreleased]`
- [ ] Versions in reverse chronological order (8.28.0 > 8.27.2 > 8.27.1...)
- [ ] No content duplicated from other versions
- [ ] New PR entries contain ONLY their own changes

**Common Mistakes to Detect** (learned from PR #228 / v8.28.0):
1. **Content Duplication**: PR copies entire previous version section
   - Example: PR #228 copied all v8.27.0 content instead of just adding Cloudflare Tag Filtering
   - Detection: grep for feature names, should not appear in multiple versions
2. **Incorrect Position**: New version positioned in middle instead of top
   - Example: v8.28.0 appeared after v8.27.1 instead of at top
   - Detection: Second line after [Unreleased] must be newest version
3. **Duplicate Sections**: Same version appears multiple times
   - Detection: `grep "^## \[X.Y.Z\]" CHANGELOG.md` should return 1 line
4. **Date Format**: Inconsistent date format
   - Must be YYYY-MM-DD

**If Issues Found**:
1. Remove duplicate sections completely
2. Move new version to correct position (immediately after [Unreleased])
3. Strip content that belongs to other versions
4. Verify chronological order with grep
5. Commit fix separately: `git commit -m "docs: fix CHANGELOG structure"`

**Post-Merge Validation** (Before creating tag):
- Run all validation commands above
- If CHANGELOG issues found, create hotfix commit before tagging
- DO NOT proceed with tag/release until CHANGELOG is structurally correct

## Quality Assurance

**Self-Verification Checklist**:
- [ ] Version follows semantic versioning strictly
- [ ] All four version files updated (init, pyproject, README, lock)
- [ ] **CHANGELOG.md**: `[Unreleased]` section collected and moved to version entry
- [ ] **CHANGELOG.md**: Entry is detailed and well-formatted
- [ ] **CHANGELOG.md**: No duplicate version sections (verified with grep)
- [ ] **CHANGELOG.md**: Versions in reverse chronological order (newest first)
- [ ] **CHANGELOG.md**: New version positioned immediately after [Unreleased]
- [ ] **CHANGELOG.md**: No content duplicated from previous versions
- [ ] **README.md**: "Latest Release" section updated with version and highlights
- [ ] **README.md**: Previous version added to "Previous Releases" list (top position)
- [ ] **CLAUDE.md**: New commands/utilities documented in appropriate sections
- [ ] **CLAUDE.md**: Version callout added if significant changes
- [ ] PR merged to develop, then develop merged to main
- [ ] Git tag created on main branch (NOT develop)
- [ ] Tag points to main merge commit (verify with `git log --oneline --graph --all --decorate`)
- [ ] Git tag pushed to remote
- [ ] GitHub release created with comprehensive notes
- [ ] All related issues identified and tracked
- [ ] PR description is complete and accurate
- [ ] Gemini review requested and feedback addressed

**Error Handling**:
- If version bump is unclear, ask for clarification with specific options
- If CHANGELOG conflicts exist, combine entries intelligently
- If PR creation fails, provide manual instructions
- If issue closure is premature, wait for release confirmation

## Communication Style

- Be proactive: Suggest release actions when appropriate
- Be precise: Provide exact version numbers and commit messages
- Be grateful: Always thank contributors when closing issues
- Be comprehensive: Include all relevant context in PRs and releases
- Be cautious: Verify breaking changes before major version bumps

## Integration with Project Context

You have access to project-specific context from CLAUDE.md. Always consider:
- Current version from `__init__.py`
- Recent changes from git history
- Open issues and their priorities
- Project conventions for commits and documentation
- Storage backend implications of changes
- MCP protocol compatibility requirements

Your goal is to make the release process seamless, consistent, and professional, ensuring that every release is well-documented, properly versioned, and thoroughly communicated to users and contributors.

```

--------------------------------------------------------------------------------
/src/mcp_memory_service/mcp_server.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
"""
FastAPI MCP Server for Memory Service

This module implements a native MCP server using the FastAPI MCP framework,
replacing the Node.js HTTP-to-MCP bridge to resolve SSL connectivity issues
and provide direct MCP protocol support.

Features:
- Native MCP protocol implementation using FastMCP
- Direct integration with existing memory storage backends
- Streamable HTTP transport for remote access
- All 22 core memory operations (excluding dashboard tools)
- SSL/HTTPS support with proper certificate handling
"""

import asyncio
import logging
import os
import socket
import sys
import time
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Any, Union, TypedDict
try:
    from typing import NotRequired  # Python 3.11+
except ImportError:
    from typing_extensions import NotRequired  # Python 3.10

# Add src to path for imports
current_dir = Path(__file__).parent
src_dir = current_dir.parent.parent
sys.path.insert(0, str(src_dir))

# FastMCP is not available in current MCP library version
# This module is kept for future compatibility
try:
    from mcp.server.fastmcp import FastMCP, Context
except ImportError:
    logger_temp = logging.getLogger(__name__)
    logger_temp.warning("FastMCP not available in mcp library - mcp_server module cannot be used")
    
    # Create dummy objects for graceful degradation
    class _DummyFastMCP:
        def tool(self):
            """Dummy decorator that does nothing."""
            def decorator(func):
                return func
            return decorator
    
    FastMCP = _DummyFastMCP  # type: ignore
    Context = None  # type: ignore

from mcp.types import TextContent

# Import existing memory service components
from .config import (
    STORAGE_BACKEND,
    CONSOLIDATION_ENABLED, EMBEDDING_MODEL_NAME, INCLUDE_HOSTNAME,
    SQLITE_VEC_PATH,
    CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_VECTORIZE_INDEX,
    CLOUDFLARE_D1_DATABASE_ID, CLOUDFLARE_R2_BUCKET, CLOUDFLARE_EMBEDDING_MODEL,
    CLOUDFLARE_LARGE_CONTENT_THRESHOLD, CLOUDFLARE_MAX_RETRIES, CLOUDFLARE_BASE_DELAY,
    HYBRID_SYNC_INTERVAL, HYBRID_BATCH_SIZE, HYBRID_MAX_QUEUE_SIZE,
    HYBRID_SYNC_ON_STARTUP, HYBRID_FALLBACK_TO_PRIMARY,
    CONTENT_PRESERVE_BOUNDARIES, CONTENT_SPLIT_OVERLAP, ENABLE_AUTO_SPLIT
)
from .storage.base import MemoryStorage
from .services.memory_service import MemoryService

# Configure logging
logging.basicConfig(level=logging.INFO)  # Default to INFO level
logger = logging.getLogger(__name__)

# =============================================================================
# GLOBAL CACHING FOR MCP SERVER PERFORMANCE OPTIMIZATION
# =============================================================================
# Module-level caches to persist storage/service instances across stateless HTTP calls.
# This reduces initialization overhead from ~1,810ms to <400ms on cache hits.
#
# Cache Keys:
# - Storage: "{backend_type}:{db_path}" (e.g., "sqlite_vec:/path/to/db")
# - MemoryService: storage instance ID (id(storage))
#
# Thread Safety:
# - Uses asyncio.Lock to prevent race conditions during concurrent access
#
# Lifecycle:
# - Cached instances persist for the lifetime of the Python process
# - NOT cleared between stateless HTTP calls (intentional for performance)
# - Cleaned up on process shutdown via lifespan context manager

_STORAGE_CACHE: Dict[str, MemoryStorage] = {}
_MEMORY_SERVICE_CACHE: Dict[int, MemoryService] = {}
_CACHE_LOCK: Optional[asyncio.Lock] = None  # Initialized on first use
_CACHE_STATS = {
    "storage_hits": 0,
    "storage_misses": 0,
    "service_hits": 0,
    "service_misses": 0,
    "total_calls": 0,
    "initialization_times": []  # Track initialization durations for cache misses
}

def _get_cache_lock() -> asyncio.Lock:
    """Get or create the global cache lock (lazy initialization to avoid event loop issues)."""
    global _CACHE_LOCK
    if _CACHE_LOCK is None:
        _CACHE_LOCK = asyncio.Lock()
    return _CACHE_LOCK

def _get_or_create_memory_service(storage: MemoryStorage) -> MemoryService:
    """
    Get cached MemoryService or create new one.

    Args:
        storage: Storage instance to use as cache key

    Returns:
        MemoryService instance (cached or newly created)
    """
    storage_id = id(storage)
    if storage_id in _MEMORY_SERVICE_CACHE:
        memory_service = _MEMORY_SERVICE_CACHE[storage_id]
        _CACHE_STATS["service_hits"] += 1
        logger.info(f"✅ MemoryService Cache HIT - Reusing service instance (storage_id: {storage_id})")
    else:
        _CACHE_STATS["service_misses"] += 1
        logger.info(f"❌ MemoryService Cache MISS - Creating new service instance...")

        # Initialize memory service with shared business logic
        memory_service = MemoryService(storage)

        # Cache the memory service instance
        _MEMORY_SERVICE_CACHE[storage_id] = memory_service
        logger.info(f"💾 Cached MemoryService instance (storage_id: {storage_id})")

    return memory_service

def _log_cache_performance(start_time: float) -> None:
    """
    Log comprehensive cache performance statistics.

    Args:
        start_time: Timer start time to calculate total elapsed time
    """
    total_time = (time.time() - start_time) * 1000
    cache_hit_rate = (
        (_CACHE_STATS["storage_hits"] + _CACHE_STATS["service_hits"]) /
        (_CACHE_STATS["total_calls"] * 2)  # 2 caches per call
    ) * 100

    logger.info(
        f"📊 Cache Stats - "
        f"Hit Rate: {cache_hit_rate:.1f}% | "
        f"Storage: {_CACHE_STATS['storage_hits']}H/{_CACHE_STATS['storage_misses']}M | "
        f"Service: {_CACHE_STATS['service_hits']}H/{_CACHE_STATS['service_misses']}M | "
        f"Total Time: {total_time:.1f}ms | "
        f"Cache Size: {len(_STORAGE_CACHE)} storage + {len(_MEMORY_SERVICE_CACHE)} services"
    )

@dataclass
class MCPServerContext:
    """Application context for the MCP server with all required components."""
    storage: MemoryStorage
    memory_service: MemoryService

@asynccontextmanager
async def mcp_server_lifespan(server: FastMCP) -> AsyncIterator[MCPServerContext]:
    """
    Manage MCP server lifecycle with global caching for performance optimization.

    Performance Impact:
    - Cache HIT: ~200-400ms (reuses existing instances)
    - Cache MISS: ~1,810ms (initializes new instances)

    Caching Strategy:
    - Storage instances cached by "{backend}:{path}" key
    - MemoryService instances cached by storage ID
    - Thread-safe with asyncio.Lock
    - Persists across stateless HTTP calls (by design)
    """
    global _STORAGE_CACHE, _MEMORY_SERVICE_CACHE, _CACHE_STATS

    # Track call statistics
    _CACHE_STATS["total_calls"] += 1
    start_time = time.time()

    logger.info(f"🔄 MCP Server Call #{_CACHE_STATS['total_calls']} - Checking global cache...")

    # Acquire lock for thread-safe cache access
    cache_lock = _get_cache_lock()
    async with cache_lock:
        # Generate cache key for storage backend
        cache_key = f"{STORAGE_BACKEND}:{SQLITE_VEC_PATH}"

        # Check storage cache
        if cache_key in _STORAGE_CACHE:
            storage = _STORAGE_CACHE[cache_key]
            _CACHE_STATS["storage_hits"] += 1
            logger.info(f"✅ Storage Cache HIT - Reusing {STORAGE_BACKEND} instance (key: {cache_key})")
        else:
            _CACHE_STATS["storage_misses"] += 1
            logger.info(f"❌ Storage Cache MISS - Initializing {STORAGE_BACKEND} instance...")

            # Initialize storage backend using shared factory
            from .storage.factory import create_storage_instance
            storage = await create_storage_instance(SQLITE_VEC_PATH, server_type="mcp")

            # Cache the storage instance
            _STORAGE_CACHE[cache_key] = storage
            init_time = (time.time() - start_time) * 1000  # Convert to ms
            _CACHE_STATS["initialization_times"].append(init_time)
            logger.info(f"💾 Cached storage instance (key: {cache_key}, init_time: {init_time:.1f}ms)")

        # Check memory service cache and log performance
        memory_service = _get_or_create_memory_service(storage)
        _log_cache_performance(start_time)

    try:
        yield MCPServerContext(
            storage=storage,
            memory_service=memory_service
        )
    finally:
        # IMPORTANT: Do NOT close cached storage instances here!
        # They are intentionally kept alive across stateless HTTP calls for performance.
        # Cleanup only happens on process shutdown (handled by FastMCP framework).
        logger.info(f"✅ MCP Server Call #{_CACHE_STATS['total_calls']} complete - Cached instances preserved")

# Create FastMCP server instance
try:
    mcp = FastMCP(
        name="MCP Memory Service", 
        host="0.0.0.0",  # Listen on all interfaces for remote access
        port=8000,       # Default port
        lifespan=mcp_server_lifespan,
        stateless_http=True  # Enable stateless HTTP for Claude Code compatibility
    )
except TypeError:
    # FastMCP not available - create dummy instance
    mcp = _DummyFastMCP()  # type: ignore

# =============================================================================
# TYPE DEFINITIONS
# =============================================================================

class StoreMemorySuccess(TypedDict):
    """Return type for successful single memory storage."""
    success: bool
    message: str
    content_hash: str

class StoreMemorySplitSuccess(TypedDict):
    """Return type for successful chunked memory storage."""
    success: bool
    message: str
    chunks_created: int
    chunk_hashes: List[str]

class StoreMemoryFailure(TypedDict):
    """Return type for failed memory storage."""
    success: bool
    message: str
    chunks_created: NotRequired[int]
    chunk_hashes: NotRequired[List[str]]

# =============================================================================
# CORE MEMORY OPERATIONS
# =============================================================================

@mcp.tool()
async def store_memory(
    content: str,
    ctx: Context,
    tags: Union[str, List[str], None] = None,
    memory_type: str = "note",
    metadata: Optional[Dict[str, Any]] = None,
    client_hostname: Optional[str] = None
) -> Union[StoreMemorySuccess, StoreMemorySplitSuccess, StoreMemoryFailure]:
    """
    Store a new memory with content and optional metadata.

    **IMPORTANT - Content Length Limits:**
    - Cloudflare backend: 800 characters max (BGE model 512 token limit)
    - SQLite-vec backend: No limit (local storage)
    - Hybrid backend: 800 characters max (constrained by Cloudflare sync)

    If content exceeds the backend's limit, it will be automatically split into
    multiple linked memory chunks with preserved context (50-char overlap).
    The splitting respects natural boundaries: paragraphs → sentences → words.

    Args:
        content: The content to store as memory
        tags: Optional tags to categorize the memory (accepts array or comma-separated string)
        memory_type: Type of memory (note, decision, task, reference)
        metadata: Additional metadata for the memory
        client_hostname: Client machine hostname for source tracking

    **Tag Formats - All Formats Supported:**
    Both the tags parameter AND metadata.tags accept ALL formats:
    - ✅ Array format: tags=["tag1", "tag2", "tag3"]
    - ✅ Comma-separated string: tags="tag1,tag2,tag3"
    - ✅ Single string: tags="single-tag"
    - ✅ In metadata: metadata={"tags": "tag1,tag2", "type": "note"}
    - ✅ In metadata (array): metadata={"tags": ["tag1", "tag2"], "type": "note"}

    All formats are automatically normalized internally. If tags are provided in both
    the tags parameter and metadata.tags, they will be merged (duplicates removed).

    Returns:
        Dictionary with:
        - success: Boolean indicating if storage succeeded
        - message: Status message
        - content_hash: Hash of original content (for single memory)
        - chunks_created: Number of chunks (if content was split)
        - chunk_hashes: List of content hashes (if content was split)
    """
    # Delegate to shared MemoryService business logic
    memory_service = ctx.request_context.lifespan_context.memory_service
    result = await memory_service.store_memory(
        content=content,
        tags=tags,
        memory_type=memory_type,
        metadata=metadata,
        client_hostname=client_hostname
    )

    # Transform MemoryService response to MCP tool format
    if not result.get("success"):
        return StoreMemoryFailure(
            success=False,
            message=result.get("error", "Failed to store memory")
        )

    # Handle chunked response (multiple memories)
    if "memories" in result:
        chunk_hashes = [mem["content_hash"] for mem in result["memories"]]
        return StoreMemorySplitSuccess(
            success=True,
            message=f"Successfully stored {len(result['memories'])} memory chunks",
            chunks_created=result["total_chunks"],
            chunk_hashes=chunk_hashes
        )

    # Handle single memory response
    memory_data = result["memory"]
    return StoreMemorySuccess(
        success=True,
        message="Memory stored successfully",
        content_hash=memory_data["content_hash"]
    )

@mcp.tool()
async def retrieve_memory(
    query: str,
    ctx: Context,
    n_results: int = 5
) -> Dict[str, Any]:
    """
    Retrieve memories based on semantic similarity to a query.

    Args:
        query: Search query for semantic similarity
        n_results: Maximum number of results to return

    Returns:
        Dictionary with retrieved memories and metadata
    """
    # Delegate to shared MemoryService business logic
    memory_service = ctx.request_context.lifespan_context.memory_service
    return await memory_service.retrieve_memories(
        query=query,
        n_results=n_results
    )

@mcp.tool()
async def search_by_tag(
    tags: Union[str, List[str]],
    ctx: Context,
    match_all: bool = False
) -> Dict[str, Any]:
    """
    Search memories by tags.

    Args:
        tags: Tag or list of tags to search for
        match_all: If True, memory must have ALL tags; if False, ANY tag

    Returns:
        Dictionary with matching memories
    """
    # Delegate to shared MemoryService business logic
    memory_service = ctx.request_context.lifespan_context.memory_service
    return await memory_service.search_by_tag(
        tags=tags,
        match_all=match_all
    )

@mcp.tool()
async def delete_memory(
    content_hash: str,
    ctx: Context
) -> Dict[str, Union[bool, str]]:
    """
    Delete a specific memory by its content hash.

    Args:
        content_hash: Hash of the memory content to delete

    Returns:
        Dictionary with success status and message
    """
    # Delegate to shared MemoryService business logic
    memory_service = ctx.request_context.lifespan_context.memory_service
    return await memory_service.delete_memory(content_hash)

@mcp.tool()
async def check_database_health(ctx: Context) -> Dict[str, Any]:
    """
    Check the health and status of the memory database.

    Returns:
        Dictionary with health status and statistics
    """
    # Delegate to shared MemoryService business logic
    memory_service = ctx.request_context.lifespan_context.memory_service
    return await memory_service.check_database_health()

@mcp.tool()
async def list_memories(
    ctx: Context,
    page: int = 1,
    page_size: int = 10,
    tag: Optional[str] = None,
    memory_type: Optional[str] = None
) -> Dict[str, Any]:
    """
    List memories with pagination and optional filtering.

    Args:
        page: Page number (1-based)
        page_size: Number of memories per page
        tag: Filter by specific tag
        memory_type: Filter by memory type

    Returns:
        Dictionary with memories and pagination info
    """
    # Delegate to shared MemoryService business logic
    memory_service = ctx.request_context.lifespan_context.memory_service
    return await memory_service.list_memories(
        page=page,
        page_size=page_size,
        tag=tag,
        memory_type=memory_type
    )

@mcp.tool()
async def get_cache_stats(ctx: Context) -> Dict[str, Any]:
    """
    Get MCP server global cache statistics for performance monitoring.

    Returns detailed metrics about storage and memory service caching,
    including hit rates, initialization times, and cache sizes.

    This tool is useful for:
    - Monitoring cache effectiveness
    - Debugging performance issues
    - Verifying cache persistence across stateless HTTP calls

    Returns:
        Dictionary with cache statistics:
        - total_calls: Total MCP server invocations
        - hit_rate: Overall cache hit rate percentage
        - storage_cache: Storage cache metrics (hits/misses/size)
        - service_cache: MemoryService cache metrics (hits/misses/size)
        - performance: Initialization time statistics (avg/min/max)
        - backend_info: Current storage backend configuration
    """
    global _CACHE_STATS, _STORAGE_CACHE, _MEMORY_SERVICE_CACHE

    # Import shared stats calculation utility
    from mcp_memory_service.utils.cache_manager import CacheStats, calculate_cache_stats_dict

    # Convert global dict to CacheStats dataclass
    stats = CacheStats(
        total_calls=_CACHE_STATS["total_calls"],
        storage_hits=_CACHE_STATS["storage_hits"],
        storage_misses=_CACHE_STATS["storage_misses"],
        service_hits=_CACHE_STATS["service_hits"],
        service_misses=_CACHE_STATS["service_misses"],
        initialization_times=_CACHE_STATS["initialization_times"]
    )

    # Calculate statistics using shared utility
    cache_sizes = (len(_STORAGE_CACHE), len(_MEMORY_SERVICE_CACHE))
    result = calculate_cache_stats_dict(stats, cache_sizes)

    # Add server-specific details
    result["storage_cache"]["keys"] = list(_STORAGE_CACHE.keys())
    result["backend_info"]["embedding_model"] = EMBEDDING_MODEL_NAME

    return result



# =============================================================================
# MAIN ENTRY POINT
# =============================================================================

def main():
    """Main entry point for the FastAPI MCP server."""
    # Configure for Claude Code integration
    port = int(os.getenv("MCP_SERVER_PORT", "8000"))
    host = os.getenv("MCP_SERVER_HOST", "0.0.0.0")
    
    logger.info(f"Starting MCP Memory Service FastAPI server on {host}:{port}")
    logger.info(f"Storage backend: {STORAGE_BACKEND}")
    
    # Run server with streamable HTTP transport
    mcp.run("streamable-http")

if __name__ == "__main__":
    main()
```

--------------------------------------------------------------------------------
/tests/consolidation/test_compression.py:
--------------------------------------------------------------------------------

```python
"""Unit tests for the semantic compression engine."""

import pytest
from datetime import datetime, timedelta

from mcp_memory_service.consolidation.compression import (
    SemanticCompressionEngine, 
    CompressionResult
)
from mcp_memory_service.consolidation.base import MemoryCluster
from mcp_memory_service.models.memory import Memory


@pytest.mark.unit
class TestSemanticCompressionEngine:
    """Test the semantic compression system."""
    
    @pytest.fixture
    def compression_engine(self, consolidation_config):
        return SemanticCompressionEngine(consolidation_config)
    
    @pytest.fixture
    def sample_cluster_with_memories(self):
        """Create a sample cluster with corresponding memories."""
        base_time = datetime.now().timestamp()
        
        memories = [
            Memory(
                content="Python list comprehensions provide a concise way to create lists",
                content_hash="hash1",
                tags=["python", "programming", "lists"],
                memory_type="reference",
                embedding=[0.1, 0.2, 0.3] * 107,  # ~320 dim
                created_at=base_time - 86400,
                created_at_iso=datetime.fromtimestamp(base_time - 86400).isoformat() + 'Z'
            ),
            Memory(
                content="List comprehensions in Python are more readable than traditional for loops",
                content_hash="hash2", 
                tags=["python", "readability", "best-practices"],
                memory_type="standard",
                embedding=[0.12, 0.18, 0.32] * 107,
                created_at=base_time - 172800,
                created_at_iso=datetime.fromtimestamp(base_time - 172800).isoformat() + 'Z'
            ),
            Memory(
                content="Example: squares = [x**2 for x in range(10)] creates a list of squares",
                content_hash="hash3",
                tags=["python", "example", "code"],
                memory_type="standard", 
                embedding=[0.11, 0.21, 0.31] * 107,
                created_at=base_time - 259200,
                created_at_iso=datetime.fromtimestamp(base_time - 259200).isoformat() + 'Z'
            ),
            Memory(
                content="Python comprehensions work for lists, sets, and dictionaries",
                content_hash="hash4",
                tags=["python", "comprehensions", "data-structures"],
                memory_type="reference",
                embedding=[0.13, 0.19, 0.29] * 107,
                created_at=base_time - 345600,
                created_at_iso=datetime.fromtimestamp(base_time - 345600).isoformat() + 'Z'
            )
        ]
        
        cluster = MemoryCluster(
            cluster_id="test_cluster",
            memory_hashes=[m.content_hash for m in memories],
            centroid_embedding=[0.12, 0.2, 0.3] * 107,
            coherence_score=0.85,
            created_at=datetime.now(),
            theme_keywords=["python", "comprehensions", "lists", "programming"],
            metadata={"test_cluster": True}
        )
        
        return cluster, memories
    
    @pytest.mark.asyncio
    async def test_basic_compression(self, compression_engine, sample_cluster_with_memories):
        """Test basic compression functionality."""
        cluster, memories = sample_cluster_with_memories
        
        results = await compression_engine.process([cluster], memories)
        
        assert len(results) == 1
        result = results[0]
        
        assert isinstance(result, CompressionResult)
        assert result.cluster_id == "test_cluster"
        assert isinstance(result.compressed_memory, Memory)
        assert result.source_memory_count == 4
        assert 0 < result.compression_ratio < 1  # Should be compressed
        assert len(result.key_concepts) > 0
        assert isinstance(result.temporal_span, dict)
    
    @pytest.mark.asyncio
    async def test_compressed_memory_properties(self, compression_engine, sample_cluster_with_memories):
        """Test properties of the compressed memory object."""
        cluster, memories = sample_cluster_with_memories
        
        results = await compression_engine.process([cluster], memories)
        compressed_memory = results[0].compressed_memory
        
        # Check basic properties
        assert compressed_memory.memory_type == "compressed_cluster"
        assert len(compressed_memory.content) <= compression_engine.max_summary_length
        assert len(compressed_memory.content) > 0
        assert compressed_memory.content_hash is not None
        
        # Check tags (should include cluster tags and compression marker)
        assert "compressed_cluster" in compressed_memory.tags or "compressed" in compressed_memory.tags
        
        # Check metadata
        assert "cluster_id" in compressed_memory.metadata
        assert "compression_date" in compressed_memory.metadata
        assert "source_memory_count" in compressed_memory.metadata
        assert "compression_ratio" in compressed_memory.metadata
        assert "key_concepts" in compressed_memory.metadata
        assert "temporal_span" in compressed_memory.metadata
        assert "theme_keywords" in compressed_memory.metadata
        
        # Check embedding (should use cluster centroid)
        assert compressed_memory.embedding == cluster.centroid_embedding
    
    @pytest.mark.asyncio
    async def test_key_concept_extraction(self, compression_engine, sample_cluster_with_memories):
        """Test extraction of key concepts from cluster memories."""
        cluster, memories = sample_cluster_with_memories
        
        key_concepts = await compression_engine._extract_key_concepts(memories, cluster.theme_keywords)
        
        assert isinstance(key_concepts, list)
        assert len(key_concepts) > 0
        
        # Should include theme keywords
        theme_overlap = set(key_concepts).intersection(set(cluster.theme_keywords))
        assert len(theme_overlap) > 0
        
        # Should extract relevant concepts from content
        expected_concepts = {"python", "comprehensions", "lists"}
        found_concepts = set(concept.lower() for concept in key_concepts)
        overlap = expected_concepts.intersection(found_concepts)
        assert len(overlap) > 0
    
    @pytest.mark.asyncio
    async def test_thematic_summary_generation(self, compression_engine, sample_cluster_with_memories):
        """Test generation of thematic summaries."""
        cluster, memories = sample_cluster_with_memories
        
        # Extract key concepts first
        key_concepts = await compression_engine._extract_key_concepts(memories, cluster.theme_keywords)
        
        # Generate summary
        summary = await compression_engine._generate_thematic_summary(memories, key_concepts)
        
        assert isinstance(summary, str)
        assert len(summary) > 0
        assert len(summary) <= compression_engine.max_summary_length
        
        # Summary should contain information about the cluster
        summary_lower = summary.lower()
        assert "cluster" in summary_lower or str(len(memories)) in summary
        
        # Should mention key concepts
        concept_mentions = sum(1 for concept in key_concepts[:3] if concept.lower() in summary_lower)
        assert concept_mentions > 0
    
    @pytest.mark.asyncio
    async def test_temporal_span_calculation(self, compression_engine, sample_cluster_with_memories):
        """Test calculation of temporal span for memories."""
        cluster, memories = sample_cluster_with_memories
        
        temporal_span = compression_engine._calculate_temporal_span(memories)
        
        assert isinstance(temporal_span, dict)
        assert "start_time" in temporal_span
        assert "end_time" in temporal_span
        assert "span_days" in temporal_span
        assert "span_description" in temporal_span
        assert "start_iso" in temporal_span
        assert "end_iso" in temporal_span
        
        # Check values make sense
        assert temporal_span["start_time"] <= temporal_span["end_time"]
        assert temporal_span["span_days"] >= 0
        assert isinstance(temporal_span["span_description"], str)
    
    @pytest.mark.asyncio
    async def test_tag_aggregation(self, compression_engine, sample_cluster_with_memories):
        """Test aggregation of tags from cluster memories."""
        cluster, memories = sample_cluster_with_memories
        
        aggregated_tags = compression_engine._aggregate_tags(memories)
        
        assert isinstance(aggregated_tags, list)
        assert "cluster" in aggregated_tags
        assert "compressed" in aggregated_tags
        
        # Should include frequent tags from original memories
        original_tags = set()
        for memory in memories:
            original_tags.update(memory.tags)
        
        # Check that some original tags are preserved
        aggregated_set = set(aggregated_tags)
        overlap = original_tags.intersection(aggregated_set)
        assert len(overlap) > 0
    
    @pytest.mark.asyncio
    async def test_metadata_aggregation(self, compression_engine, sample_cluster_with_memories):
        """Test aggregation of metadata from cluster memories."""
        cluster, memories = sample_cluster_with_memories
        
        # Add some metadata to memories
        memories[0].metadata["test_field"] = "value1"
        memories[1].metadata["test_field"] = "value1"  # Same value
        memories[2].metadata["test_field"] = "value2"  # Different value
        memories[3].metadata["unique_field"] = "unique"
        
        aggregated_metadata = compression_engine._aggregate_metadata(memories)
        
        assert isinstance(aggregated_metadata, dict)
        assert "source_memory_hashes" in aggregated_metadata
        
        # Should handle common values
        if "common_test_field" in aggregated_metadata:
            assert aggregated_metadata["common_test_field"] in ["value1", "value2"]
        
        # Should handle varied values
        if "varied_test_field" in aggregated_metadata:
            assert isinstance(aggregated_metadata["varied_test_field"], list)
        
        # Should track variety
        if "unique_field_variety_count" in aggregated_metadata:
            assert aggregated_metadata["unique_field_variety_count"] == 1
    
    @pytest.mark.asyncio
    async def test_compression_ratio_calculation(self, compression_engine, sample_cluster_with_memories):
        """Test compression ratio calculation."""
        cluster, memories = sample_cluster_with_memories
        
        results = await compression_engine.process([cluster], memories)
        result = results[0]
        
        # Calculate expected ratio
        original_size = sum(len(m.content) for m in memories)
        compressed_size = len(result.compressed_memory.content)
        expected_ratio = compressed_size / original_size
        
        assert abs(result.compression_ratio - expected_ratio) < 0.01  # Small tolerance
        assert 0 < result.compression_ratio < 1  # Should be compressed
    
    @pytest.mark.asyncio
    async def test_sentence_splitting(self, compression_engine):
        """Test sentence splitting functionality."""
        text = "This is the first sentence. This is the second sentence! Is this a question? Yes, it is."
        
        sentences = compression_engine._split_into_sentences(text)
        
        assert isinstance(sentences, list)
        assert len(sentences) >= 3  # Should find multiple sentences
        
        # Check that sentences are properly cleaned
        for sentence in sentences:
            assert len(sentence) > 10  # Minimum length filter
            assert sentence.strip() == sentence  # Should be trimmed
    
    @pytest.mark.asyncio
    async def test_empty_cluster_handling(self, compression_engine):
        """Test handling of empty clusters."""
        results = await compression_engine.process([], [])
        assert results == []
    
    @pytest.mark.asyncio
    async def test_single_memory_cluster(self, compression_engine):
        """Test handling of cluster with single memory (should be skipped)."""
        memory = Memory(
            content="Single memory content",
            content_hash="single",
            tags=["test"],
            embedding=[0.1] * 320,
            created_at=datetime.now().timestamp()
        )
        
        cluster = MemoryCluster(
            cluster_id="single_cluster",
            memory_hashes=["single"],
            centroid_embedding=[0.1] * 320,
            coherence_score=1.0,
            created_at=datetime.now(),
            theme_keywords=["test"]
        )
        
        results = await compression_engine.process([cluster], [memory])
        
        # Should skip clusters with insufficient memories
        assert results == []
    
    @pytest.mark.asyncio
    async def test_missing_memories_handling(self, compression_engine):
        """Test handling of cluster referencing missing memories."""
        cluster = MemoryCluster(
            cluster_id="missing_cluster",
            memory_hashes=["missing1", "missing2", "missing3"],
            centroid_embedding=[0.1] * 320,
            coherence_score=0.8,
            created_at=datetime.now(),
            theme_keywords=["missing"]
        )
        
        # Provide empty memories list
        results = await compression_engine.process([cluster], [])
        
        # Should handle missing memories gracefully
        assert results == []
    
    @pytest.mark.asyncio
    async def test_compression_benefit_estimation(self, compression_engine, sample_cluster_with_memories):
        """Test estimation of compression benefits."""
        cluster, memories = sample_cluster_with_memories
        
        benefits = await compression_engine.estimate_compression_benefit([cluster], memories)
        
        assert isinstance(benefits, dict)
        assert "compressible_clusters" in benefits
        assert "total_original_size" in benefits
        assert "estimated_compressed_size" in benefits
        assert "compression_ratio" in benefits
        assert "estimated_savings_bytes" in benefits
        assert "estimated_savings_percent" in benefits
        
        # Check values make sense
        assert benefits["compressible_clusters"] >= 0
        assert benefits["total_original_size"] >= 0
        assert benefits["estimated_compressed_size"] >= 0
        assert 0 <= benefits["compression_ratio"] <= 1
        assert benefits["estimated_savings_bytes"] >= 0
        assert 0 <= benefits["estimated_savings_percent"] <= 100
    
    @pytest.mark.asyncio
    async def test_large_content_truncation(self, compression_engine):
        """Test handling of content that exceeds max summary length."""
        # Create memories with very long content
        long_memories = []
        base_time = datetime.now().timestamp()
        
        for i in range(3):
            # Create content longer than max_summary_length
            long_content = "This is a very long memory content. " * 50  # Much longer than 200 chars
            memory = Memory(
                content=long_content,
                content_hash=f"long_{i}",
                tags=["long", "test"],
                embedding=[0.1 + i*0.1] * 320,
                created_at=base_time - (i * 3600)
            )
            long_memories.append(memory)
        
        cluster = MemoryCluster(
            cluster_id="long_cluster",
            memory_hashes=[m.content_hash for m in long_memories],
            centroid_embedding=[0.2] * 320,
            coherence_score=0.8,
            created_at=datetime.now(),
            theme_keywords=["long", "content"]
        )
        
        results = await compression_engine.process([cluster], long_memories)
        
        if results:
            compressed_content = results[0].compressed_memory.content
            # Should be truncated to max length
            assert len(compressed_content) <= compression_engine.max_summary_length
            
            # Should indicate truncation if content was cut off
            if len(compressed_content) == compression_engine.max_summary_length:
                assert compressed_content.endswith("...")
    
    @pytest.mark.asyncio
    async def test_key_concept_extraction_comprehensive(self, compression_engine):
        """Test comprehensive key concept extraction from memories."""
        # Create memories with various content patterns
        memories = []
        base_time = datetime.now().timestamp()
        
        content_examples = [
            "Check out https://example.com for more info about CamelCaseVariable usage.",
            "Email me at [email protected] if you have questions about the API response.",  
            "The system returns {'status': 'success', 'code': 200} for valid requests.",
            "Today's date is 2024-01-15 and the time is 14:30 for scheduling.",
            "See 'important documentation' for details on snake_case_variable patterns."
        ]
        
        for i, content in enumerate(content_examples):
            memory = Memory(
                content=content,
                content_hash=f"concept_test_{i}",
                tags=["test", "concept", "extraction"],
                embedding=[0.1 + i*0.01] * 320,
                created_at=base_time - (i * 3600)
            )
            memories.append(memory)
        
        theme_keywords = ["test", "API", "documentation", "variable"]
        
        concepts = await compression_engine._extract_key_concepts(memories, theme_keywords)
        
        # Should include theme keywords
        assert any("test" in concepts for concept in [theme_keywords])
        
        # Should extract concepts from content
        assert isinstance(concepts, list)
        assert len(concepts) > 0
        
        # Concepts should be strings
        assert all(isinstance(concept, str) for concept in concepts)
    
    @pytest.mark.asyncio
    async def test_memories_without_timestamps(self, compression_engine):
        """Test handling of memories with timestamps (Memory model auto-sets them)."""
        memories = [
            Memory(
                content="Memory with auto-generated timestamp",
                content_hash="auto_timestamp",
                tags=["test"],
                embedding=[0.1] * 320,
                created_at=None  # Will be auto-set by Memory model
            )
        ]
        
        cluster = MemoryCluster(
            cluster_id="auto_timestamp_cluster",
            memory_hashes=["auto_timestamp"],
            centroid_embedding=[0.1] * 320,
            coherence_score=0.8,
            created_at=datetime.now(),
            theme_keywords=["test"]
        )
        
        # Should handle gracefully without crashing
        temporal_span = compression_engine._calculate_temporal_span(memories)
        
        # Memory model auto-sets timestamps, so these will be actual values
        assert temporal_span["start_time"] is not None
        assert temporal_span["end_time"] is not None
        assert temporal_span["span_days"] >= 0
        assert isinstance(temporal_span["span_description"], str)
```

--------------------------------------------------------------------------------
/tests/test_hybrid_storage.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
"""
Comprehensive tests for HybridMemoryStorage implementation.

Tests cover:
- Basic storage operations (store, retrieve, delete)
- Background synchronization service
- Failover and graceful degradation
- Configuration and health monitoring
- Performance characteristics
"""

import asyncio
import pytest
import pytest_asyncio
import tempfile
import os
import sys
import logging
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
from typing import Dict, Any

# Add src to path for imports
current_dir = Path(__file__).parent
src_dir = current_dir.parent / "src"
sys.path.insert(0, str(src_dir))

from mcp_memory_service.storage.hybrid import HybridMemoryStorage, BackgroundSyncService, SyncOperation
from mcp_memory_service.storage.sqlite_vec import SqliteVecMemoryStorage
from mcp_memory_service.models.memory import Memory, MemoryQueryResult
from mcp_memory_service.utils.hashing import generate_content_hash

# Configure test logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

class MockCloudflareStorage:
    """Mock Cloudflare storage for testing."""

    def __init__(self, **kwargs):
        self.initialized = False
        self.stored_memories = {}
        self.fail_operations = False
        self.fail_initialization = False

    async def initialize(self):
        if self.fail_initialization:
            raise Exception("Mock Cloudflare initialization failed")
        self.initialized = True

    async def store(self, memory: Memory):
        if self.fail_operations:
            return False, "Mock Cloudflare operation failed"
        self.stored_memories[memory.content_hash] = memory
        return True, "Memory stored successfully"

    async def delete(self, content_hash: str):
        if self.fail_operations:
            return False, "Mock Cloudflare operation failed"
        if content_hash in self.stored_memories:
            del self.stored_memories[content_hash]
            return True, "Memory deleted successfully"
        return False, "Memory not found"

    async def update_memory_metadata(self, content_hash: str, updates: Dict[str, Any], preserve_timestamps: bool = True):
        if self.fail_operations:
            return False, "Mock Cloudflare operation failed"
        if content_hash in self.stored_memories:
            # Simple mock update
            return True, "Memory updated successfully"
        return False, "Memory not found"

    async def get_stats(self):
        if self.fail_operations:
            raise Exception("Mock Cloudflare stats failed")
        return {
            "total_memories": len(self.stored_memories),
            "storage_backend": "MockCloudflareStorage"
        }

    async def close(self):
        pass


@pytest_asyncio.fixture
async def temp_sqlite_db():
    """Create a temporary SQLite database for testing."""
    with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as tmp_file:
        db_path = tmp_file.name

    yield db_path

    # Cleanup
    if os.path.exists(db_path):
        os.unlink(db_path)


@pytest_asyncio.fixture
async def mock_cloudflare_config():
    """Mock Cloudflare configuration for testing."""
    return {
        'api_token': 'test_token',
        'account_id': 'test_account',
        'vectorize_index': 'test_index',
        'd1_database_id': 'test_db_id'
    }


@pytest_asyncio.fixture
async def hybrid_storage(temp_sqlite_db, mock_cloudflare_config):
    """Create a HybridMemoryStorage instance for testing."""
    with patch('mcp_memory_service.storage.hybrid.CloudflareStorage', MockCloudflareStorage):
        storage = HybridMemoryStorage(
            sqlite_db_path=temp_sqlite_db,
            embedding_model="all-MiniLM-L6-v2",
            cloudflare_config=mock_cloudflare_config,
            sync_interval=1,  # Short interval for testing
            batch_size=5
        )
        await storage.initialize()
        yield storage
        await storage.close()


@pytest.fixture
def sample_memory():
    """Create a sample memory for testing."""
    content = "This is a test memory for hybrid storage"
    return Memory(
        content=content,
        content_hash=generate_content_hash(content),
        tags=["test", "sample"],
        memory_type="test",
        metadata={},
        created_at=1638360000.0
    )


class TestHybridMemoryStorage:
    """Test cases for HybridMemoryStorage functionality."""

    @pytest.mark.asyncio
    async def test_initialization_with_cloudflare(self, temp_sqlite_db, mock_cloudflare_config):
        """Test successful initialization with Cloudflare configuration."""
        with patch('mcp_memory_service.storage.hybrid.CloudflareStorage', MockCloudflareStorage):
            storage = HybridMemoryStorage(
                sqlite_db_path=temp_sqlite_db,
                cloudflare_config=mock_cloudflare_config
            )

            await storage.initialize()

            assert storage.initialized
            assert storage.primary is not None
            assert storage.secondary is not None
            assert storage.sync_service is not None
            assert storage.sync_service.is_running

            await storage.close()

    @pytest.mark.asyncio
    async def test_initialization_without_cloudflare(self, temp_sqlite_db):
        """Test initialization without Cloudflare configuration (SQLite-only mode)."""
        storage = HybridMemoryStorage(sqlite_db_path=temp_sqlite_db)

        await storage.initialize()

        assert storage.initialized
        assert storage.primary is not None
        assert storage.secondary is None
        assert storage.sync_service is None

        await storage.close()

    @pytest.mark.asyncio
    async def test_initialization_with_cloudflare_failure(self, temp_sqlite_db, mock_cloudflare_config):
        """Test graceful handling of Cloudflare initialization failure."""
        def failing_cloudflare_storage(**kwargs):
            storage = MockCloudflareStorage(**kwargs)
            storage.fail_initialization = True
            return storage

        with patch('mcp_memory_service.storage.hybrid.CloudflareStorage', failing_cloudflare_storage):
            storage = HybridMemoryStorage(
                sqlite_db_path=temp_sqlite_db,
                cloudflare_config=mock_cloudflare_config
            )

            await storage.initialize()

            # Should fall back to SQLite-only mode
            assert storage.initialized
            assert storage.primary is not None
            assert storage.secondary is None
            assert storage.sync_service is None

            await storage.close()

    @pytest.mark.asyncio
    async def test_store_memory(self, hybrid_storage, sample_memory):
        """Test storing a memory in hybrid storage."""
        success, message = await hybrid_storage.store(sample_memory)

        assert success
        assert "success" in message.lower() or message == ""

        # Verify memory is stored in primary
        results = await hybrid_storage.retrieve(sample_memory.content, n_results=1)
        assert len(results) == 1
        assert results[0].memory.content == sample_memory.content

    @pytest.mark.asyncio
    async def test_retrieve_memory(self, hybrid_storage, sample_memory):
        """Test retrieving memories from hybrid storage."""
        # Store a memory first
        await hybrid_storage.store(sample_memory)

        # Retrieve by query
        results = await hybrid_storage.retrieve("test memory", n_results=5)
        assert len(results) >= 1

        # Check that we get the stored memory
        found = any(result.memory.content == sample_memory.content for result in results)
        assert found

    @pytest.mark.asyncio
    async def test_delete_memory(self, hybrid_storage, sample_memory):
        """Test deleting a memory from hybrid storage."""
        # Store a memory first
        await hybrid_storage.store(sample_memory)

        # Delete the memory
        success, message = await hybrid_storage.delete(sample_memory.content_hash)
        assert success

        # Verify memory is deleted from primary
        results = await hybrid_storage.retrieve(sample_memory.content, n_results=1)
        # Should not find the deleted memory
        found = any(result.memory.content_hash == sample_memory.content_hash for result in results)
        assert not found

    @pytest.mark.asyncio
    async def test_search_by_tags(self, hybrid_storage, sample_memory):
        """Test searching memories by tags."""
        # Store a memory first
        await hybrid_storage.store(sample_memory)

        # Search by tags
        results = await hybrid_storage.search_by_tags(["test"])
        assert len(results) >= 1

        # Check that we get the stored memory
        found = any(memory.content == sample_memory.content for memory in results)
        assert found

    @pytest.mark.asyncio
    async def test_get_stats(self, hybrid_storage):
        """Test getting statistics from hybrid storage."""
        stats = await hybrid_storage.get_stats()

        assert "storage_backend" in stats
        assert stats["storage_backend"] == "Hybrid (SQLite-vec + Cloudflare)"
        assert "primary_stats" in stats
        assert "sync_enabled" in stats
        assert stats["sync_enabled"] == True
        assert "sync_status" in stats

    @pytest.mark.asyncio
    async def test_force_sync(self, hybrid_storage, sample_memory):
        """Test forcing immediate synchronization."""
        # Store some memories
        await hybrid_storage.store(sample_memory)

        # Force sync
        result = await hybrid_storage.force_sync()

        assert "status" in result
        assert result["status"] in ["completed", "partial"]
        assert "primary_memories" in result
        assert result["primary_memories"] >= 1


class TestBackgroundSyncService:
    """Test cases for BackgroundSyncService functionality."""

    @pytest_asyncio.fixture
    async def sync_service_components(self, temp_sqlite_db):
        """Create components needed for sync service testing."""
        primary = SqliteVecMemoryStorage(temp_sqlite_db)
        await primary.initialize()

        secondary = MockCloudflareStorage()
        await secondary.initialize()

        sync_service = BackgroundSyncService(
            primary, secondary,
            sync_interval=1,
            batch_size=3
        )

        yield primary, secondary, sync_service

        if sync_service.is_running:
            await sync_service.stop()

        if hasattr(primary, 'close'):
            await primary.close()

    @pytest.mark.asyncio
    async def test_sync_service_start_stop(self, sync_service_components):
        """Test starting and stopping the background sync service."""
        primary, secondary, sync_service = sync_service_components

        # Start service
        await sync_service.start()
        assert sync_service.is_running

        # Stop service
        await sync_service.stop()
        assert not sync_service.is_running

    @pytest.mark.asyncio
    async def test_operation_enqueue(self, sync_service_components, sample_memory):
        """Test enqueuing sync operations."""
        primary, secondary, sync_service = sync_service_components

        await sync_service.start()

        # Enqueue a store operation
        operation = SyncOperation(operation='store', memory=sample_memory)
        await sync_service.enqueue_operation(operation)

        # Wait a bit for processing
        await asyncio.sleep(0.1)

        # Check queue size decreased
        status = await sync_service.get_sync_status()
        assert status['queue_size'] >= 0  # Should be processed or in progress

        await sync_service.stop()

    @pytest.mark.asyncio
    async def test_sync_with_cloudflare_failure(self, sync_service_components):
        """Test sync behavior when Cloudflare operations fail."""
        primary, secondary, sync_service = sync_service_components

        # Make Cloudflare operations fail
        secondary.fail_operations = True

        await sync_service.start()

        # Create a test memory
        content = "test content"
        memory = Memory(
            content=content,
            content_hash=generate_content_hash(content),
            tags=["test"],
            memory_type="test"
        )

        # Enqueue operation
        operation = SyncOperation(operation='store', memory=memory)
        await sync_service.enqueue_operation(operation)

        # Wait for processing
        await asyncio.sleep(0.2)

        # Check that service marked Cloudflare as unavailable
        status = await sync_service.get_sync_status()
        assert status['cloudflare_available'] == False

        await sync_service.stop()

    @pytest.mark.asyncio
    async def test_force_sync_functionality(self, sync_service_components):
        """Test force sync functionality."""
        primary, secondary, sync_service = sync_service_components

        # Store some test memories in primary
        content1 = "test memory 1"
        content2 = "test memory 2"
        memory1 = Memory(
            content=content1,
            content_hash=generate_content_hash(content1),
            tags=["test"],
            memory_type="test"
        )
        memory2 = Memory(
            content=content2,
            content_hash=generate_content_hash(content2),
            tags=["test"],
            memory_type="test"
        )

        await primary.store(memory1)
        await primary.store(memory2)

        await sync_service.start()

        # Force sync
        result = await sync_service.force_sync()

        assert result['status'] == 'completed'
        assert result['primary_memories'] == 2
        assert result['synced_to_secondary'] >= 0

        await sync_service.stop()

    @pytest.mark.asyncio
    async def test_sync_status_reporting(self, sync_service_components):
        """Test sync status reporting functionality."""
        primary, secondary, sync_service = sync_service_components

        await sync_service.start()

        status = await sync_service.get_sync_status()

        assert 'is_running' in status
        assert status['is_running'] == True
        assert 'queue_size' in status
        assert 'stats' in status
        assert 'cloudflare_available' in status

        await sync_service.stop()


class TestPerformanceCharacteristics:
    """Test performance characteristics of hybrid storage."""

    @pytest.mark.asyncio
    async def test_read_performance(self, hybrid_storage, sample_memory):
        """Test that reads are fast (should use SQLite-vec)."""
        # Store a memory
        await hybrid_storage.store(sample_memory)

        # Measure read performance
        import time

        start_time = time.time()
        results = await hybrid_storage.retrieve(sample_memory.content[:10], n_results=1)
        duration = time.time() - start_time

        # Should be very fast (< 100ms for local SQLite-vec)
        assert duration < 0.1
        assert len(results) >= 0  # Should get some results

    @pytest.mark.asyncio
    async def test_write_performance(self, hybrid_storage):
        """Test that writes are fast (immediate SQLite-vec write)."""
        content = "Performance test memory"
        memory = Memory(
            content=content,
            content_hash=generate_content_hash(content),
            tags=["perf"],
            memory_type="performance_test"
        )

        import time

        start_time = time.time()
        success, message = await hybrid_storage.store(memory)
        duration = time.time() - start_time

        # Should be very fast (< 100ms for local SQLite-vec)
        assert duration < 0.1
        assert success

    @pytest.mark.asyncio
    async def test_concurrent_operations(self, hybrid_storage):
        """Test concurrent memory operations."""
        # Create multiple memories
        memories = []
        for i in range(10):
            content = f"Concurrent test memory {i}"
            memory = Memory(
                content=content,
                content_hash=generate_content_hash(content),
                tags=["concurrent", f"test{i}"],
                memory_type="concurrent_test"
            )
            memories.append(memory)

        # Store all memories concurrently
        tasks = [hybrid_storage.store(memory) for memory in memories]
        results = await asyncio.gather(*tasks)

        # All operations should succeed
        assert all(success for success, message in results)

        # Should be able to retrieve all memories
        search_results = await hybrid_storage.search_by_tags(["concurrent"])
        assert len(search_results) == 10


class TestErrorHandlingAndFallback:
    """Test error handling and fallback scenarios."""

    @pytest.mark.asyncio
    async def test_sqlite_only_mode(self, temp_sqlite_db):
        """Test operation in SQLite-only mode (no Cloudflare)."""
        storage = HybridMemoryStorage(sqlite_db_path=temp_sqlite_db)
        await storage.initialize()

        # Should work normally without Cloudflare
        content = "SQLite-only test memory"
        memory = Memory(
            content=content,
            content_hash=generate_content_hash(content),
            tags=["local"],
            memory_type="sqlite_only"
        )

        success, message = await storage.store(memory)
        assert success

        results = await storage.retrieve(memory.content, n_results=1)
        assert len(results) >= 1

        await storage.close()

    @pytest.mark.asyncio
    async def test_graceful_degradation(self, temp_sqlite_db, mock_cloudflare_config):
        """Test graceful degradation when Cloudflare becomes unavailable."""
        def unreliable_cloudflare_storage(**kwargs):
            storage = MockCloudflareStorage(**kwargs)
            # Will start working but then fail
            return storage

        with patch('mcp_memory_service.storage.hybrid.CloudflareStorage', unreliable_cloudflare_storage):
            storage = HybridMemoryStorage(
                sqlite_db_path=temp_sqlite_db,
                cloudflare_config=mock_cloudflare_config
            )

            await storage.initialize()

            # Initially should work
            content = "Degradation test memory"
            memory = Memory(
                content=content,
                content_hash=generate_content_hash(content),
                tags=["test"],
                memory_type="degradation_test"
            )

            success, message = await storage.store(memory)
            assert success

            # Make Cloudflare fail
            storage.secondary.fail_operations = True

            # Should still work (primary storage unaffected)
            content2 = "Second test memory"
            memory2 = Memory(
                content=content2,
                content_hash=generate_content_hash(content2),
                tags=["test"],
                memory_type="degradation_test"
            )
            success, message = await storage.store(memory2)
            assert success

            # Retrieval should still work
            results = await storage.retrieve("test memory", n_results=10)
            assert len(results) >= 2

            await storage.close()


if __name__ == "__main__":
    # Run tests
    pytest.main([__file__, "-v"])
```

--------------------------------------------------------------------------------
/docs/tutorials/data-analysis.md:
--------------------------------------------------------------------------------

```markdown
# Data Analysis Examples

This guide demonstrates how to extract insights, patterns, and visualizations from your MCP Memory Service data, transforming stored knowledge into actionable intelligence.

## 🎯 Overview

The MCP Memory Service can be used not just for storage and retrieval, but as a powerful analytics platform for understanding knowledge patterns, usage trends, and information relationships. This guide shows practical examples of data analysis techniques that reveal valuable insights about your knowledge base.

## 📊 Types of Analysis

### 1. Temporal Analysis
Understanding when and how your knowledge base grows over time.

### 2. Content Analysis  
Analyzing what types of information are stored and how they're organized.

### 3. Usage Pattern Analysis
Identifying how information is accessed and utilized.

### 4. Quality Analysis
Measuring the health and organization of your knowledge base.

### 5. Relationship Analysis
Discovering connections and patterns between different pieces of information.

## 📈 Temporal Distribution Analysis

### Basic Time-Based Queries

**Monthly Distribution:**
```javascript
// Retrieve memories by time period
const januaryMemories = await recall_memory({
  "query": "memories from january 2025",
  "n_results": 50
});

const juneMemories = await recall_memory({
  "query": "memories from june 2025", 
  "n_results": 50
});

// Analyze patterns
console.log(`January: ${januaryMemories.length} memories`);
console.log(`June: ${juneMemories.length} memories`);
```

**Weekly Activity Patterns:**
```javascript
// Get recent activity
const lastWeek = await recall_memory({
  "query": "memories from last week",
  "n_results": 25
});

const thisWeek = await recall_memory({
  "query": "memories from this week",
  "n_results": 25
});

// Compare activity levels
const weeklyGrowth = ((thisWeek.length - lastWeek.length) / lastWeek.length) * 100;
console.log(`Weekly growth rate: ${weeklyGrowth.toFixed(1)}%`);
```

### Advanced Temporal Analysis

**Memory Creation Frequency:**
```javascript
// Process temporal data for visualization
function analyzeMemoryDistribution(memories) {
  const monthlyDistribution = {};
  
  memories.forEach(memory => {
    // Extract date from timestamp
    const date = new Date(memory.timestamp);
    const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
    
    if (!monthlyDistribution[monthKey]) {
      monthlyDistribution[monthKey] = {
        count: 0,
        memories: []
      };
    }
    
    monthlyDistribution[monthKey].count++;
    monthlyDistribution[monthKey].memories.push(memory);
  });
  
  return monthlyDistribution;
}

// Convert to chart data
function prepareChartData(distribution) {
  return Object.entries(distribution)
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([month, data]) => {
      const [year, monthNum] = month.split('-');
      const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 
                         'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
      const monthName = monthNames[parseInt(monthNum) - 1];
      
      return {
        month: `${monthName} ${year}`,
        count: data.count,
        monthKey: month,
        memories: data.memories
      };
    });
}
```

**Project Lifecycle Analysis:**
```javascript
// Analyze project phases through memory patterns
async function analyzeProjectLifecycle(projectTag) {
  const projectMemories = await search_by_tag({
    "tags": [projectTag]
  });
  
  // Group by status tags
  const phases = {
    planning: [],
    development: [],
    testing: [],
    deployment: [],
    maintenance: []
  };
  
  projectMemories.forEach(memory => {
    const tags = memory.tags || [];
    
    if (tags.includes('planning') || tags.includes('design')) {
      phases.planning.push(memory);
    } else if (tags.includes('development') || tags.includes('implementation')) {
      phases.development.push(memory);
    } else if (tags.includes('testing') || tags.includes('debugging')) {
      phases.testing.push(memory);
    } else if (tags.includes('deployment') || tags.includes('production')) {
      phases.deployment.push(memory);
    } else if (tags.includes('maintenance') || tags.includes('optimization')) {
      phases.maintenance.push(memory);
    }
  });
  
  return phases;
}

// Usage example
const mcpLifecycle = await analyzeProjectLifecycle('mcp-memory-service');
console.log('Project phases:', {
  planning: mcpLifecycle.planning.length,
  development: mcpLifecycle.development.length,
  testing: mcpLifecycle.testing.length,
  deployment: mcpLifecycle.deployment.length,
  maintenance: mcpLifecycle.maintenance.length
});
```

## 🏷️ Tag Analysis

### Tag Frequency Analysis

**Most Used Tags:**
```javascript
async function analyzeTagFrequency() {
  // Get all memories (you may need to paginate for large datasets)
  const allMemories = await retrieve_memory({
    "query": "all memories",
    "n_results": 500
  });
  
  const tagFrequency = {};
  
  allMemories.forEach(memory => {
    const tags = memory.tags || [];
    tags.forEach(tag => {
      tagFrequency[tag] = (tagFrequency[tag] || 0) + 1;
    });
  });
  
  // Sort by frequency
  const sortedTags = Object.entries(tagFrequency)
    .sort(([,a], [,b]) => b - a)
    .slice(0, 20); // Top 20 tags
  
  return sortedTags;
}

// Generate insights
const topTags = await analyzeTagFrequency();
console.log('Most used tags:');
topTags.forEach(([tag, count]) => {
  console.log(`${tag}: ${count} memories`);
});
```

**Tag Co-occurrence Analysis:**
```javascript
function analyzeTagRelationships(memories) {
  const cooccurrence = {};
  
  memories.forEach(memory => {
    const tags = memory.tags || [];
    
    // For each pair of tags in the memory
    for (let i = 0; i < tags.length; i++) {
      for (let j = i + 1; j < tags.length; j++) {
        const pair = [tags[i], tags[j]].sort().join(' + ');
        cooccurrence[pair] = (cooccurrence[pair] || 0) + 1;
      }
    }
  });
  
  // Find most common tag combinations
  return Object.entries(cooccurrence)
    .sort(([,a], [,b]) => b - a)
    .slice(0, 10);
}

// Usage
const tagRelationships = analyzeTagRelationships(allMemories);
console.log('Common tag combinations:');
tagRelationships.forEach(([pair, count]) => {
  console.log(`${pair}: ${count} times`);
});
```

### Tag Category Analysis

**Category Distribution:**
```javascript
function categorizeTagsByType(tags) {
  const categories = {
    projects: [],
    technologies: [],
    activities: [],
    status: [],
    content: [],
    temporal: [],
    other: []
  };
  
  // Define patterns for each category
  const patterns = {
    projects: /^(mcp-memory-service|memory-dashboard|github-integration)/,
    technologies: /^(python|react|typescript|chromadb|git|docker)/,
    activities: /^(testing|debugging|development|documentation|deployment)/,
    status: /^(resolved|in-progress|blocked|verified|completed)/,
    content: /^(concept|architecture|tutorial|reference|example)/,
    temporal: /^(january|february|march|april|may|june|q1|q2|2025)/
  };
  
  tags.forEach(([tag, count]) => {
    let categorized = false;
    
    for (const [category, pattern] of Object.entries(patterns)) {
      if (pattern.test(tag)) {
        categories[category].push([tag, count]);
        categorized = true;
        break;
      }
    }
    
    if (!categorized) {
      categories.other.push([tag, count]);
    }
  });
  
  return categories;
}

// Analyze tag distribution by category
const tagCategories = categorizeTagsByType(topTags);
console.log('Tags by category:');
Object.entries(tagCategories).forEach(([category, tags]) => {
  console.log(`${category}: ${tags.length} unique tags`);
});
```

## 📋 Content Quality Analysis

### Tagging Quality Assessment

**Untagged Memory Detection:**
```javascript
async function findUntaggedMemories() {
  // Search for potentially untagged content
  const candidates = await retrieve_memory({
    "query": "test simple basic example memory",
    "n_results": 50
  });
  
  const untagged = candidates.filter(memory => {
    const tags = memory.tags || [];
    return tags.length === 0 || 
           (tags.length === 1 && ['test', 'memory', 'note'].includes(tags[0]));
  });
  
  return {
    total: candidates.length,
    untagged: untagged.length,
    percentage: (untagged.length / candidates.length) * 100,
    examples: untagged.slice(0, 5)
  };
}

// Quality assessment
const qualityReport = await findUntaggedMemories();
console.log(`Tagging quality: ${(100 - qualityReport.percentage).toFixed(1)}% properly tagged`);
```

**Tag Consistency Analysis:**
```javascript
function analyzeTagConsistency(memories) {
  const patterns = {};
  const inconsistencies = [];
  
  memories.forEach(memory => {
    const content = memory.content;
    const tags = memory.tags || [];
    
    // Look for common content patterns
    if (content.includes('issue') || content.includes('bug')) {
      const hasIssueTag = tags.some(tag => tag.includes('issue') || tag.includes('bug'));
      if (!hasIssueTag) {
        inconsistencies.push({
          type: 'missing-issue-tag',
          memory: memory.content.substring(0, 100),
          tags: tags
        });
      }
    }
    
    if (content.includes('test') || content.includes('TEST')) {
      const hasTestTag = tags.includes('test') || tags.includes('testing');
      if (!hasTestTag) {
        inconsistencies.push({
          type: 'missing-test-tag',
          memory: memory.content.substring(0, 100),
          tags: tags
        });
      }
    }
  });
  
  return {
    totalMemories: memories.length,
    inconsistencies: inconsistencies.length,
    consistencyScore: ((memories.length - inconsistencies.length) / memories.length) * 100,
    examples: inconsistencies.slice(0, 5)
  };
}
```

## 📊 Visualization Examples

### Memory Distribution Chart Data

**Prepare data for visualization:**
```javascript
function prepareDistributionData(memories) {
  const distribution = analyzeMemoryDistribution(memories);
  const chartData = prepareChartData(distribution);
  
  // Add additional metrics
  const total = chartData.reduce((sum, item) => sum + item.count, 0);
  const average = total / chartData.length;
  
  // Identify peaks and valleys
  const peak = chartData.reduce((max, item) => 
    item.count > max.count ? item : max, chartData[0]);
  const valley = chartData.reduce((min, item) => 
    item.count < min.count ? item : min, chartData[0]);
  
  return {
    chartData,
    metrics: {
      total,
      average: Math.round(average * 10) / 10,
      peak: { month: peak.month, count: peak.count },
      valley: { month: valley.month, count: valley.count },
      growth: calculateGrowthRate(chartData)
    }
  };
}

function calculateGrowthRate(chartData) {
  if (chartData.length < 2) return 0;
  
  const first = chartData[0].count;
  const last = chartData[chartData.length - 1].count;
  
  return ((last - first) / first) * 100;
}
```

### Activity Heatmap Data

**Generate activity patterns:**
```javascript
function generateActivityHeatmap(memories) {
  const heatmapData = {};
  
  memories.forEach(memory => {
    const date = new Date(memory.timestamp);
    const dayOfWeek = date.getDay(); // 0 = Sunday
    const hour = date.getHours();
    
    const key = `${dayOfWeek}-${hour}`;
    heatmapData[key] = (heatmapData[key] || 0) + 1;
  });
  
  // Convert to matrix format for visualization
  const matrix = [];
  const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
  
  for (let day = 0; day < 7; day++) {
    const dayData = [];
    for (let hour = 0; hour < 24; hour++) {
      const key = `${day}-${hour}`;
      dayData.push({
        day: days[day],
        hour: hour,
        value: heatmapData[key] || 0
      });
    }
    matrix.push(dayData);
  }
  
  return matrix;
}
```

## 🔍 Advanced Analytics

### Semantic Similarity Analysis

**Find related memories:**
```javascript
async function findRelatedMemories(targetMemory, threshold = 0.7) {
  // Use semantic search to find similar content
  const related = await retrieve_memory({
    "query": targetMemory.content.substring(0, 200),
    "n_results": 20
  });
  
  // Filter by relevance score (if available)
  const highlyRelated = related.filter(memory => 
    memory.relevanceScore > threshold &&
    memory.content_hash !== targetMemory.content_hash
  );
  
  return highlyRelated;
}

// Build knowledge graph data
async function buildKnowledgeGraph(memories) {
  const nodes = [];
  const edges = [];
  
  for (const memory of memories.slice(0, 50)) { // Limit for performance
    nodes.push({
      id: memory.content_hash,
      label: memory.content.substring(0, 50) + '...',
      tags: memory.tags || [],
      group: memory.tags?.[0] || 'untagged'
    });
    
    const related = await findRelatedMemories(memory, 0.8);
    
    related.forEach(relatedMemory => {
      edges.push({
        from: memory.content_hash,
        to: relatedMemory.content_hash,
        weight: relatedMemory.relevanceScore || 0.5
      });
    });
  }
  
  return { nodes, edges };
}
```

### Trend Analysis

**Identify emerging patterns:**
```javascript
function analyzeTrends(memories, timeWindow = 30) {
  const now = new Date();
  const cutoff = new Date(now - timeWindow * 24 * 60 * 60 * 1000);
  
  const recentMemories = memories.filter(memory => 
    new Date(memory.timestamp) > cutoff
  );
  
  const historicalMemories = memories.filter(memory => 
    new Date(memory.timestamp) <= cutoff
  );
  
  // Analyze tag frequency changes
  const recentTags = getTagFrequency(recentMemories);
  const historicalTags = getTagFrequency(historicalMemories);
  
  const trends = [];
  
  Object.entries(recentTags).forEach(([tag, recentCount]) => {
    const historicalCount = historicalTags[tag] || 0;
    const change = recentCount - historicalCount;
    const changePercent = historicalCount > 0 ? 
      (change / historicalCount) * 100 : 100;
    
    if (Math.abs(changePercent) > 50) { // Significant change
      trends.push({
        tag,
        trend: changePercent > 0 ? 'increasing' : 'decreasing',
        change: changePercent,
        recentCount,
        historicalCount
      });
    }
  });
  
  return trends.sort((a, b) => Math.abs(b.change) - Math.abs(a.change));
}

function getTagFrequency(memories) {
  const frequency = {};
  memories.forEach(memory => {
    (memory.tags || []).forEach(tag => {
      frequency[tag] = (frequency[tag] || 0) + 1;
    });
  });
  return frequency;
}
```

## 📋 Analysis Workflows

### Daily Analytics Routine

```javascript
async function runDailyAnalytics() {
  console.log('🔍 Daily Memory Analytics Report');
  console.log('================================');
  
  // 1. Recent activity
  const todayMemories = await recall_memory({
    "query": "memories from today",
    "n_results": 50
  });
  console.log(`📊 Memories added today: ${todayMemories.length}`);
  
  // 2. Tag quality check
  const qualityReport = await findUntaggedMemories();
  console.log(`🏷️  Tagging quality: ${(100 - qualityReport.percentage).toFixed(1)}%`);
  
  // 3. Most active projects
  const topTags = await analyzeTagFrequency();
  const topProjects = topTags.filter(([tag]) => 
    tag.includes('project') || tag.includes('service')
  ).slice(0, 3);
  console.log('🚀 Most active projects:', topProjects);
  
  // 4. Database health
  const health = await check_database_health();
  console.log(`💾 Database health: ${health.status}`);
  
  console.log('\n✅ Daily analytics complete');
}
```

### Weekly Analysis Report

```javascript
async function generateWeeklyReport() {
  const weekMemories = await recall_memory({
    "query": "memories from last week",
    "n_results": 100
  });
  
  const report = {
    summary: {
      totalMemories: weekMemories.length,
      date: new Date().toISOString().split('T')[0]
    },
    
    topCategories: analyzeTagFrequency(weekMemories),
    
    qualityMetrics: await findUntaggedMemories(),
    
    trends: analyzeTrends(weekMemories, 7),
    
    recommendations: generateRecommendations(weekMemories)
  };
  
  // Store report as memory
  await store_memory({
    "content": `Weekly Analytics Report - ${report.summary.date}: ${JSON.stringify(report, null, 2)}`,
    "metadata": {
      "tags": ["analytics", "weekly-report", "metrics", "summary"],
      "type": "analytics-report"
    }
  });
  
  return report;
}

function generateRecommendations(memories) {
  const recommendations = [];
  
  // Tag consistency recommendations
  const untagged = memories.filter(m => (m.tags || []).length === 0);
  if (untagged.length > 0) {
    recommendations.push({
      type: 'tagging',
      priority: 'high',
      message: `${untagged.length} memories need tagging`
    });
  }
  
  // Content organization recommendations
  const testMemories = memories.filter(m => 
    m.content.toLowerCase().includes('test') && 
    !(m.tags || []).includes('test')
  );
  if (testMemories.length > 0) {
    recommendations.push({
      type: 'organization',
      priority: 'medium',
      message: `${testMemories.length} test memories need proper categorization`
    });
  }
  
  return recommendations;
}
```

## 🎯 Practical Implementation

### Setting Up Analytics Pipeline

**1. Create analysis script:**
```javascript
// analytics.js
const MemoryAnalytics = {
  async runFullAnalysis() {
    const results = {
      temporal: await this.analyzeTemporalDistribution(),
      tags: await this.analyzeTagUsage(),
      quality: await this.assessQuality(),
      trends: await this.identifyTrends()
    };
    
    return results;
  },
  
  async generateVisualizationData() {
    const memories = await this.getAllMemories();
    return prepareDistributionData(memories);
  }
};
```

**2. Schedule regular analysis:**
```javascript
// Run analytics and store results
async function scheduledAnalysis() {
  const results = await MemoryAnalytics.runFullAnalysis();
  
  await store_memory({
    "content": `Automated Analytics Report: ${JSON.stringify(results, null, 2)}`,
    "metadata": {
      "tags": ["automated-analytics", "system-analysis", "metrics"],
      "type": "analytics-report"
    }
  });
}

// Run weekly
setInterval(scheduledAnalysis, 7 * 24 * 60 * 60 * 1000);
```

## 📊 Export and Integration

### Data Export for External Tools

**CSV Export:**
```javascript
function exportToCSV(memories) {
  const headers = ['Timestamp', 'Content_Preview', 'Tags', 'Type'];
  const rows = memories.map(memory => [
    memory.timestamp,
    memory.content.substring(0, 100).replace(/,/g, ';'),
    (memory.tags || []).join(';'),
    memory.type || 'unknown'
  ]);
  
  const csv = [headers, ...rows]
    .map(row => row.map(field => `"${field}"`).join(','))
    .join('\n');
  
  return csv;
}
```

**JSON Export for Visualization Tools:**
```javascript
function exportForVisualization(memories) {
  return {
    metadata: {
      total: memories.length,
      exported: new Date().toISOString(),
      schema_version: '1.0'
    },
    
    temporal_data: prepareDistributionData(memories),
    
    tag_analysis: analyzeTagFrequency(memories),
    
    relationships: buildKnowledgeGraph(memories),
    
    quality_metrics: assessQuality(memories)
  };
}
```

---

*These analysis examples demonstrate the power of treating your MCP Memory Service as not just storage, but as a comprehensive analytics platform for understanding and optimizing your knowledge management workflows.*
```

--------------------------------------------------------------------------------
/src/mcp_memory_service/services/memory_service.py:
--------------------------------------------------------------------------------

```python
"""
Memory Service - Shared business logic for memory operations.

This service contains the shared business logic that was previously duplicated
between mcp_server.py and server.py. It provides a single source of truth for
all memory operations, eliminating the DRY violation and ensuring consistent behavior.
"""

import logging
from typing import Dict, List, Optional, Any, Union, Tuple, TypedDict
from datetime import datetime

from ..config import (
    INCLUDE_HOSTNAME,
    CONTENT_PRESERVE_BOUNDARIES,
    CONTENT_SPLIT_OVERLAP,
    ENABLE_AUTO_SPLIT
)
from ..storage.base import MemoryStorage
from ..models.memory import Memory
from ..utils.content_splitter import split_content
from ..utils.hashing import generate_content_hash

logger = logging.getLogger(__name__)


def normalize_tags(tags: Union[str, List[str], None]) -> List[str]:
    """
    Normalize tags to a consistent list format.

    Handles all input formats:
    - None → []
    - "tag1,tag2,tag3" → ["tag1", "tag2", "tag3"]
    - "single-tag" → ["single-tag"]
    - ["tag1", "tag2"] → ["tag1", "tag2"]

    Args:
        tags: Tags in any supported format (None, string, comma-separated string, or list)

    Returns:
        List of tag strings, empty list if None or empty string
    """
    if tags is None:
        return []

    if isinstance(tags, str):
        # Empty string returns empty list
        if not tags.strip():
            return []
        # Split by comma if present, otherwise single tag
        if ',' in tags:
            return [tag.strip() for tag in tags.split(',') if tag.strip()]
        return [tags.strip()]

    # Already a list - return as-is
    return tags


class MemoryResult(TypedDict):
    """Type definition for memory operation results."""
    content: str
    content_hash: str
    tags: List[str]
    memory_type: Optional[str]
    metadata: Optional[Dict[str, Any]]
    created_at: str
    updated_at: str
    created_at_iso: str
    updated_at_iso: str


# Store Memory Return Types
class StoreMemorySingleSuccess(TypedDict):
    """Return type for successful single memory storage."""
    success: bool
    memory: MemoryResult


class StoreMemoryChunkedSuccess(TypedDict):
    """Return type for successful chunked memory storage."""
    success: bool
    memories: List[MemoryResult]
    total_chunks: int
    original_hash: str


class StoreMemoryFailure(TypedDict):
    """Return type for failed memory storage."""
    success: bool
    error: str


# List Memories Return Types
class ListMemoriesSuccess(TypedDict):
    """Return type for successful memory listing."""
    memories: List[MemoryResult]
    page: int
    page_size: int
    total: int
    has_more: bool


class ListMemoriesError(TypedDict):
    """Return type for failed memory listing."""
    success: bool
    error: str
    memories: List[MemoryResult]
    page: int
    page_size: int


# Retrieve Memories Return Types
class RetrieveMemoriesSuccess(TypedDict):
    """Return type for successful memory retrieval."""
    memories: List[MemoryResult]
    query: str
    count: int


class RetrieveMemoriesError(TypedDict):
    """Return type for failed memory retrieval."""
    memories: List[MemoryResult]
    query: str
    error: str


# Search by Tag Return Types
class SearchByTagSuccess(TypedDict):
    """Return type for successful tag search."""
    memories: List[MemoryResult]
    tags: List[str]
    match_type: str
    count: int


class SearchByTagError(TypedDict):
    """Return type for failed tag search."""
    memories: List[MemoryResult]
    tags: List[str]
    error: str


# Delete Memory Return Types
class DeleteMemorySuccess(TypedDict):
    """Return type for successful memory deletion."""
    success: bool
    content_hash: str


class DeleteMemoryFailure(TypedDict):
    """Return type for failed memory deletion."""
    success: bool
    content_hash: str
    error: str


# Health Check Return Types
class HealthCheckSuccess(TypedDict, total=False):
    """Return type for successful health check."""
    healthy: bool
    storage_type: str
    total_memories: int
    last_updated: str
    # Additional fields from storage stats (marked as not required via total=False)


class HealthCheckFailure(TypedDict):
    """Return type for failed health check."""
    healthy: bool
    error: str


class MemoryService:
    """
    Shared service for memory operations with consistent business logic.

    This service centralizes all memory-related business logic to ensure
    consistent behavior across API endpoints and MCP tools, eliminating
    code duplication and potential inconsistencies.
    """

    def __init__(self, storage: MemoryStorage):
        self.storage = storage

    async def list_memories(
        self,
        page: int = 1,
        page_size: int = 10,
        tag: Optional[str] = None,
        memory_type: Optional[str] = None
    ) -> Union[ListMemoriesSuccess, ListMemoriesError]:
        """
        List memories with pagination and optional filtering.

        This method provides database-level filtering for optimal performance,
        avoiding the common anti-pattern of loading all records into memory.

        Args:
            page: Page number (1-based)
            page_size: Number of memories per page
            tag: Filter by specific tag
            memory_type: Filter by memory type

        Returns:
            Dictionary with memories and pagination info
        """
        try:
            # Calculate offset for pagination
            offset = (page - 1) * page_size

            # Use database-level filtering for optimal performance
            tags_list = [tag] if tag else None
            memories = await self.storage.get_all_memories(
                limit=page_size,
                offset=offset,
                memory_type=memory_type,
                tags=tags_list
            )

            # Get accurate total count for pagination
            total = await self.storage.count_all_memories(
                memory_type=memory_type,
                tags=tags_list
            )

            # Format results for API response
            results = []
            for memory in memories:
                results.append(self._format_memory_response(memory))

            return {
                "memories": results,
                "page": page,
                "page_size": page_size,
                "total": total,
                "has_more": offset + page_size < total
            }

        except Exception as e:
            logger.exception(f"Unexpected error listing memories: {e}")
            return {
                "success": False,
                "error": f"Failed to list memories: {str(e)}",
                "memories": [],
                "page": page,
                "page_size": page_size
            }

    async def store_memory(
        self,
        content: str,
        tags: Union[str, List[str], None] = None,
        memory_type: Optional[str] = None,
        metadata: Optional[Dict[str, Any]] = None,
        client_hostname: Optional[str] = None
    ) -> Union[StoreMemorySingleSuccess, StoreMemoryChunkedSuccess, StoreMemoryFailure]:
        """
        Store a new memory with validation and content processing.

        Accepts tags in multiple formats for maximum flexibility:
        - None → []
        - "tag1,tag2,tag3" → ["tag1", "tag2", "tag3"]
        - "single-tag" → ["single-tag"]
        - ["tag1", "tag2"] → ["tag1", "tag2"]

        Args:
            content: The memory content
            tags: Optional tags for the memory (string, comma-separated string, or list)
            memory_type: Optional memory type classification
            metadata: Optional additional metadata (can also contain tags)
            client_hostname: Optional client hostname for source tagging

        Returns:
            Dictionary with operation result
        """
        try:
            # Normalize tags from parameter (handles all formats)
            final_tags = normalize_tags(tags)

            # Extract and normalize metadata.tags if present
            final_metadata = metadata or {}
            if metadata and "tags" in metadata:
                metadata_tags = normalize_tags(metadata.get("tags"))
                # Merge with parameter tags and remove duplicates
                final_tags = list(set(final_tags + metadata_tags))

            # Apply hostname tagging if provided (for consistent source tracking)
            if client_hostname:
                source_tag = f"source:{client_hostname}"
                if source_tag not in final_tags:
                    final_tags.append(source_tag)
                final_metadata["hostname"] = client_hostname

            # Generate content hash for deduplication
            content_hash = generate_content_hash(content)

            # Process content if auto-splitting is enabled and content exceeds max length
            max_length = self.storage.max_content_length
            if ENABLE_AUTO_SPLIT and max_length and len(content) > max_length:
                # Split content into chunks
                chunks = split_content(
                    content,
                    max_length=max_length,
                    preserve_boundaries=CONTENT_PRESERVE_BOUNDARIES,
                    overlap=CONTENT_SPLIT_OVERLAP
                )
                stored_memories = []

                for i, chunk in enumerate(chunks):
                    chunk_hash = generate_content_hash(chunk)
                    chunk_metadata = final_metadata.copy()
                    chunk_metadata["chunk_index"] = i
                    chunk_metadata["total_chunks"] = len(chunks)
                    chunk_metadata["original_hash"] = content_hash

                    memory = Memory(
                        content=chunk,
                        content_hash=chunk_hash,
                        tags=final_tags,
                        memory_type=memory_type,
                        metadata=chunk_metadata
                    )

                    success, message = await self.storage.store(memory)
                    if success:
                        stored_memories.append(self._format_memory_response(memory))

                return {
                    "success": True,
                    "memories": stored_memories,
                    "total_chunks": len(chunks),
                    "original_hash": content_hash
                }
            else:
                # Store as single memory
                memory = Memory(
                    content=content,
                    content_hash=content_hash,
                    tags=final_tags,
                    memory_type=memory_type,
                    metadata=final_metadata
                )

                success, message = await self.storage.store(memory)

                if success:
                    return {
                        "success": True,
                        "memory": self._format_memory_response(memory)
                    }
                else:
                    return {
                        "success": False,
                        "error": message
                    }

        except ValueError as e:
            # Handle validation errors specifically
            logger.warning(f"Validation error storing memory: {e}")
            return {
                "success": False,
                "error": f"Invalid memory data: {str(e)}"
            }
        except ConnectionError as e:
            # Handle storage connectivity issues
            logger.error(f"Storage connection error: {e}")
            return {
                "success": False,
                "error": f"Storage connection failed: {str(e)}"
            }
        except Exception as e:
            # Handle unexpected errors
            logger.exception(f"Unexpected error storing memory: {e}")
            return {
                "success": False,
                "error": f"Failed to store memory: {str(e)}"
            }

    async def retrieve_memories(
        self,
        query: str,
        n_results: int = 10,
        tags: Optional[List[str]] = None,
        memory_type: Optional[str] = None
    ) -> Union[RetrieveMemoriesSuccess, RetrieveMemoriesError]:
        """
        Retrieve memories by semantic search with optional filtering.

        Args:
            query: Search query string
            n_results: Maximum number of results
            tags: Optional tag filtering
            memory_type: Optional memory type filtering

        Returns:
            Dictionary with search results
        """
        try:
            # Retrieve memories using semantic search
            # Note: storage.retrieve() only supports query and n_results
            # We'll filter by tags/type after retrieval if needed
            memories = await self.storage.retrieve(
                query=query,
                n_results=n_results
            )

            # Apply optional post-filtering
            filtered_memories = memories
            if tags or memory_type:
                filtered_memories = []
                for memory in memories:
                    # Filter by tags if specified
                    if tags:
                        memory_tags = memory.metadata.get('tags', []) if hasattr(memory, 'metadata') else []
                        if not any(tag in memory_tags for tag in tags):
                            continue

                    # Filter by memory_type if specified
                    if memory_type:
                        mem_type = memory.metadata.get('memory_type', '') if hasattr(memory, 'metadata') else ''
                        if mem_type != memory_type:
                            continue

                    filtered_memories.append(memory)

            results = []
            for result in filtered_memories:
                # Extract Memory object from MemoryQueryResult and add similarity score
                memory_dict = self._format_memory_response(result.memory)
                memory_dict['similarity_score'] = result.relevance_score
                results.append(memory_dict)

            return {
                "memories": results,
                "query": query,
                "count": len(results)
            }

        except Exception as e:
            logger.error(f"Error retrieving memories: {e}")
            return {
                "memories": [],
                "query": query,
                "error": f"Failed to retrieve memories: {str(e)}"
            }

    async def search_by_tag(
        self,
        tags: Union[str, List[str]],
        match_all: bool = False
    ) -> Union[SearchByTagSuccess, SearchByTagError]:
        """
        Search memories by tags with flexible matching options.

        Args:
            tags: Tag or list of tags to search for
            match_all: If True, memory must have ALL tags; if False, ANY tag

        Returns:
            Dictionary with matching memories
        """
        try:
            # Normalize tags to list (handles all formats including comma-separated)
            tags = normalize_tags(tags)

            # Search using database-level filtering
            # Note: Using search_by_tag from base class (singular)
            memories = await self.storage.search_by_tag(tags=tags)

            # Format results
            results = []
            for memory in memories:
                results.append(self._format_memory_response(memory))

            # Determine match type description
            match_type = "ALL" if match_all else "ANY"

            return {
                "memories": results,
                "tags": tags,
                "match_type": match_type,
                "count": len(results)
            }

        except Exception as e:
            logger.error(f"Error searching by tags: {e}")
            return {
                "memories": [],
                "tags": tags if isinstance(tags, list) else [tags],
                "error": f"Failed to search by tags: {str(e)}"
            }

    async def get_memory_by_hash(self, content_hash: str) -> Dict[str, Any]:
        """
        Retrieve a specific memory by its content hash using O(1) direct lookup.

        Args:
            content_hash: The content hash of the memory

        Returns:
            Dictionary with memory data or error
        """
        try:
            # Use direct O(1) lookup via storage.get_by_hash()
            memory = await self.storage.get_by_hash(content_hash)

            if memory:
                return {
                    "memory": self._format_memory_response(memory),
                    "found": True
                }
            else:
                return {
                    "found": False,
                    "content_hash": content_hash
                }

        except Exception as e:
            logger.error(f"Error getting memory by hash: {e}")
            return {
                "found": False,
                "content_hash": content_hash,
                "error": f"Failed to get memory: {str(e)}"
            }

    async def delete_memory(self, content_hash: str) -> Union[DeleteMemorySuccess, DeleteMemoryFailure]:
        """
        Delete a memory by its content hash.

        Args:
            content_hash: The content hash of the memory to delete

        Returns:
            Dictionary with operation result
        """
        try:
            success, message = await self.storage.delete(content_hash)
            if success:
                return {
                    "success": True,
                    "content_hash": content_hash
                }
            else:
                return {
                    "success": False,
                    "content_hash": content_hash,
                    "error": message
                }

        except Exception as e:
            logger.error(f"Error deleting memory: {e}")
            return {
                "success": False,
                "content_hash": content_hash,
                "error": f"Failed to delete memory: {str(e)}"
            }

    async def health_check(self) -> Union[HealthCheckSuccess, HealthCheckFailure]:
        """
        Perform a health check on the memory storage system.

        Returns:
            Dictionary with health status and statistics
        """
        try:
            stats = await self.storage.get_stats()
            return {
                "healthy": True,
                "storage_type": stats.get("backend", "unknown"),
                "total_memories": stats.get("total_memories", 0),
                "last_updated": datetime.now().isoformat(),
                **stats
            }

        except Exception as e:
            logger.error(f"Health check failed: {e}")
            return {
                "healthy": False,
                "error": f"Health check failed: {str(e)}"
            }

    def _format_memory_response(self, memory: Memory) -> MemoryResult:
        """
        Format a memory object for API response.

        Args:
            memory: The memory object to format

        Returns:
            Formatted memory dictionary
        """
        return {
            "content": memory.content,
            "content_hash": memory.content_hash,
            "tags": memory.tags,
            "memory_type": memory.memory_type,
            "metadata": memory.metadata,
            "created_at": memory.created_at,
            "updated_at": memory.updated_at,
            "created_at_iso": memory.created_at_iso,
            "updated_at_iso": memory.updated_at_iso
        }

```

--------------------------------------------------------------------------------
/claude-hooks/CONFIGURATION.md:
--------------------------------------------------------------------------------

```markdown
# Memory Hooks Configuration Guide

## Overview

This guide documents all configuration properties for the Claude Code memory awareness hooks, with detailed explanations of their behavior and impact on memory retrieval.

## Configuration Structure

The hooks are configured via `config.json` in the hooks directory. Configuration follows this hierarchy:

1. **Memory Service** - Connection and protocol settings
2. **Project Detection** - How projects are identified
3. **Memory Scoring** - How memories are ranked for relevance
4. **Git Analysis** - Repository context integration
5. **Time Windows** - Temporal scoping for queries
6. **Output** - Display and logging options

---

## Memory Service Connection Configuration

### `memoryService` Object

Controls how the hooks connect to the MCP Memory Service.

```json
"memoryService": {
  "protocol": "auto",
  "preferredProtocol": "http",
  "fallbackEnabled": true,
  "http": {
    "endpoint": "http://127.0.0.1:8889",
    "apiKey": "YOUR_API_KEY_HERE",
    "healthCheckTimeout": 3000,
    "useDetailedHealthCheck": true
  },
  "mcp": {
    "serverCommand": ["uv", "run", "memory", "server", "-s", "hybrid"],
    "serverWorkingDir": "../",
    "connectionTimeout": 2000,
    "toolCallTimeout": 3000
  }
}
```

#### HTTP Configuration

**`endpoint`** (String): URL of the HTTP memory service.

**Security Considerations:**
- **HTTP (`http://`)**: Default for local development. Traffic is **unencrypted** - only use for localhost connections.
- **HTTPS (`https://`)**: Recommended if connecting to remote servers or when encryption-in-transit is required.
  - For self-signed certificates, your system must trust the certificate authority.
  - The hooks enforce certificate validation - `rejectUnauthorized` is always enabled for security.

**`apiKey`** (String): API key for authenticating with the memory service.
- **Default**: Empty string `""` - the application will validate and prompt for a valid key on startup
- **Best practice**: Set via environment variable or secure configuration file
- **Security**: Never commit actual API keys to version control

#### MCP Configuration

**`serverCommand`** (Array): Command to launch the MCP memory service locally.
- Example: `["uv", "run", "memory", "server", "-s", "hybrid"]`
- Adjust storage backend flag (`-s`) as needed: `hybrid`, `cloudflare`, `sqlite_vec`, `chromadb`

**`serverWorkingDir`** (String): Working directory for the MCP server process.
- **Relative paths**: `"../"` assumes hooks are in a subdirectory (e.g., `project/claude-hooks/`)
- **Absolute paths**: Use full path for explicit configuration
- **Environment variables**: Consider using `process.env.MCP_MEMORY_PROJECT_ROOT` for flexibility

**Directory Structure Assumption (for `../` relative path):**
```
project-root/
├── src/                    # MCP Memory Service code
├── claude-hooks/           # This hooks directory
│   ├── config.json
│   └── utilities/
└── pyproject.toml
```

If your structure differs, update `serverWorkingDir` accordingly or use an absolute path.

**`connectionTimeout`** (Number): Milliseconds to wait for MCP server connection (default: 2000).

**`toolCallTimeout`** (Number): Milliseconds to wait for MCP tool call responses (default: 3000).

---

## Memory Scoring Configuration

### `memoryScoring` Object

Controls how memories are scored and ranked for relevance to the current session.

#### `weights` (Object)

Relative importance of different scoring factors. These weights are applied to individual component scores (0.0-1.0 each), then summed together with additive bonuses (typeBonus, recencyBonus). The final score is clamped to [0, 1].

**Note**: Weights don't need to sum to exactly 1.0 since additional bonuses are added separately and the final score is normalized by clamping. The weights shown below sum to 1.00 for the base scoring (without conversation context) or 1.25 when conversation context is enabled.

```json
"weights": {
  "timeDecay": 0.40,           // Recency weight (default: 0.40)
  "tagRelevance": 0.25,        // Tag matching weight (default: 0.25)
  "contentRelevance": 0.15,    // Content keyword weight (default: 0.15)
  "contentQuality": 0.20,      // Quality assessment weight (default: 0.20)
  "conversationRelevance": 0.25 // Conversation context weight (default: 0.25, only when enabled)
}
```

**Property Details:**

- **`timeDecay`** (0.0-1.0, recommended: 0.35-0.45)
  - Weight given to memory age in scoring
  - Higher values prioritize recent memories
  - Lower values allow older, high-quality memories to rank higher
  - **Impact**: At 0.40, a 7-day-old memory with perfect tags can outscore a 60-day-old memory with perfect tags and high quality
  - **Recommendation**: Set to 0.40-0.45 for active development, 0.25-0.35 for research/reference work

- **`tagRelevance`** (0.0-1.0, recommended: 0.20-0.30)
  - Weight given to tag matching with project context
  - Higher values favor well-tagged memories
  - **Impact**: Tags like `projectName`, `language`, `framework` significantly boost scores
  - **Trade-off**: High tag weight can cause old, well-documented memories to dominate over recent work
  - **Recommendation**: Set to 0.25 for balanced tag importance, 0.20 if recency is critical

- **`contentRelevance`** (0.0-1.0, recommended: 0.10-0.20)
  - Weight for keyword matching in memory content
  - Matches against project name, language, frameworks, technical terms
  - **Impact**: Memories mentioning project-specific terms rank higher
  - **Recommendation**: Keep at 0.15 unless doing very keyword-focused work

- **`contentQuality`** (0.0-1.0, recommended: 0.15-0.25)
  - Weight for assessed content quality (length, diversity, meaningful indicators)
  - Penalizes generic session summaries
  - **Impact**: Filters out low-quality auto-generated content
  - **Quality Indicators**: "decided", "implemented", "fixed", "because", "approach", "solution"
  - **Recommendation**: Set to 0.20 to balance quality with other factors

- **`conversationRelevance`** (0.0-1.0, recommended: 0.20-0.30)
  - Weight for matching current conversation topics and intent
  - Only active when conversation context is available
  - **Impact**: Dynamically adjusts based on what user is discussing
  - **Recommendation**: Keep at 0.25 for adaptive context awareness

#### `minRelevanceScore` (Number)

Minimum score threshold for a memory to be included in context.

```json
"minRelevanceScore": 0.4  // Default: 0.4
```

**Details:**
- Range: 0.0 to 1.0
- Memories below this threshold are filtered out entirely
- **Impact on Quality**:
  - `0.3`: Permissive, may include generic old content
  - `0.4`: Balanced, filters most low-quality memories (recommended)
  - `0.5`: Strict, only high-relevance memories
- **Trade-off**: Higher threshold = fewer but higher quality memories

#### `timeDecayRate` (Number)

Rate of exponential decay for time-based scoring.

```json
"timeDecayRate": 0.05  // Default: 0.05
```

**Formula**: `score = e^(-rate * days)`

**Details:**
- Range: 0.01 to 0.2 (practical range)
- Lower rate = gentler decay (memories age slower)
- Higher rate = aggressive decay (memories age faster)

**Decay Examples**:

| Days Old | Rate 0.05 | Rate 0.10 | Rate 0.15 |
|----------|-----------|-----------|-----------|
| 7 days   | 0.70      | 0.50      | 0.35      |
| 14 days  | 0.50      | 0.25      | 0.12      |
| 30 days  | 0.22      | 0.05      | 0.01      |
| 60 days  | 0.05      | 0.002     | ~0        |

**Recommendation**:
- `0.05`: Balanced, keeps 2-4 week memories relevant (recommended)
- `0.10`: Aggressive, prioritizes last 1-2 weeks only
- `0.03`: Gentle, treats 1-2 month memories as still valuable

#### `enableConversationContext` (Boolean)

Whether to use conversation analysis for dynamic memory scoring.

```json
"enableConversationContext": true  // Default: true
```

---

## Git Analysis Configuration

### `gitAnalysis` Object

Controls how git repository context influences memory retrieval.

```json
"gitAnalysis": {
  "enabled": true,
  "commitLookback": 14,
  "maxCommits": 20,
  "includeChangelog": true,
  "maxGitMemories": 3,
  "gitContextWeight": 1.8
}
```

#### `gitContextWeight` (Number)

Multiplier applied to memories derived from git context queries.

**Details:**
- Range: 1.0 to 2.5 (practical range)
- Applied multiplicatively to base memory score
- **Impact Examples**:
  - Base score 0.5 × weight 1.2 = final 0.6
  - Base score 0.5 × weight 1.8 = final 0.9

**Behavior by Value**:
- `1.0`: No boost (git context treated equally)
- `1.2`: Small boost (git-aware memories slightly favored)
- `1.8`: Strong boost (git-aware memories highly prioritized) ✅ **Recommended**
- `2.0+`: Very strong boost (git context dominates)

**Use Cases**:
- **Active development** (`1.8`): Prioritize memories matching recent commits/keywords
- **Maintenance work** (`1.2-1.5`): Balance git context with other signals
- **Research/planning** (`1.0`): Disable git preference

#### Other Git Properties

- **`commitLookback`** (Number, default: 14): Days of git history to analyze
- **`maxCommits`** (Number, default: 20): Maximum commits to process
- **`includeChangelog`** (Boolean, default: true): Parse CHANGELOG.md for context
- **`maxGitMemories`** (Number, default: 3): Max memories from git-context phase

---

## Time Windows Configuration

### Memory Service Time Windows

Controls temporal scoping for memory queries.

```json
"memoryService": {
  "recentTimeWindow": "last-month",      // Default: "last-month"
  "fallbackTimeWindow": "last-3-months"  // Default: "last-3-months"
}
```

#### `recentTimeWindow` (String)

Time window for Phase 1 recent memory queries.

**Supported Values:**
- `"last-day"`: Last 24 hours
- `"last-week"`: Last 7 days
- `"last-2-weeks"`: Last 14 days
- `"last-month"`: Last 30 days ✅ **Recommended**
- `"last-3-months"`: Last 90 days

**Impact:**
- **Narrow window** (`last-week`): Only very recent memories, may miss context during development gaps
- **Balanced window** (`last-month`): Captures recent sprint/iteration cycle
- **Wide window** (`last-3-months`): Includes seasonal patterns, may dilute recency focus

**Recommendation**:
- Active development: `"last-month"`
- Periodic/seasonal work: `"last-3-months"`

#### `fallbackTimeWindow` (String)

Time window for fallback queries when recent memories are insufficient.

**Supported Values:** Same as `recentTimeWindow`

**Purpose:** Ensures minimum context when recent work is sparse.

**Recommendation**: Set 2-3× wider than recent window (e.g., `last-month` → `last-3-months`)

---

## Recency Bonus System

### Automatic Recency Bonuses

The memory scorer applies explicit additive bonuses based on memory age (implemented in `memory-scorer.js`):

```javascript
// Automatic bonuses (no configuration needed)
< 7 days:  +0.15 bonus  // Strong boost for last week
< 14 days: +0.10 bonus  // Moderate boost for last 2 weeks
< 30 days: +0.05 bonus  // Small boost for last month
> 30 days: 0 bonus      // No bonus for older memories
```

**How It Works:**
- Applied **additively** (not multiplicatively) to final score
- Ensures very recent memories get absolute advantage
- Creates clear tier separation (weekly/biweekly/monthly)

**Example Impact:**
```
Memory A (5 days old):
  Base score: 0.50
  Recency bonus: +0.15
  Final score: 0.65

Memory B (60 days old):
  Base score: 0.60 (higher quality/tags)
  Recency bonus: 0
  Final score: 0.60

Result: Recent memory wins despite lower base score
```

**Design Rationale:**
- Compensates for aggressive time decay
- Prevents old, well-tagged memories from dominating
- Aligns with user expectation that recent work is most relevant

---

## Complete Configuration Example

### Optimized for Active Development (Recommended)

```json
{
  "memoryService": {
    "maxMemoriesPerSession": 8,
    "recentFirstMode": true,
    "recentMemoryRatio": 0.6,
    "recentTimeWindow": "last-month",
    "fallbackTimeWindow": "last-3-months"
  },
  "memoryScoring": {
    "weights": {
      "timeDecay": 0.40,
      "tagRelevance": 0.25,
      "contentRelevance": 0.15,
      "contentQuality": 0.20,
      "conversationRelevance": 0.25
    },
    "minRelevanceScore": 0.4,
    "timeDecayRate": 0.05,
    "enableConversationContext": true
  },
  "gitAnalysis": {
    "enabled": true,
    "commitLookback": 14,
    "maxCommits": 20,
    "includeChangelog": true,
    "maxGitMemories": 3,
    "gitContextWeight": 1.8
  }
}
```

### Optimized for Research/Reference Work

```json
{
  "memoryService": {
    "recentTimeWindow": "last-month",
    "fallbackTimeWindow": "last-3-months"
  },
  "memoryScoring": {
    "weights": {
      "timeDecay": 0.25,
      "tagRelevance": 0.35,
      "contentRelevance": 0.20,
      "contentQuality": 0.30,
      "conversationRelevance": 0.20
    },
    "minRelevanceScore": 0.3,
    "timeDecayRate": 0.03
  },
  "gitAnalysis": {
    "gitContextWeight": 1.0
  }
}
```

---

## Tuning Guide

### Problem: Recent work not appearing in context

**Symptoms:**
- Old documentation/decisions dominate
- Recent bug fixes/features missing
- Context feels outdated

**Solutions:**
1. Increase `timeDecay` weight: `0.40` → `0.45`
2. Increase `gitContextWeight`: `1.8` → `2.0`
3. Widen `recentTimeWindow`: `"last-week"` → `"last-month"`
4. Reduce `tagRelevance` weight: `0.25` → `0.20`

### Problem: Too many low-quality memories

**Symptoms:**
- Generic session summaries in context
- Duplicate or trivial information
- Context feels noisy

**Solutions:**
1. Increase `minRelevanceScore`: `0.4` → `0.5`
2. Increase `contentQuality` weight: `0.20` → `0.25`
3. Reduce `maxMemoriesPerSession`: `8` → `5`

### Problem: Missing important old architectural decisions

**Symptoms:**
- Lose context of foundational decisions
- Architectural rationale missing
- Only seeing recent tactical work

**Solutions:**
1. Reduce `timeDecay` weight: `0.40` → `0.30`
2. Increase `tagRelevance` weight: `0.25` → `0.30`
3. Gentler `timeDecayRate`: `0.05` → `0.03`
4. Tag important decisions with `"architecture"`, `"decision"` tags

### Problem: Git context overwhelming other signals

**Symptoms:**
- Only git-keyword memories showing up
- Missing memories that don't match commit messages
- Over-focused on recent commits

**Solutions:**
1. Reduce `gitContextWeight`: `1.8` → `1.4`
2. Reduce `maxGitMemories`: `3` → `2`
3. Disable git analysis temporarily: `"enabled": false`

### Problem: "No relevant memories found" despite healthy database

**Symptoms:**
- Hooks show "No relevant memories found" or "No active connection available"
- HTTP server is running and healthy
- Database contains many memories
- Hook logs show connection failures or wrong endpoint

**Root Causes:**

1. **Port Mismatch**: Config endpoint doesn't match actual HTTP server port
   ```json
   // WRONG - Server runs on 8000, config shows 8889
   "endpoint": "http://127.0.0.1:8889"
   ```

2. **Stale Configuration**: Config not updated after reinstalling hooks or changing server port

**Solutions:**

1. **Verify HTTP server port**:
   ```bash
   # Check what port the server is actually running on
   lsof -i :8000    # Linux/macOS
   netstat -ano | findstr "8000"  # Windows

   # Or check server logs
   systemctl --user status mcp-memory-http.service  # Linux systemd
   ```

2. **Fix endpoint in config** (`~/.claude/hooks/config.json`):
   ```json
   {
     "memoryService": {
       "http": {
         "endpoint": "http://127.0.0.1:8000",  // Match actual server port!
         "apiKey": "your-api-key"
       }
     }
   }
   ```

3. **Test connection manually**:
   ```bash
   curl http://127.0.0.1:8000/api/health
   # Should return: {"status":"healthy","version":"..."}
   ```

4. **Verify configuration is loaded**:
   ```bash
   # Run a hook manually and check the output
   node ~/.claude/hooks/core/session-start.js
   # Look for connection protocol and storage info in output
   ```

**Related Configuration Issues:**

After updating from repository, verify these settings match your preferences:
- `recentTimeWindow`: Repository default is `"last week"` (not `"last 3 days"`)
- `fallbackTimeWindow`: Repository default is `"last 2 weeks"` (not `"last week"`)
- `timeDecay` weight: Repository default is `0.50` (not `0.60`)
- `minRelevanceScore`: Repository default is `0.4` (not `0.25`)
- `commitLookback`: Repository default is `14` days (not `7`)

**See also:** [CLAUDE.md § Configuration Management](../../CLAUDE.md#configuration-management) for complete troubleshooting guide.

---

## Migration from Previous Versions

### v1.0 → v2.0 (Recency Optimization)

**Breaking Changes:**
- `timeDecay` weight increased from `0.25` to `0.40`
- `tagRelevance` weight decreased from `0.35` to `0.25`
- `timeDecayRate` decreased from `0.10` to `0.05`
- `minRelevanceScore` increased from `0.3` to `0.4`
- `gitContextWeight` increased from `1.2` to `1.8`

**Impact:** Recent memories (< 30 days) will rank significantly higher. Adjust weights if you need more historical context.

**Migration Steps:**
1. Backup current `config.json`
2. Update weights to new defaults
3. Test with `test-recency-scoring.js`
4. Fine-tune based on your workflow

---

## Advanced: Scoring Algorithm Details

### Final Score Calculation

```javascript
// Step 1: Calculate base score (weighted sum of components + bonuses)
let baseScore =
  (timeDecayScore * timeDecayWeight) +
  (tagRelevanceScore * tagRelevanceWeight) +
  (contentRelevanceScore * contentRelevanceWeight) +
  (contentQualityScore * contentQualityWeight) +
  typeBonus +
  recencyBonus

// Step 2: Add conversation context if enabled (additive)
if (conversationContextEnabled) {
  baseScore += (conversationRelevanceScore * conversationRelevanceWeight)
}

// Step 3: Apply git context boost (multiplicative - boosts ALL components)
// Note: This multiplies the entire score including conversation relevance
// Implementation: Applied in session-start.js after scoring, not in memory-scorer.js
if (isGitContextMemory) {
  baseScore *= gitContextWeight
}

// Step 4: Apply quality penalty for very low quality (multiplicative)
if (contentQualityScore < 0.2) {
  baseScore *= 0.5
}

// Step 5: Normalize to [0, 1]
finalScore = clamp(baseScore, 0, 1)
```

### Score Component Ranges

- **Time Decay**: 0.01 - 1.0 (exponential decay based on age)
- **Tag Relevance**: 0.1 - 1.0 (0.3 default if no tags)
- **Content Relevance**: 0.1 - 1.0 (0.3 default if no keywords)
- **Content Quality**: 0.05 - 1.0 (0.3 default for normal content)
- **Type Bonus**: -0.1 - 0.3 (based on memory type)
- **Recency Bonus**: 0 - 0.15 (tiered based on age)

### Type Bonuses

```javascript
{
  'decision': 0.3,      // Architectural decisions
  'architecture': 0.3,  // Architecture docs
  'reference': 0.2,     // Reference materials
  'session': 0.15,      // Session summaries
  'insight': 0.2,       // Insights
  'bug-fix': 0.15,      // Bug fixes
  'feature': 0.1,       // Feature descriptions
  'note': 0.05,         // General notes
  'temporary': -0.1     // Temporary notes (penalized)
}
```

---

## Testing Configuration Changes

Use the included test script to validate your configuration:

```bash
cd /path/to/claude-hooks
node test-recency-scoring.js
```

This will show:
- Time decay calculations for different ages
- Recency bonus application
- Final scoring with your config weights
- Ranking of test memories

Expected output should show recent memories (< 7 days) in top 3 positions.

---

## See Also

- [README.md](./README.md) - General hooks documentation
- [MIGRATION.md](./MIGRATION.md) - Migration guides
- [README-NATURAL-TRIGGERS.md](./README-NATURAL-TRIGGERS.md) - Natural triggers documentation

```

--------------------------------------------------------------------------------
/claude-hooks/test-natural-triggers.js:
--------------------------------------------------------------------------------

```javascript
#!/usr/bin/env node

/**
 * Comprehensive Test Suite for Natural Memory Triggers
 * Tests performance-aware conversation monitoring and pattern detection
 */

const { TieredConversationMonitor } = require('./utilities/tiered-conversation-monitor');
const { AdaptivePatternDetector } = require('./utilities/adaptive-pattern-detector');
const { PerformanceManager } = require('./utilities/performance-manager');
const { MidConversationHook } = require('./core/mid-conversation');

class NaturalTriggersTestSuite {
    constructor() {
        this.testResults = [];
        this.performanceMetrics = [];
    }

    /**
     * Run all tests
     */
    async runAllTests() {
        console.log('🧪 Natural Memory Triggers - Comprehensive Test Suite');
        console.log('═'.repeat(60));

        // Test categories
        const testCategories = [
            { name: 'Performance Management', tests: this.performanceTests },
            { name: 'Pattern Detection', tests: this.patternDetectionTests },
            { name: 'Conversation Monitoring', tests: this.conversationMonitorTests },
            { name: 'Integration Tests', tests: this.integrationTests },
            { name: 'Performance Profiles', tests: this.performanceProfileTests }
        ];

        for (const category of testCategories) {
            console.log(`\n📂 ${category.name}`);
            console.log('─'.repeat(40));

            await category.tests.call(this);
        }

        // Summary
        this.printTestSummary();
        return this.testResults;
    }

    /**
     * Performance Management Tests
     */
    async performanceTests() {
        // Test 1: Performance Manager Initialization
        await this.runTest('Performance Manager Initialization', async () => {
            const perfManager = new PerformanceManager({
                defaultProfile: 'balanced'
            });

            this.assert(perfManager.activeProfile === 'balanced', 'Should initialize with correct profile');
            this.assert(perfManager.performanceBudget.maxLatency === 200, 'Should have correct latency budget');
            return { perfManager };
        });

        // Test 2: Timing Operations
        await this.runTest('Timing Operations', async () => {
            const perfManager = new PerformanceManager();

            const timing = perfManager.startTiming('test_operation', 'fast');
            await this.sleep(50);
            const result = perfManager.endTiming(timing);

            // Test performance tracking functionality without relying on exact timing
            this.assert(typeof result.latency === 'number' && result.latency >= 0, 'Should record numeric latency');
            this.assert(result.latency > 10, 'Should record reasonable latency for 50ms operation');
            this.assert(result.tier === 'fast', 'Should record correct tier');
            this.assert(typeof result.withinBudget === 'boolean', 'Should determine budget compliance');
        });

        // Test 3: Profile Switching
        await this.runTest('Profile Switching', async () => {
            const perfManager = new PerformanceManager();

            const originalProfile = perfManager.activeProfile;
            perfManager.switchProfile('speed_focused');

            this.assert(perfManager.activeProfile === 'speed_focused', 'Should switch to speed focused profile');
            this.assert(perfManager.performanceBudget.maxLatency === 100, 'Should update latency budget');

            perfManager.switchProfile(originalProfile); // Reset
        });

        // Test 4: Adaptive Learning
        await this.runTest('Adaptive Learning', async () => {
            const perfManager = new PerformanceManager();

            // Simulate positive feedback
            perfManager.recordUserFeedback(true, { latency: 300 });
            perfManager.recordUserFeedback(true, { latency: 350 });

            // User tolerance should increase
            const toleranceBefore = perfManager.userPreferences.toleranceLevel;
            perfManager.recordUserFeedback(true, { latency: 400 });
            const toleranceAfter = perfManager.userPreferences.toleranceLevel;

            this.assert(toleranceAfter >= toleranceBefore, 'User tolerance should increase with positive feedback');
        });
    }

    /**
     * Pattern Detection Tests
     */
    async patternDetectionTests() {
        // Test 1: Explicit Memory Requests
        await this.runTest('Explicit Memory Request Detection', async () => {
            const detector = new AdaptivePatternDetector({
                sensitivity: 0.7,
                adaptiveLearning: false // Disable learning for consistent tests
            });

            const testCases = [
                { message: "What did we decide about the authentication approach?", shouldTrigger: true },
                { message: "Remind me how we handled user sessions", shouldTrigger: true },
                { message: "Remember when we discussed the database schema?", shouldTrigger: true },
                { message: "Just implementing a new feature", shouldTrigger: false }
            ];

            for (const testCase of testCases) {
                const result = await detector.detectPatterns(testCase.message);
                const actualTrigger = result.triggerRecommendation;

                // Debug output for failing tests
                if (actualTrigger !== testCase.shouldTrigger) {
                    console.log(`\nDEBUG: "${testCase.message}"`);
                    console.log(`Expected: ${testCase.shouldTrigger}, Got: ${actualTrigger}`);
                    console.log(`Confidence: ${result.confidence}, Matches: ${result.matches.length}`);
                    result.matches.forEach(m => console.log(`  - ${m.category}: ${m.confidence}`));
                }

                this.assert(actualTrigger === testCase.shouldTrigger,
                    `"${testCase.message}" should ${testCase.shouldTrigger ? '' : 'not '}trigger (got ${actualTrigger})`);
            }
        });

        // Test 2: Technical Discussion Patterns
        await this.runTest('Technical Discussion Detection', async () => {
            const detector = new AdaptivePatternDetector({ sensitivity: 0.6 });

            const technicalMessages = [
                "Let's discuss the authentication architecture",
                "What's our approach to database migrations?",
                "How should we implement the security layer?"
            ];

            for (const message of technicalMessages) {
                const result = await detector.detectPatterns(message, {
                    projectContext: { name: 'test-project', language: 'JavaScript' }
                });

                this.assert(result.matches.length > 0, `Technical message should have pattern matches: "${message}"`);
                this.assert(result.confidence > 0.2, `Technical message should have reasonable confidence: ${result.confidence} for "${message}"`);
            }
        });

        // Test 3: Sensitivity Adjustment
        await this.runTest('Sensitivity Adjustment', async () => {
            const lowSensitivity = new AdaptivePatternDetector({ sensitivity: 0.3 });
            const highSensitivity = new AdaptivePatternDetector({ sensitivity: 0.9 });

            const ambiguousMessage = "How do we handle this?";

            const lowResult = await lowSensitivity.detectPatterns(ambiguousMessage);
            const highResult = await highSensitivity.detectPatterns(ambiguousMessage);

            this.assert(highResult.confidence >= lowResult.confidence,
                'Higher sensitivity should yield higher confidence for ambiguous messages');
        });

        // Test 4: Learning from Feedback
        await this.runTest('Learning from Feedback', async () => {
            const detector = new AdaptivePatternDetector({ sensitivity: 0.7, adaptiveLearning: true });

            const message = "What's our standard approach?";
            const initialResult = await detector.detectPatterns(message);
            const initialConfidence = initialResult.confidence;

            // Provide positive feedback multiple times
            for (let i = 0; i < 5; i++) {
                detector.recordUserFeedback(true, initialResult);
            }

            const learnedResult = await detector.detectPatterns(message);

            // Note: In a real implementation, this might increase confidence for similar patterns
            // For now, we just verify the feedback was recorded
            const stats = detector.getStatistics();
            this.assert(stats.positiveRate > 0, 'Should record positive feedback');
        });
    }

    /**
     * Conversation Monitoring Tests
     */
    async conversationMonitorTests() {
        // Test 1: Topic Extraction
        await this.runTest('Topic Extraction', async () => {
            const monitor = new TieredConversationMonitor({
                contextWindow: 5
            });

            const technicalMessage = "Let's implement authentication using OAuth and JWT tokens for our React application";
            const analysis = await monitor.analyzeMessage(technicalMessage);

            this.assert(analysis.topics.length > 0, 'Should extract topics from technical message');
            this.assert(analysis.confidence > 0.4, `Should have reasonable confidence: ${analysis.confidence}`);
            this.assert(analysis.processingTier !== 'none', 'Should process with some tier');
        });

        // Test 2: Semantic Shift Detection
        await this.runTest('Semantic Shift Detection', async () => {
            const monitor = new TieredConversationMonitor();

            // First message establishes context
            await monitor.analyzeMessage("Working on React components and state management");

            // Second message on same topic
            const sameTopicResult = await monitor.analyzeMessage("Adding more React hooks to the component");

            // Third message on different topic
            const differentTopicResult = await monitor.analyzeMessage("Let's switch to database schema design");

            this.assert(differentTopicResult.semanticShift > sameTopicResult.semanticShift,
                'Topic change should register higher semantic shift');
        });

        // Test 3: Performance Tier Selection
        await this.runTest('Performance Tier Selection', async () => {
            const perfManager = new PerformanceManager({ defaultProfile: 'speed_focused' });
            const monitor = new TieredConversationMonitor({}, perfManager);

            const message = "Simple question about React";
            const analysis = await monitor.analyzeMessage(message);

            // In speed_focused mode, should prefer instant tier
            this.assert(analysis.processingTier === 'instant' || analysis.processingTier === 'fast',
                `Speed focused mode should use fast tiers, got: ${analysis.processingTier}`);
        });

        // Test 4: Caching Behavior
        await this.runTest('Caching Behavior', async () => {
            const monitor = new TieredConversationMonitor({
                enableCaching: true
            });

            const message = "What is React?";

            // First analysis
            const start1 = Date.now();
            const result1 = await monitor.analyzeMessage(message);
            const time1 = Date.now() - start1;

            // Second analysis (should use cache)
            const start2 = Date.now();
            const result2 = await monitor.analyzeMessage(message);
            const time2 = Date.now() - start2;

            // Check that both results have reasonable confidence values
            this.assert(typeof result1.confidence === 'number', 'First result should have confidence');
            this.assert(typeof result2.confidence === 'number', 'Second result should have confidence');
            // Note: Processing tiers may vary due to performance-based decisions, which is expected behavior
            this.assert(result1.processingTier && result2.processingTier, 'Both results should have processing tiers');
            // Note: Due to timestamps and context changes, exact confidence equality might vary
        });
    }

    /**
     * Integration Tests
     */
    async integrationTests() {
        // Test 1: Full Mid-Conversation Hook
        await this.runTest('Full Mid-Conversation Hook Analysis', async () => {
            const hook = new MidConversationHook({
                enabled: true,
                triggerThreshold: 0.6,
                maxMemoriesPerTrigger: 3,
                performance: { defaultProfile: 'balanced' }
            });

            const context = {
                userMessage: "What did we decide about the authentication strategy?",
                projectContext: {
                    name: 'test-project',
                    language: 'JavaScript',
                    frameworks: ['React']
                }
            };

            const result = await hook.analyzeMessage(context.userMessage, context);

            this.assert(result !== null, 'Should return analysis result');
            this.assert(typeof result.confidence === 'number', 'Should include confidence score');
            this.assert(typeof result.shouldTrigger === 'boolean', 'Should include trigger decision');
            this.assert(result.reasoning, 'Should include reasoning for decision');

            await hook.cleanup();
        });

        // Test 2: Performance Budget Compliance
        await this.runTest('Performance Budget Compliance', async () => {
            const hook = new MidConversationHook({
                performance: { defaultProfile: 'speed_focused' }
            });

            const start = Date.now();
            const result = await hook.analyzeMessage("Quick question about React hooks");
            const elapsed = Date.now() - start;

            // Speed focused should complete and return results
            this.assert(result !== null, `Speed focused mode should return analysis result`);
            console.log(`[Test] Speed focused analysis completed in ${elapsed}ms`);

            await hook.cleanup();
        });

        // Test 3: Cooldown Period
        await this.runTest('Cooldown Period Enforcement', async () => {
            const hook = new MidConversationHook({
                cooldownPeriod: 1000, // 1 second
                triggerThreshold: 0.5
            });

            const message = "What did we decide about authentication?";

            // First trigger
            const result1 = await hook.analyzeMessage(message);

            // Immediate second attempt (should be in cooldown)
            const result2 = await hook.analyzeMessage(message);

            if (result1.shouldTrigger) {
                this.assert(result2.reasoning?.includes('cooldown') || !result2.shouldTrigger,
                    'Should respect cooldown period');
            }

            await hook.cleanup();
        });
    }

    /**
     * Performance Profile Tests
     */
    async performanceProfileTests() {
        // Test 1: Profile Configuration Loading
        await this.runTest('Performance Profile Loading', async () => {
            const profiles = ['speed_focused', 'balanced', 'memory_aware', 'adaptive'];

            for (const profileName of profiles) {
                const perfManager = new PerformanceManager({ defaultProfile: profileName });

                this.assert(perfManager.activeProfile === profileName,
                    `Should load ${profileName} profile correctly`);

                const budget = perfManager.performanceBudget;
                this.assert(budget !== null, `${profileName} should have performance budget`);

                if (profileName !== 'adaptive') {
                    this.assert(typeof budget.maxLatency === 'number',
                        `${profileName} should have numeric maxLatency`);
                }
            }
        });

        // Test 2: Tier Enabling/Disabling
        await this.runTest('Tier Configuration', async () => {
            const speedFocused = new PerformanceManager({ defaultProfile: 'speed_focused' });
            const memoryAware = new PerformanceManager({ defaultProfile: 'memory_aware' });

            // Speed focused should have fewer enabled tiers
            const speedTiers = speedFocused.performanceBudget.enabledTiers || [];
            const memoryTiers = memoryAware.performanceBudget.enabledTiers || [];

            this.assert(speedTiers.length <= memoryTiers.length,
                'Speed focused should have fewer or equal enabled tiers');

            this.assert(speedTiers.includes('instant'),
                'Speed focused should at least include instant tier');
        });

        // Test 3: Adaptive Profile Behavior
        await this.runTest('Adaptive Profile Behavior', async () => {
            const adaptive = new PerformanceManager({ defaultProfile: 'adaptive' });

            // Simulate performance history
            for (let i = 0; i < 20; i++) {
                adaptive.recordTotalLatency(150); // Consistent good performance
            }

            // Check if adaptive calculation makes sense
            const budget = adaptive.getProfileBudget('adaptive');
            this.assert(budget.autoAdjust === true, 'Adaptive profile should have autoAdjust enabled');
        });
    }

    /**
     * Utility Methods
     */

    async runTest(testName, testFunction) {
        try {
            console.log(`  🧪 ${testName}...`);
            const start = Date.now();

            const result = await testFunction();

            const duration = Date.now() - start;
            this.performanceMetrics.push({ testName, duration });

            console.log(`  ✅ ${testName} (${duration}ms)`);
            this.testResults.push({ name: testName, status: 'passed', duration });

            return result;

        } catch (error) {
            console.log(`  ❌ ${testName}: ${error.message}`);
            this.testResults.push({ name: testName, status: 'failed', error: error.message });
            throw error; // Re-throw to stop execution if needed
        }
    }

    assert(condition, message) {
        if (!condition) {
            throw new Error(`Assertion failed: ${message}`);
        }
    }

    async sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    printTestSummary() {
        console.log('\n📊 Test Summary');
        console.log('═'.repeat(50));

        const passed = this.testResults.filter(r => r.status === 'passed').length;
        const failed = this.testResults.filter(r => r.status === 'failed').length;
        const total = this.testResults.length;

        console.log(`Total Tests: ${total}`);
        console.log(`Passed: ${passed} ✅`);
        console.log(`Failed: ${failed} ${failed > 0 ? '❌' : ''}`);
        console.log(`Success Rate: ${((passed / total) * 100).toFixed(1)}%`);

        // Performance summary
        const totalTime = this.performanceMetrics.reduce((sum, m) => sum + m.duration, 0);
        const avgTime = totalTime / this.performanceMetrics.length;

        console.log(`\n⚡ Performance`);
        console.log(`Total Time: ${totalTime}ms`);
        console.log(`Average per Test: ${avgTime.toFixed(1)}ms`);

        if (failed > 0) {
            console.log('\n❌ Failed Tests:');
            this.testResults
                .filter(r => r.status === 'failed')
                .forEach(r => console.log(`  • ${r.name}: ${r.error}`));
        }
    }
}

/**
 * Run tests if called directly
 */
if (require.main === module) {
    const suite = new NaturalTriggersTestSuite();

    suite.runAllTests()
        .then(results => {
            const failed = results.filter(r => r.status === 'failed').length;
            process.exit(failed > 0 ? 1 : 0);
        })
        .catch(error => {
            console.error('❌ Test suite failed:', error.message);
            process.exit(1);
        });
}

module.exports = { NaturalTriggersTestSuite };
```
Page 22/35FirstPrevNextLast