superlocalmemory 2.8.6 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -1
- package/NOTICE +63 -0
- package/README.md +165 -480
- package/bin/slm +17 -449
- package/bin/slm-npm +62 -48
- package/conftest.py +5 -0
- package/docs/api-reference.md +284 -0
- package/docs/architecture.md +149 -0
- package/docs/auto-memory.md +150 -0
- package/docs/cli-reference.md +276 -0
- package/docs/compliance.md +191 -0
- package/docs/configuration.md +182 -0
- package/docs/getting-started.md +102 -0
- package/docs/ide-setup.md +261 -0
- package/docs/mcp-tools.md +220 -0
- package/docs/migration-from-v2.md +170 -0
- package/docs/profiles.md +173 -0
- package/docs/troubleshooting.md +310 -0
- package/{configs → ide/configs}/antigravity-mcp.json +3 -3
- package/ide/configs/chatgpt-desktop-mcp.json +16 -0
- package/{configs → ide/configs}/claude-desktop-mcp.json +3 -3
- package/{configs → ide/configs}/codex-mcp.toml +4 -4
- package/{configs → ide/configs}/continue-mcp.yaml +4 -3
- package/{configs → ide/configs}/continue-skills.yaml +6 -6
- package/ide/configs/cursor-mcp.json +15 -0
- package/{configs → ide/configs}/gemini-cli-mcp.json +2 -2
- package/{configs → ide/configs}/jetbrains-mcp.json +2 -2
- package/{configs → ide/configs}/opencode-mcp.json +2 -2
- package/{configs → ide/configs}/perplexity-mcp.json +2 -2
- package/{configs → ide/configs}/vscode-copilot-mcp.json +2 -2
- package/{configs → ide/configs}/windsurf-mcp.json +3 -3
- package/{configs → ide/configs}/zed-mcp.json +2 -2
- package/{hooks → ide/hooks}/context-hook.js +9 -20
- package/ide/hooks/memory-list-skill.js +70 -0
- package/ide/hooks/memory-profile-skill.js +101 -0
- package/ide/hooks/memory-recall-skill.js +62 -0
- package/ide/hooks/memory-remember-skill.js +68 -0
- package/ide/hooks/memory-reset-skill.js +160 -0
- package/{hooks → ide/hooks}/post-recall-hook.js +2 -2
- package/ide/integrations/langchain/README.md +106 -0
- package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +9 -0
- package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +201 -0
- package/ide/integrations/langchain/pyproject.toml +38 -0
- package/{src/learning → ide/integrations/langchain}/tests/__init__.py +1 -0
- package/ide/integrations/langchain/tests/test_chat_message_history.py +215 -0
- package/ide/integrations/langchain/tests/test_security.py +117 -0
- package/ide/integrations/llamaindex/README.md +81 -0
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +9 -0
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +316 -0
- package/ide/integrations/llamaindex/pyproject.toml +43 -0
- package/{src/lifecycle → ide/integrations/llamaindex}/tests/__init__.py +1 -2
- package/ide/integrations/llamaindex/tests/test_chat_store.py +294 -0
- package/ide/integrations/llamaindex/tests/test_security.py +241 -0
- package/{skills → ide/skills}/slm-build-graph/SKILL.md +6 -6
- package/{skills → ide/skills}/slm-list-recent/SKILL.md +5 -5
- package/{skills → ide/skills}/slm-recall/SKILL.md +5 -5
- package/{skills → ide/skills}/slm-remember/SKILL.md +6 -6
- package/{skills → ide/skills}/slm-show-patterns/SKILL.md +7 -7
- package/{skills → ide/skills}/slm-status/SKILL.md +9 -9
- package/{skills → ide/skills}/slm-switch-profile/SKILL.md +9 -9
- package/package.json +13 -22
- package/pyproject.toml +85 -0
- package/scripts/build-dmg.sh +417 -0
- package/scripts/install-skills.ps1 +334 -0
- package/scripts/postinstall.js +2 -2
- package/scripts/start-dashboard.ps1 +52 -0
- package/scripts/start-dashboard.sh +41 -0
- package/scripts/sync-wiki.ps1 +127 -0
- package/scripts/sync-wiki.sh +82 -0
- package/scripts/test-dmg.sh +161 -0
- package/scripts/test-npm-package.ps1 +252 -0
- package/scripts/test-npm-package.sh +207 -0
- package/scripts/verify-install.ps1 +294 -0
- package/scripts/verify-install.sh +266 -0
- package/src/superlocalmemory/__init__.py +0 -0
- package/src/superlocalmemory/attribution/__init__.py +9 -0
- package/src/superlocalmemory/attribution/mathematical_dna.py +235 -0
- package/src/superlocalmemory/attribution/signer.py +153 -0
- package/src/superlocalmemory/attribution/watermark.py +189 -0
- package/src/superlocalmemory/cli/__init__.py +5 -0
- package/src/superlocalmemory/cli/commands.py +245 -0
- package/src/superlocalmemory/cli/main.py +89 -0
- package/src/superlocalmemory/cli/migrate_cmd.py +55 -0
- package/src/superlocalmemory/cli/post_install.py +99 -0
- package/src/superlocalmemory/cli/setup_wizard.py +129 -0
- package/src/superlocalmemory/compliance/__init__.py +0 -0
- package/src/superlocalmemory/compliance/abac.py +204 -0
- package/src/superlocalmemory/compliance/audit.py +314 -0
- package/src/superlocalmemory/compliance/eu_ai_act.py +131 -0
- package/src/superlocalmemory/compliance/gdpr.py +294 -0
- package/src/superlocalmemory/compliance/lifecycle.py +158 -0
- package/src/superlocalmemory/compliance/retention.py +232 -0
- package/src/superlocalmemory/compliance/scheduler.py +148 -0
- package/src/superlocalmemory/core/__init__.py +0 -0
- package/src/superlocalmemory/core/config.py +391 -0
- package/src/superlocalmemory/core/embeddings.py +293 -0
- package/src/superlocalmemory/core/engine.py +701 -0
- package/src/superlocalmemory/core/hooks.py +65 -0
- package/src/superlocalmemory/core/maintenance.py +172 -0
- package/src/superlocalmemory/core/modes.py +140 -0
- package/src/superlocalmemory/core/profiles.py +234 -0
- package/src/superlocalmemory/core/registry.py +117 -0
- package/src/superlocalmemory/dynamics/__init__.py +0 -0
- package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +223 -0
- package/src/superlocalmemory/encoding/__init__.py +0 -0
- package/src/superlocalmemory/encoding/consolidator.py +485 -0
- package/src/superlocalmemory/encoding/emotional.py +125 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +525 -0
- package/src/superlocalmemory/encoding/entropy_gate.py +104 -0
- package/src/superlocalmemory/encoding/fact_extractor.py +775 -0
- package/src/superlocalmemory/encoding/foresight.py +91 -0
- package/src/superlocalmemory/encoding/graph_builder.py +302 -0
- package/src/superlocalmemory/encoding/observation_builder.py +160 -0
- package/src/superlocalmemory/encoding/scene_builder.py +183 -0
- package/src/superlocalmemory/encoding/signal_inference.py +90 -0
- package/src/superlocalmemory/encoding/temporal_parser.py +426 -0
- package/src/superlocalmemory/encoding/type_router.py +235 -0
- package/src/superlocalmemory/hooks/__init__.py +3 -0
- package/src/superlocalmemory/hooks/auto_capture.py +111 -0
- package/src/superlocalmemory/hooks/auto_recall.py +93 -0
- package/src/superlocalmemory/hooks/ide_connector.py +204 -0
- package/src/superlocalmemory/hooks/rules_engine.py +99 -0
- package/src/superlocalmemory/infra/__init__.py +3 -0
- package/src/superlocalmemory/infra/auth_middleware.py +82 -0
- package/src/superlocalmemory/infra/backup.py +317 -0
- package/src/superlocalmemory/infra/cache_manager.py +267 -0
- package/src/superlocalmemory/infra/event_bus.py +381 -0
- package/src/superlocalmemory/infra/rate_limiter.py +135 -0
- package/src/{webhook_dispatcher.py → superlocalmemory/infra/webhook_dispatcher.py} +104 -101
- package/src/superlocalmemory/learning/__init__.py +0 -0
- package/src/superlocalmemory/learning/adaptive.py +172 -0
- package/src/superlocalmemory/learning/behavioral.py +490 -0
- package/src/superlocalmemory/learning/behavioral_listener.py +94 -0
- package/src/superlocalmemory/learning/bootstrap.py +298 -0
- package/src/superlocalmemory/learning/cross_project.py +399 -0
- package/src/superlocalmemory/learning/database.py +376 -0
- package/src/superlocalmemory/learning/engagement.py +323 -0
- package/src/superlocalmemory/learning/features.py +138 -0
- package/src/superlocalmemory/learning/feedback.py +316 -0
- package/src/superlocalmemory/learning/outcomes.py +255 -0
- package/src/superlocalmemory/learning/project_context.py +366 -0
- package/src/superlocalmemory/learning/ranker.py +155 -0
- package/src/superlocalmemory/learning/source_quality.py +303 -0
- package/src/superlocalmemory/learning/workflows.py +309 -0
- package/src/superlocalmemory/llm/__init__.py +0 -0
- package/src/superlocalmemory/llm/backbone.py +316 -0
- package/src/superlocalmemory/math/__init__.py +0 -0
- package/src/superlocalmemory/math/fisher.py +356 -0
- package/src/superlocalmemory/math/langevin.py +398 -0
- package/src/superlocalmemory/math/sheaf.py +257 -0
- package/src/superlocalmemory/mcp/__init__.py +0 -0
- package/src/superlocalmemory/mcp/resources.py +245 -0
- package/src/superlocalmemory/mcp/server.py +61 -0
- package/src/superlocalmemory/mcp/tools.py +18 -0
- package/src/superlocalmemory/mcp/tools_core.py +305 -0
- package/src/superlocalmemory/mcp/tools_v28.py +223 -0
- package/src/superlocalmemory/mcp/tools_v3.py +286 -0
- package/src/superlocalmemory/retrieval/__init__.py +0 -0
- package/src/superlocalmemory/retrieval/agentic.py +295 -0
- package/src/superlocalmemory/retrieval/ann_index.py +223 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +185 -0
- package/src/superlocalmemory/retrieval/bridge_discovery.py +170 -0
- package/src/superlocalmemory/retrieval/engine.py +390 -0
- package/src/superlocalmemory/retrieval/entity_channel.py +179 -0
- package/src/superlocalmemory/retrieval/fusion.py +78 -0
- package/src/superlocalmemory/retrieval/profile_channel.py +105 -0
- package/src/superlocalmemory/retrieval/reranker.py +154 -0
- package/src/superlocalmemory/retrieval/semantic_channel.py +232 -0
- package/src/superlocalmemory/retrieval/strategy.py +96 -0
- package/src/superlocalmemory/retrieval/temporal_channel.py +175 -0
- package/src/superlocalmemory/server/__init__.py +1 -0
- package/src/superlocalmemory/server/api.py +248 -0
- package/src/superlocalmemory/server/routes/__init__.py +4 -0
- package/src/superlocalmemory/server/routes/agents.py +107 -0
- package/src/superlocalmemory/server/routes/backup.py +91 -0
- package/src/superlocalmemory/server/routes/behavioral.py +127 -0
- package/src/superlocalmemory/server/routes/compliance.py +160 -0
- package/src/superlocalmemory/server/routes/data_io.py +188 -0
- package/src/superlocalmemory/server/routes/events.py +183 -0
- package/src/superlocalmemory/server/routes/helpers.py +85 -0
- package/src/superlocalmemory/server/routes/learning.py +273 -0
- package/src/superlocalmemory/server/routes/lifecycle.py +116 -0
- package/src/superlocalmemory/server/routes/memories.py +399 -0
- package/src/superlocalmemory/server/routes/profiles.py +219 -0
- package/src/superlocalmemory/server/routes/stats.py +346 -0
- package/src/superlocalmemory/server/routes/v3_api.py +365 -0
- package/src/superlocalmemory/server/routes/ws.py +82 -0
- package/src/superlocalmemory/server/security_middleware.py +57 -0
- package/src/superlocalmemory/server/ui.py +245 -0
- package/src/superlocalmemory/storage/__init__.py +0 -0
- package/src/superlocalmemory/storage/access_control.py +182 -0
- package/src/superlocalmemory/storage/database.py +594 -0
- package/src/superlocalmemory/storage/migrations.py +303 -0
- package/src/superlocalmemory/storage/models.py +406 -0
- package/src/superlocalmemory/storage/schema.py +726 -0
- package/src/superlocalmemory/storage/v2_migrator.py +317 -0
- package/src/superlocalmemory/trust/__init__.py +0 -0
- package/src/superlocalmemory/trust/gate.py +130 -0
- package/src/superlocalmemory/trust/provenance.py +124 -0
- package/src/superlocalmemory/trust/scorer.py +347 -0
- package/src/superlocalmemory/trust/signals.py +153 -0
- package/ui/index.html +278 -5
- package/ui/js/auto-settings.js +70 -0
- package/ui/js/dashboard.js +90 -0
- package/ui/js/fact-detail.js +92 -0
- package/ui/js/feedback.js +2 -2
- package/ui/js/ide-status.js +102 -0
- package/ui/js/math-health.js +98 -0
- package/ui/js/recall-lab.js +127 -0
- package/ui/js/settings.js +2 -2
- package/ui/js/trust-dashboard.js +73 -0
- package/api_server.py +0 -724
- package/bin/aider-smart +0 -72
- package/bin/superlocalmemoryv2-learning +0 -4
- package/bin/superlocalmemoryv2-list +0 -3
- package/bin/superlocalmemoryv2-patterns +0 -4
- package/bin/superlocalmemoryv2-profile +0 -3
- package/bin/superlocalmemoryv2-recall +0 -3
- package/bin/superlocalmemoryv2-remember +0 -3
- package/bin/superlocalmemoryv2-reset +0 -3
- package/bin/superlocalmemoryv2-status +0 -3
- package/configs/chatgpt-desktop-mcp.json +0 -16
- package/configs/cursor-mcp.json +0 -15
- package/hooks/memory-list-skill.js +0 -139
- package/hooks/memory-profile-skill.js +0 -273
- package/hooks/memory-recall-skill.js +0 -114
- package/hooks/memory-remember-skill.js +0 -127
- package/hooks/memory-reset-skill.js +0 -274
- package/mcp_server.py +0 -1808
- package/requirements-core.txt +0 -22
- package/requirements-learning.txt +0 -12
- package/requirements.txt +0 -12
- package/src/agent_registry.py +0 -411
- package/src/auth_middleware.py +0 -61
- package/src/auto_backup.py +0 -459
- package/src/behavioral/__init__.py +0 -49
- package/src/behavioral/behavioral_listener.py +0 -203
- package/src/behavioral/behavioral_patterns.py +0 -275
- package/src/behavioral/cross_project_transfer.py +0 -206
- package/src/behavioral/outcome_inference.py +0 -194
- package/src/behavioral/outcome_tracker.py +0 -193
- package/src/behavioral/tests/__init__.py +0 -4
- package/src/behavioral/tests/test_behavioral_integration.py +0 -108
- package/src/behavioral/tests/test_behavioral_patterns.py +0 -150
- package/src/behavioral/tests/test_cross_project_transfer.py +0 -142
- package/src/behavioral/tests/test_mcp_behavioral.py +0 -139
- package/src/behavioral/tests/test_mcp_report_outcome.py +0 -117
- package/src/behavioral/tests/test_outcome_inference.py +0 -107
- package/src/behavioral/tests/test_outcome_tracker.py +0 -96
- package/src/cache_manager.py +0 -518
- package/src/compliance/__init__.py +0 -48
- package/src/compliance/abac_engine.py +0 -149
- package/src/compliance/abac_middleware.py +0 -116
- package/src/compliance/audit_db.py +0 -215
- package/src/compliance/audit_logger.py +0 -148
- package/src/compliance/retention_manager.py +0 -289
- package/src/compliance/retention_scheduler.py +0 -186
- package/src/compliance/tests/__init__.py +0 -4
- package/src/compliance/tests/test_abac_enforcement.py +0 -95
- package/src/compliance/tests/test_abac_engine.py +0 -124
- package/src/compliance/tests/test_abac_mcp_integration.py +0 -118
- package/src/compliance/tests/test_audit_db.py +0 -123
- package/src/compliance/tests/test_audit_logger.py +0 -98
- package/src/compliance/tests/test_mcp_audit.py +0 -128
- package/src/compliance/tests/test_mcp_retention_policy.py +0 -125
- package/src/compliance/tests/test_retention_manager.py +0 -131
- package/src/compliance/tests/test_retention_scheduler.py +0 -99
- package/src/compression/__init__.py +0 -25
- package/src/compression/cli.py +0 -150
- package/src/compression/cold_storage.py +0 -217
- package/src/compression/config.py +0 -72
- package/src/compression/orchestrator.py +0 -133
- package/src/compression/tier2_compressor.py +0 -228
- package/src/compression/tier3_compressor.py +0 -153
- package/src/compression/tier_classifier.py +0 -148
- package/src/db_connection_manager.py +0 -536
- package/src/embedding_engine.py +0 -63
- package/src/embeddings/__init__.py +0 -47
- package/src/embeddings/cache.py +0 -70
- package/src/embeddings/cli.py +0 -113
- package/src/embeddings/constants.py +0 -47
- package/src/embeddings/database.py +0 -91
- package/src/embeddings/engine.py +0 -247
- package/src/embeddings/model_loader.py +0 -145
- package/src/event_bus.py +0 -562
- package/src/graph/__init__.py +0 -36
- package/src/graph/build_helpers.py +0 -74
- package/src/graph/cli.py +0 -87
- package/src/graph/cluster_builder.py +0 -188
- package/src/graph/cluster_summary.py +0 -148
- package/src/graph/constants.py +0 -47
- package/src/graph/edge_builder.py +0 -162
- package/src/graph/entity_extractor.py +0 -95
- package/src/graph/graph_core.py +0 -226
- package/src/graph/graph_search.py +0 -231
- package/src/graph/hierarchical.py +0 -207
- package/src/graph/schema.py +0 -99
- package/src/graph_engine.py +0 -52
- package/src/hnsw_index.py +0 -628
- package/src/hybrid_search.py +0 -46
- package/src/learning/__init__.py +0 -217
- package/src/learning/adaptive_ranker.py +0 -682
- package/src/learning/bootstrap/__init__.py +0 -69
- package/src/learning/bootstrap/constants.py +0 -93
- package/src/learning/bootstrap/db_queries.py +0 -316
- package/src/learning/bootstrap/sampling.py +0 -82
- package/src/learning/bootstrap/text_utils.py +0 -71
- package/src/learning/cross_project_aggregator.py +0 -857
- package/src/learning/db/__init__.py +0 -40
- package/src/learning/db/constants.py +0 -44
- package/src/learning/db/schema.py +0 -279
- package/src/learning/engagement_tracker.py +0 -628
- package/src/learning/feature_extractor.py +0 -708
- package/src/learning/feedback_collector.py +0 -806
- package/src/learning/learning_db.py +0 -915
- package/src/learning/project_context_manager.py +0 -572
- package/src/learning/ranking/__init__.py +0 -33
- package/src/learning/ranking/constants.py +0 -84
- package/src/learning/ranking/helpers.py +0 -278
- package/src/learning/source_quality_scorer.py +0 -676
- package/src/learning/synthetic_bootstrap.py +0 -755
- package/src/learning/tests/test_adaptive_ranker.py +0 -325
- package/src/learning/tests/test_adaptive_ranker_v28.py +0 -60
- package/src/learning/tests/test_aggregator.py +0 -306
- package/src/learning/tests/test_auto_retrain_v28.py +0 -35
- package/src/learning/tests/test_e2e_ranking_v28.py +0 -82
- package/src/learning/tests/test_feature_extractor_v28.py +0 -93
- package/src/learning/tests/test_feedback_collector.py +0 -294
- package/src/learning/tests/test_learning_db.py +0 -602
- package/src/learning/tests/test_learning_db_v28.py +0 -110
- package/src/learning/tests/test_learning_init_v28.py +0 -48
- package/src/learning/tests/test_outcome_signals.py +0 -48
- package/src/learning/tests/test_project_context.py +0 -292
- package/src/learning/tests/test_schema_migration.py +0 -319
- package/src/learning/tests/test_signal_inference.py +0 -397
- package/src/learning/tests/test_source_quality.py +0 -351
- package/src/learning/tests/test_synthetic_bootstrap.py +0 -429
- package/src/learning/tests/test_workflow_miner.py +0 -318
- package/src/learning/workflow_pattern_miner.py +0 -655
- package/src/lifecycle/__init__.py +0 -54
- package/src/lifecycle/bounded_growth.py +0 -239
- package/src/lifecycle/compaction_engine.py +0 -226
- package/src/lifecycle/lifecycle_engine.py +0 -355
- package/src/lifecycle/lifecycle_evaluator.py +0 -257
- package/src/lifecycle/lifecycle_scheduler.py +0 -130
- package/src/lifecycle/retention_policy.py +0 -285
- package/src/lifecycle/tests/test_bounded_growth.py +0 -193
- package/src/lifecycle/tests/test_compaction.py +0 -179
- package/src/lifecycle/tests/test_lifecycle_engine.py +0 -137
- package/src/lifecycle/tests/test_lifecycle_evaluation.py +0 -177
- package/src/lifecycle/tests/test_lifecycle_scheduler.py +0 -127
- package/src/lifecycle/tests/test_lifecycle_search.py +0 -109
- package/src/lifecycle/tests/test_mcp_compact.py +0 -149
- package/src/lifecycle/tests/test_mcp_lifecycle_status.py +0 -114
- package/src/lifecycle/tests/test_retention_policy.py +0 -162
- package/src/mcp_tools_v28.py +0 -281
- package/src/memory/__init__.py +0 -36
- package/src/memory/cli.py +0 -205
- package/src/memory/constants.py +0 -39
- package/src/memory/helpers.py +0 -28
- package/src/memory/schema.py +0 -166
- package/src/memory-profiles.py +0 -595
- package/src/memory-reset.py +0 -491
- package/src/memory_compression.py +0 -989
- package/src/memory_store_v2.py +0 -1155
- package/src/migrate_v1_to_v2.py +0 -629
- package/src/pattern_learner.py +0 -34
- package/src/patterns/__init__.py +0 -24
- package/src/patterns/analyzers.py +0 -251
- package/src/patterns/learner.py +0 -271
- package/src/patterns/scoring.py +0 -171
- package/src/patterns/store.py +0 -225
- package/src/patterns/terminology.py +0 -140
- package/src/provenance_tracker.py +0 -312
- package/src/qualixar_attribution.py +0 -139
- package/src/qualixar_watermark.py +0 -78
- package/src/query_optimizer.py +0 -511
- package/src/rate_limiter.py +0 -83
- package/src/search/__init__.py +0 -20
- package/src/search/cli.py +0 -77
- package/src/search/constants.py +0 -26
- package/src/search/engine.py +0 -241
- package/src/search/fusion.py +0 -122
- package/src/search/index_loader.py +0 -114
- package/src/search/methods.py +0 -162
- package/src/search_engine_v2.py +0 -401
- package/src/setup_validator.py +0 -482
- package/src/subscription_manager.py +0 -391
- package/src/tree/__init__.py +0 -59
- package/src/tree/builder.py +0 -185
- package/src/tree/nodes.py +0 -202
- package/src/tree/queries.py +0 -257
- package/src/tree/schema.py +0 -80
- package/src/tree_manager.py +0 -19
- package/src/trust/__init__.py +0 -45
- package/src/trust/constants.py +0 -66
- package/src/trust/queries.py +0 -157
- package/src/trust/schema.py +0 -95
- package/src/trust/scorer.py +0 -299
- package/src/trust/signals.py +0 -95
- package/src/trust_scorer.py +0 -44
- package/ui/app.js +0 -1588
- package/ui/js/graph-cytoscape-monolithic-backup.js +0 -1168
- package/ui/js/graph-cytoscape.js +0 -1168
- package/ui/js/graph-d3-backup.js +0 -32
- package/ui/js/graph.js +0 -32
- package/ui_server.py +0 -286
- /package/docs/{ACCESSIBILITY.md → v2-archive/ACCESSIBILITY.md} +0 -0
- /package/docs/{ARCHITECTURE.md → v2-archive/ARCHITECTURE.md} +0 -0
- /package/docs/{CLI-COMMANDS-REFERENCE.md → v2-archive/CLI-COMMANDS-REFERENCE.md} +0 -0
- /package/docs/{COMPRESSION-README.md → v2-archive/COMPRESSION-README.md} +0 -0
- /package/docs/{FRAMEWORK-INTEGRATIONS.md → v2-archive/FRAMEWORK-INTEGRATIONS.md} +0 -0
- /package/docs/{MCP-MANUAL-SETUP.md → v2-archive/MCP-MANUAL-SETUP.md} +0 -0
- /package/docs/{MCP-TROUBLESHOOTING.md → v2-archive/MCP-TROUBLESHOOTING.md} +0 -0
- /package/docs/{PATTERN-LEARNING.md → v2-archive/PATTERN-LEARNING.md} +0 -0
- /package/docs/{PROFILES-GUIDE.md → v2-archive/PROFILES-GUIDE.md} +0 -0
- /package/docs/{RESET-GUIDE.md → v2-archive/RESET-GUIDE.md} +0 -0
- /package/docs/{SEARCH-ENGINE-V2.2.0.md → v2-archive/SEARCH-ENGINE-V2.2.0.md} +0 -0
- /package/docs/{SEARCH-INTEGRATION-GUIDE.md → v2-archive/SEARCH-INTEGRATION-GUIDE.md} +0 -0
- /package/docs/{UI-SERVER.md → v2-archive/UI-SERVER.md} +0 -0
- /package/docs/{UNIVERSAL-INTEGRATION.md → v2-archive/UNIVERSAL-INTEGRATION.md} +0 -0
- /package/docs/{V2.2.0-OPTIONAL-SEARCH.md → v2-archive/V2.2.0-OPTIONAL-SEARCH.md} +0 -0
- /package/docs/{WINDOWS-INSTALL-README.txt → v2-archive/WINDOWS-INSTALL-README.txt} +0 -0
- /package/docs/{WINDOWS-POST-INSTALL.txt → v2-archive/WINDOWS-POST-INSTALL.txt} +0 -0
- /package/docs/{example_graph_usage.py → v2-archive/example_graph_usage.py} +0 -0
- /package/{completions → ide/completions}/slm.bash +0 -0
- /package/{completions → ide/completions}/slm.zsh +0 -0
- /package/{configs → ide/configs}/cody-commands.json +0 -0
- /package/{install-skills.sh → scripts/install-skills.sh} +0 -0
- /package/{install.ps1 → scripts/install.ps1} +0 -0
- /package/{install.sh → scripts/install.sh} +0 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under the MIT License - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
"""LRU cache with optional TTL for search-result caching.
|
|
5
|
+
|
|
6
|
+
Key features:
|
|
7
|
+
* O(1) get / set via ``OrderedDict``
|
|
8
|
+
* Time-to-live expiry per entry
|
|
9
|
+
* Size-based eviction (LRU)
|
|
10
|
+
* Optional thread safety
|
|
11
|
+
* Hit / miss / eviction statistics
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import time
|
|
18
|
+
from collections import OrderedDict
|
|
19
|
+
from threading import RLock
|
|
20
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger("superlocalmemory.cache")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CacheEntry:
|
|
26
|
+
"""Single cache entry with metadata."""
|
|
27
|
+
|
|
28
|
+
__slots__ = ["value", "timestamp", "access_count", "size_estimate"]
|
|
29
|
+
|
|
30
|
+
def __init__(self, value: Any, size_estimate: int = 0) -> None:
|
|
31
|
+
self.value = value
|
|
32
|
+
self.timestamp = time.time()
|
|
33
|
+
self.access_count = 0
|
|
34
|
+
self.size_estimate = size_estimate
|
|
35
|
+
|
|
36
|
+
def is_expired(self, ttl_seconds: Optional[float]) -> bool:
|
|
37
|
+
"""Return ``True`` when the entry has exceeded *ttl_seconds*."""
|
|
38
|
+
if ttl_seconds is None:
|
|
39
|
+
return False
|
|
40
|
+
return (time.time() - self.timestamp) > ttl_seconds
|
|
41
|
+
|
|
42
|
+
def mark_accessed(self) -> None:
|
|
43
|
+
"""Increment access counter."""
|
|
44
|
+
self.access_count += 1
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CacheManager:
|
|
48
|
+
"""LRU cache manager with TTL support.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
max_size: Maximum number of entries before LRU eviction.
|
|
52
|
+
ttl_seconds: Per-entry time-to-live (``None`` = never expire).
|
|
53
|
+
thread_safe: Wrap operations in a reentrant lock.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
max_size: int = 100,
|
|
59
|
+
ttl_seconds: Optional[float] = 300,
|
|
60
|
+
thread_safe: bool = False,
|
|
61
|
+
) -> None:
|
|
62
|
+
self.max_size = max(1, max_size)
|
|
63
|
+
self.ttl_seconds = ttl_seconds
|
|
64
|
+
self.thread_safe = thread_safe
|
|
65
|
+
|
|
66
|
+
self._cache: OrderedDict[str, CacheEntry] = OrderedDict()
|
|
67
|
+
self._lock: Optional[RLock] = RLock() if thread_safe else None
|
|
68
|
+
|
|
69
|
+
# Statistics
|
|
70
|
+
self._hits = 0
|
|
71
|
+
self._misses = 0
|
|
72
|
+
self._evictions = 0
|
|
73
|
+
self._total_size_estimate = 0
|
|
74
|
+
|
|
75
|
+
# ------------------------------------------------------------------
|
|
76
|
+
# Key helpers
|
|
77
|
+
# ------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def _hash_key(query: str, **kwargs: Any) -> str:
|
|
81
|
+
"""Deterministic cache key from *query* + keyword args."""
|
|
82
|
+
key_data = {"query": query, **kwargs}
|
|
83
|
+
key_str = json.dumps(key_data, sort_keys=True)
|
|
84
|
+
return hashlib.sha256(key_str.encode()).hexdigest()[:16]
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def _estimate_size(value: Any) -> int:
|
|
88
|
+
"""Rough byte-size estimate for analytics."""
|
|
89
|
+
try:
|
|
90
|
+
if isinstance(value, list):
|
|
91
|
+
return len(value) * 100
|
|
92
|
+
return len(json.dumps(value, default=str))
|
|
93
|
+
except Exception:
|
|
94
|
+
return 1000
|
|
95
|
+
|
|
96
|
+
# ------------------------------------------------------------------
|
|
97
|
+
# Core API (simple key-based)
|
|
98
|
+
# ------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
def set(self, key: str, value: Any) -> None:
|
|
101
|
+
"""Store *value* under a plain string *key*.
|
|
102
|
+
|
|
103
|
+
This is the simple interface expected by the test suite.
|
|
104
|
+
"""
|
|
105
|
+
self._put_internal(key, value)
|
|
106
|
+
|
|
107
|
+
def get(self, key: str, **kwargs: Any) -> Optional[Any]:
|
|
108
|
+
"""Retrieve value by plain string *key*.
|
|
109
|
+
|
|
110
|
+
Returns ``None`` on cache miss or expiry.
|
|
111
|
+
"""
|
|
112
|
+
if kwargs:
|
|
113
|
+
key = self._hash_key(key, **kwargs)
|
|
114
|
+
return self._get_internal(key)
|
|
115
|
+
|
|
116
|
+
# ------------------------------------------------------------------
|
|
117
|
+
# Query-oriented API (hashed keys)
|
|
118
|
+
# ------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
def put(self, query: str, value: Any, **kwargs: Any) -> None:
|
|
121
|
+
"""Store *value* keyed by the hash of *query* + *kwargs*."""
|
|
122
|
+
hashed = self._hash_key(query, **kwargs)
|
|
123
|
+
self._put_internal(hashed, value)
|
|
124
|
+
|
|
125
|
+
def get_by_query(self, query: str, **kwargs: Any) -> Optional[Any]:
|
|
126
|
+
"""Retrieve value keyed by the hash of *query* + *kwargs*."""
|
|
127
|
+
hashed = self._hash_key(query, **kwargs)
|
|
128
|
+
return self._get_internal(hashed)
|
|
129
|
+
|
|
130
|
+
# ------------------------------------------------------------------
|
|
131
|
+
# Internal get / put (shared logic)
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
def _get_internal(self, key: str) -> Optional[Any]:
|
|
135
|
+
if self._lock:
|
|
136
|
+
self._lock.acquire()
|
|
137
|
+
try:
|
|
138
|
+
if key not in self._cache:
|
|
139
|
+
self._misses += 1
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
entry = self._cache[key]
|
|
143
|
+
|
|
144
|
+
if entry.is_expired(self.ttl_seconds):
|
|
145
|
+
del self._cache[key]
|
|
146
|
+
self._total_size_estimate -= entry.size_estimate
|
|
147
|
+
self._misses += 1
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
self._cache.move_to_end(key)
|
|
151
|
+
entry.mark_accessed()
|
|
152
|
+
self._hits += 1
|
|
153
|
+
return entry.value
|
|
154
|
+
finally:
|
|
155
|
+
if self._lock:
|
|
156
|
+
self._lock.release()
|
|
157
|
+
|
|
158
|
+
def _put_internal(self, key: str, value: Any) -> None:
|
|
159
|
+
size_estimate = self._estimate_size(value)
|
|
160
|
+
|
|
161
|
+
if self._lock:
|
|
162
|
+
self._lock.acquire()
|
|
163
|
+
try:
|
|
164
|
+
if key in self._cache:
|
|
165
|
+
old = self._cache[key]
|
|
166
|
+
self._total_size_estimate -= old.size_estimate
|
|
167
|
+
del self._cache[key]
|
|
168
|
+
|
|
169
|
+
if len(self._cache) >= self.max_size:
|
|
170
|
+
_, evicted = self._cache.popitem(last=False)
|
|
171
|
+
self._total_size_estimate -= evicted.size_estimate
|
|
172
|
+
self._evictions += 1
|
|
173
|
+
|
|
174
|
+
self._cache[key] = CacheEntry(value, size_estimate)
|
|
175
|
+
self._total_size_estimate += size_estimate
|
|
176
|
+
finally:
|
|
177
|
+
if self._lock:
|
|
178
|
+
self._lock.release()
|
|
179
|
+
|
|
180
|
+
# ------------------------------------------------------------------
|
|
181
|
+
# Maintenance
|
|
182
|
+
# ------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
def invalidate(self, query: str, **kwargs: Any) -> bool:
|
|
185
|
+
"""Remove entry for *query*. Returns ``True`` if it existed."""
|
|
186
|
+
key = self._hash_key(query, **kwargs)
|
|
187
|
+
if self._lock:
|
|
188
|
+
self._lock.acquire()
|
|
189
|
+
try:
|
|
190
|
+
if key in self._cache:
|
|
191
|
+
entry = self._cache.pop(key)
|
|
192
|
+
self._total_size_estimate -= entry.size_estimate
|
|
193
|
+
return True
|
|
194
|
+
return False
|
|
195
|
+
finally:
|
|
196
|
+
if self._lock:
|
|
197
|
+
self._lock.release()
|
|
198
|
+
|
|
199
|
+
def clear(self) -> None:
|
|
200
|
+
"""Remove all entries."""
|
|
201
|
+
if self._lock:
|
|
202
|
+
self._lock.acquire()
|
|
203
|
+
try:
|
|
204
|
+
self._cache.clear()
|
|
205
|
+
self._total_size_estimate = 0
|
|
206
|
+
finally:
|
|
207
|
+
if self._lock:
|
|
208
|
+
self._lock.release()
|
|
209
|
+
|
|
210
|
+
def evict_expired(self) -> int:
|
|
211
|
+
"""Manually remove all expired entries. Returns count removed."""
|
|
212
|
+
if self.ttl_seconds is None:
|
|
213
|
+
return 0
|
|
214
|
+
if self._lock:
|
|
215
|
+
self._lock.acquire()
|
|
216
|
+
try:
|
|
217
|
+
expired = [
|
|
218
|
+
k for k, e in self._cache.items()
|
|
219
|
+
if e.is_expired(self.ttl_seconds)
|
|
220
|
+
]
|
|
221
|
+
for k in expired:
|
|
222
|
+
entry = self._cache.pop(k)
|
|
223
|
+
self._total_size_estimate -= entry.size_estimate
|
|
224
|
+
return len(expired)
|
|
225
|
+
finally:
|
|
226
|
+
if self._lock:
|
|
227
|
+
self._lock.release()
|
|
228
|
+
|
|
229
|
+
# ------------------------------------------------------------------
|
|
230
|
+
# Stats
|
|
231
|
+
# ------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
234
|
+
"""Return cache statistics snapshot."""
|
|
235
|
+
total = self._hits + self._misses
|
|
236
|
+
hit_rate = self._hits / total if total > 0 else 0.0
|
|
237
|
+
|
|
238
|
+
avg_access = 0.0
|
|
239
|
+
if self._cache:
|
|
240
|
+
avg_access = sum(e.access_count for e in self._cache.values()) / len(self._cache)
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
"max_size": self.max_size,
|
|
244
|
+
"current_size": len(self._cache),
|
|
245
|
+
"ttl_seconds": self.ttl_seconds,
|
|
246
|
+
"hits": self._hits,
|
|
247
|
+
"misses": self._misses,
|
|
248
|
+
"hit_rate": hit_rate,
|
|
249
|
+
"evictions": self._evictions,
|
|
250
|
+
"total_size_estimate_kb": self._total_size_estimate / 1024,
|
|
251
|
+
"avg_access_count": avg_access,
|
|
252
|
+
"thread_safe": self.thread_safe,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
def get_top_queries(self, limit: int = 10) -> List[Tuple[str, int]]:
|
|
256
|
+
"""Return the *limit* most-accessed cache keys."""
|
|
257
|
+
if self._lock:
|
|
258
|
+
self._lock.acquire()
|
|
259
|
+
try:
|
|
260
|
+
items = [
|
|
261
|
+
(k, e.access_count) for k, e in self._cache.items()
|
|
262
|
+
]
|
|
263
|
+
items.sort(key=lambda x: x[1], reverse=True)
|
|
264
|
+
return items[:limit]
|
|
265
|
+
finally:
|
|
266
|
+
if self._lock:
|
|
267
|
+
self._lock.release()
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under the MIT License - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
"""EventBus -- Real-time event broadcasting for memory operations.
|
|
5
|
+
|
|
6
|
+
Thread-safe singleton per DB path. In-memory deque buffer + SQLite persistence.
|
|
7
|
+
Listener callbacks run on the emitter's thread.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import sqlite3
|
|
13
|
+
import threading
|
|
14
|
+
from collections import deque
|
|
15
|
+
from datetime import datetime, timedelta
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("superlocalmemory.events")
|
|
20
|
+
|
|
21
|
+
# Default retention windows (hours)
|
|
22
|
+
DEFAULT_HOT_HOURS = 48
|
|
23
|
+
DEFAULT_WARM_HOURS = 14 * 24 # 14 days
|
|
24
|
+
DEFAULT_COLD_HOURS = 30 * 24 # 30 days
|
|
25
|
+
|
|
26
|
+
# In-memory buffer size for real-time delivery
|
|
27
|
+
EVENT_BUFFER_SIZE = 200
|
|
28
|
+
|
|
29
|
+
# Valid event types (V3 superset)
|
|
30
|
+
VALID_EVENT_TYPES = frozenset([
|
|
31
|
+
"memory.stored", # New memory written (was memory.created in V2)
|
|
32
|
+
"memory.updated", # Existing memory modified
|
|
33
|
+
"memory.deleted", # Memory removed
|
|
34
|
+
"memory.recalled", # Memory retrieved by an agent
|
|
35
|
+
"graph.updated", # Knowledge graph rebuilt
|
|
36
|
+
"pattern.learned", # New pattern detected
|
|
37
|
+
"agent.connected", # New agent connects
|
|
38
|
+
"agent.disconnected", # Agent disconnects
|
|
39
|
+
"trust.signal", # V3: trust score change
|
|
40
|
+
"compliance.audit", # V3: compliance event logged
|
|
41
|
+
"learning.feedback", # V3: learning feedback received
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class EventBus:
|
|
46
|
+
"""
|
|
47
|
+
Central event bus for SuperLocalMemory V3.
|
|
48
|
+
|
|
49
|
+
Singleton per database path. Emits events to persistent storage and
|
|
50
|
+
in-memory listeners simultaneously. Thread-safe.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
_instances: Dict[str, "EventBus"] = {}
|
|
54
|
+
_instances_lock = threading.Lock()
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def get_instance(cls, db_path: Optional[Path] = None) -> "EventBus":
|
|
58
|
+
"""Get or create the singleton EventBus for a database path."""
|
|
59
|
+
if db_path is None:
|
|
60
|
+
db_path = Path.home() / ".superlocalmemory" / "memory.db"
|
|
61
|
+
|
|
62
|
+
key = str(db_path)
|
|
63
|
+
with cls._instances_lock:
|
|
64
|
+
if key not in cls._instances:
|
|
65
|
+
cls._instances[key] = cls(db_path)
|
|
66
|
+
return cls._instances[key]
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def reset_instance(cls, db_path: Optional[Path] = None) -> None:
|
|
70
|
+
"""Remove and close a singleton instance. Used for testing."""
|
|
71
|
+
with cls._instances_lock:
|
|
72
|
+
if db_path is None:
|
|
73
|
+
cls._instances.clear()
|
|
74
|
+
else:
|
|
75
|
+
key = str(db_path)
|
|
76
|
+
if key in cls._instances:
|
|
77
|
+
del cls._instances[key]
|
|
78
|
+
|
|
79
|
+
def __init__(self, db_path: Path) -> None:
|
|
80
|
+
"""Initialize EventBus. Prefer get_instance() over direct construction."""
|
|
81
|
+
self.db_path = Path(db_path)
|
|
82
|
+
self._buffer: deque = deque(maxlen=EVENT_BUFFER_SIZE)
|
|
83
|
+
self._buffer_lock = threading.Lock()
|
|
84
|
+
self._event_counter = 0
|
|
85
|
+
self._counter_lock = threading.Lock()
|
|
86
|
+
self._listeners: List[Callable[[dict], None]] = []
|
|
87
|
+
self._listeners_lock = threading.Lock()
|
|
88
|
+
self._write_count = 0
|
|
89
|
+
self._last_prune = datetime.now()
|
|
90
|
+
self._init_schema()
|
|
91
|
+
logger.info("EventBus initialized: db=%s", self.db_path)
|
|
92
|
+
|
|
93
|
+
def _init_schema(self) -> None:
|
|
94
|
+
"""Create the memory_events table if it does not exist."""
|
|
95
|
+
conn = sqlite3.connect(str(self.db_path))
|
|
96
|
+
try:
|
|
97
|
+
cur = conn.cursor()
|
|
98
|
+
cur.execute("""
|
|
99
|
+
CREATE TABLE IF NOT EXISTS memory_events (
|
|
100
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
101
|
+
event_type TEXT NOT NULL,
|
|
102
|
+
memory_id INTEGER,
|
|
103
|
+
source_agent TEXT DEFAULT 'user',
|
|
104
|
+
source_protocol TEXT DEFAULT 'internal',
|
|
105
|
+
payload TEXT,
|
|
106
|
+
importance INTEGER DEFAULT 5,
|
|
107
|
+
tier TEXT DEFAULT 'hot',
|
|
108
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
109
|
+
)
|
|
110
|
+
""")
|
|
111
|
+
cur.execute("CREATE INDEX IF NOT EXISTS idx_events_type ON memory_events(event_type)")
|
|
112
|
+
cur.execute("CREATE INDEX IF NOT EXISTS idx_events_created ON memory_events(created_at)")
|
|
113
|
+
cur.execute("CREATE INDEX IF NOT EXISTS idx_events_tier ON memory_events(tier)")
|
|
114
|
+
conn.commit()
|
|
115
|
+
finally:
|
|
116
|
+
conn.close()
|
|
117
|
+
|
|
118
|
+
def emit(
|
|
119
|
+
self,
|
|
120
|
+
event_type: str,
|
|
121
|
+
payload: Optional[Dict[str, Any]] = None,
|
|
122
|
+
memory_id: Optional[int] = None,
|
|
123
|
+
source_agent: str = "user",
|
|
124
|
+
source_protocol: str = "internal",
|
|
125
|
+
importance: int = 5,
|
|
126
|
+
) -> Optional[int]:
|
|
127
|
+
"""Emit an event to all subscribers and persist to database."""
|
|
128
|
+
if event_type not in VALID_EVENT_TYPES:
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"Invalid event type: {event_type}. "
|
|
131
|
+
f"Valid: {', '.join(sorted(VALID_EVENT_TYPES))}"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
importance = max(1, min(10, importance))
|
|
135
|
+
|
|
136
|
+
now = datetime.now().isoformat()
|
|
137
|
+
with self._counter_lock:
|
|
138
|
+
self._event_counter += 1
|
|
139
|
+
seq = self._event_counter
|
|
140
|
+
|
|
141
|
+
event: Dict[str, Any] = {
|
|
142
|
+
"seq": seq,
|
|
143
|
+
"event_type": event_type,
|
|
144
|
+
"memory_id": memory_id,
|
|
145
|
+
"source_agent": source_agent,
|
|
146
|
+
"source_protocol": source_protocol,
|
|
147
|
+
"payload": payload or {},
|
|
148
|
+
"importance": importance,
|
|
149
|
+
"timestamp": now,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# 1. Persist
|
|
153
|
+
event_id = self._persist_event(event)
|
|
154
|
+
if event_id is not None:
|
|
155
|
+
event["id"] = event_id
|
|
156
|
+
|
|
157
|
+
# 2. Buffer
|
|
158
|
+
with self._buffer_lock:
|
|
159
|
+
self._buffer.append(event)
|
|
160
|
+
|
|
161
|
+
# 3. Notify
|
|
162
|
+
self._notify_listeners(event)
|
|
163
|
+
|
|
164
|
+
logger.debug(
|
|
165
|
+
"Event emitted: type=%s id=%s memory_id=%s",
|
|
166
|
+
event_type, event_id, memory_id,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Auto-prune heuristic
|
|
170
|
+
self._write_count += 1
|
|
171
|
+
if (
|
|
172
|
+
self._write_count >= 100
|
|
173
|
+
or (datetime.now() - self._last_prune).total_seconds() > 86400
|
|
174
|
+
):
|
|
175
|
+
try:
|
|
176
|
+
self.prune_events()
|
|
177
|
+
except Exception:
|
|
178
|
+
pass
|
|
179
|
+
self._write_count = 0
|
|
180
|
+
self._last_prune = datetime.now()
|
|
181
|
+
|
|
182
|
+
return event_id
|
|
183
|
+
|
|
184
|
+
# Alias for V3 compatibility
|
|
185
|
+
publish = emit
|
|
186
|
+
|
|
187
|
+
def _persist_event(self, event: dict) -> Optional[int]:
|
|
188
|
+
"""Persist event to the memory_events table. Returns row id or None."""
|
|
189
|
+
try:
|
|
190
|
+
conn = sqlite3.connect(str(self.db_path))
|
|
191
|
+
try:
|
|
192
|
+
cur = conn.cursor()
|
|
193
|
+
cur.execute(
|
|
194
|
+
"INSERT INTO memory_events (event_type, memory_id, source_agent,"
|
|
195
|
+
" source_protocol, payload, importance, tier, created_at)"
|
|
196
|
+
" VALUES (?, ?, ?, ?, ?, ?, 'hot', ?)",
|
|
197
|
+
(event["event_type"], event.get("memory_id"),
|
|
198
|
+
event["source_agent"], event["source_protocol"],
|
|
199
|
+
json.dumps(event["payload"]), event["importance"],
|
|
200
|
+
event["timestamp"]),
|
|
201
|
+
)
|
|
202
|
+
conn.commit()
|
|
203
|
+
return cur.lastrowid
|
|
204
|
+
finally:
|
|
205
|
+
conn.close()
|
|
206
|
+
except Exception as exc:
|
|
207
|
+
logger.error("Failed to persist event: %s", exc)
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
def add_listener(self, callback: Callable[[dict], None]) -> None:
|
|
211
|
+
"""Register a listener that receives every emitted event."""
|
|
212
|
+
with self._listeners_lock:
|
|
213
|
+
self._listeners.append(callback)
|
|
214
|
+
|
|
215
|
+
# Alias
|
|
216
|
+
subscribe = add_listener
|
|
217
|
+
|
|
218
|
+
def remove_listener(self, callback: Callable[[dict], None]) -> None:
|
|
219
|
+
"""Remove a previously registered listener."""
|
|
220
|
+
with self._listeners_lock:
|
|
221
|
+
try:
|
|
222
|
+
self._listeners.remove(callback)
|
|
223
|
+
except ValueError:
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
# Alias
|
|
227
|
+
unsubscribe = remove_listener
|
|
228
|
+
|
|
229
|
+
def _notify_listeners(self, event: dict) -> None:
|
|
230
|
+
"""Call all registered listeners. Errors are logged, not raised."""
|
|
231
|
+
with self._listeners_lock:
|
|
232
|
+
listeners = list(self._listeners)
|
|
233
|
+
|
|
234
|
+
for listener in listeners:
|
|
235
|
+
try:
|
|
236
|
+
listener(event)
|
|
237
|
+
except Exception as exc:
|
|
238
|
+
logger.error("Event listener failed: %s", exc)
|
|
239
|
+
|
|
240
|
+
def get_recent_events(
|
|
241
|
+
self,
|
|
242
|
+
since_id: Optional[int] = None,
|
|
243
|
+
limit: int = 50,
|
|
244
|
+
event_type: Optional[str] = None,
|
|
245
|
+
) -> List[dict]:
|
|
246
|
+
"""Get recent events from the database."""
|
|
247
|
+
limit = min(limit, 200)
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
conn = sqlite3.connect(str(self.db_path))
|
|
251
|
+
try:
|
|
252
|
+
cur = conn.cursor()
|
|
253
|
+
|
|
254
|
+
query = ("SELECT id, event_type, memory_id, source_agent,"
|
|
255
|
+
" source_protocol, payload, importance, tier, created_at"
|
|
256
|
+
" FROM memory_events WHERE 1=1")
|
|
257
|
+
params: List[Any] = []
|
|
258
|
+
|
|
259
|
+
if since_id is not None:
|
|
260
|
+
query += " AND id > ?"
|
|
261
|
+
params.append(since_id)
|
|
262
|
+
|
|
263
|
+
if event_type:
|
|
264
|
+
query += " AND event_type = ?"
|
|
265
|
+
params.append(event_type)
|
|
266
|
+
|
|
267
|
+
query += " ORDER BY id ASC LIMIT ?"
|
|
268
|
+
params.append(limit)
|
|
269
|
+
|
|
270
|
+
cur.execute(query, params)
|
|
271
|
+
rows = cur.fetchall()
|
|
272
|
+
finally:
|
|
273
|
+
conn.close()
|
|
274
|
+
|
|
275
|
+
events: List[dict] = []
|
|
276
|
+
for row in rows:
|
|
277
|
+
try:
|
|
278
|
+
parsed = json.loads(row[5]) if row[5] else {}
|
|
279
|
+
except (json.JSONDecodeError, TypeError):
|
|
280
|
+
parsed = {}
|
|
281
|
+
events.append({
|
|
282
|
+
"id": row[0], "event_type": row[1], "memory_id": row[2],
|
|
283
|
+
"source_agent": row[3], "source_protocol": row[4],
|
|
284
|
+
"payload": parsed, "importance": row[6],
|
|
285
|
+
"tier": row[7], "timestamp": row[8],
|
|
286
|
+
})
|
|
287
|
+
return events
|
|
288
|
+
|
|
289
|
+
except Exception as exc:
|
|
290
|
+
logger.error("Failed to get recent events: %s", exc)
|
|
291
|
+
return []
|
|
292
|
+
|
|
293
|
+
def get_buffered_events(self, since_seq: int = 0) -> List[dict]:
|
|
294
|
+
"""Get events from the in-memory buffer (no DB hit)."""
|
|
295
|
+
with self._buffer_lock:
|
|
296
|
+
return [e for e in self._buffer if e.get("seq", 0) > since_seq]
|
|
297
|
+
|
|
298
|
+
def get_event_stats(self) -> dict:
|
|
299
|
+
"""Get event system statistics."""
|
|
300
|
+
try:
|
|
301
|
+
conn = sqlite3.connect(str(self.db_path))
|
|
302
|
+
try:
|
|
303
|
+
cur = conn.cursor()
|
|
304
|
+
|
|
305
|
+
total = cur.execute("SELECT COUNT(*) FROM memory_events").fetchone()[0]
|
|
306
|
+
cur.execute("SELECT event_type, COUNT(*) FROM memory_events GROUP BY event_type")
|
|
307
|
+
by_type = dict(cur.fetchall())
|
|
308
|
+
cur.execute("SELECT tier, COUNT(*) FROM memory_events GROUP BY tier")
|
|
309
|
+
by_tier = dict(cur.fetchall())
|
|
310
|
+
cur.execute("SELECT COUNT(*) FROM memory_events WHERE created_at >= datetime('now', '-24 hours')")
|
|
311
|
+
last_24h = cur.fetchone()[0]
|
|
312
|
+
finally:
|
|
313
|
+
conn.close()
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
**by_type,
|
|
317
|
+
"total_events": total,
|
|
318
|
+
"events_last_24h": last_24h,
|
|
319
|
+
"by_type": by_type,
|
|
320
|
+
"by_tier": by_tier,
|
|
321
|
+
"buffer_size": len(self._buffer),
|
|
322
|
+
"listener_count": len(self._listeners),
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
except Exception as exc:
|
|
326
|
+
logger.error("Failed to get event stats: %s", exc)
|
|
327
|
+
return {"total_events": 0, "error": str(exc)}
|
|
328
|
+
|
|
329
|
+
def prune_events(
|
|
330
|
+
self,
|
|
331
|
+
hot_hours: int = DEFAULT_HOT_HOURS,
|
|
332
|
+
warm_hours: int = DEFAULT_WARM_HOURS,
|
|
333
|
+
cold_hours: int = DEFAULT_COLD_HOURS,
|
|
334
|
+
) -> dict:
|
|
335
|
+
"""Apply tiered retention policy to persisted events."""
|
|
336
|
+
try:
|
|
337
|
+
conn = sqlite3.connect(str(self.db_path))
|
|
338
|
+
try:
|
|
339
|
+
cur = conn.cursor()
|
|
340
|
+
now = datetime.now()
|
|
341
|
+
stats = {"hot_to_warm": 0, "warm_to_cold": 0, "archived": 0}
|
|
342
|
+
|
|
343
|
+
# Hot -> Warm: older than hot_hours, importance < 5
|
|
344
|
+
warm_cutoff = (now - timedelta(hours=hot_hours)).isoformat()
|
|
345
|
+
cur.execute(
|
|
346
|
+
"UPDATE memory_events SET tier = 'warm' "
|
|
347
|
+
"WHERE tier = 'hot' AND created_at < ? AND importance < 5",
|
|
348
|
+
(warm_cutoff,),
|
|
349
|
+
)
|
|
350
|
+
stats["hot_to_warm"] = cur.rowcount
|
|
351
|
+
|
|
352
|
+
# Warm -> Cold: delete warm events older than warm_hours
|
|
353
|
+
cold_cutoff = (now - timedelta(hours=warm_hours)).isoformat()
|
|
354
|
+
cur.execute(
|
|
355
|
+
"DELETE FROM memory_events "
|
|
356
|
+
"WHERE tier = 'warm' AND created_at < ?",
|
|
357
|
+
(cold_cutoff,),
|
|
358
|
+
)
|
|
359
|
+
stats["warm_to_cold"] = cur.rowcount
|
|
360
|
+
|
|
361
|
+
# Archive: delete everything older than cold_hours
|
|
362
|
+
archive_cutoff = (now - timedelta(hours=cold_hours)).isoformat()
|
|
363
|
+
cur.execute(
|
|
364
|
+
"DELETE FROM memory_events WHERE created_at < ?",
|
|
365
|
+
(archive_cutoff,),
|
|
366
|
+
)
|
|
367
|
+
stats["archived"] = cur.rowcount
|
|
368
|
+
|
|
369
|
+
conn.commit()
|
|
370
|
+
finally:
|
|
371
|
+
conn.close()
|
|
372
|
+
|
|
373
|
+
logger.info(
|
|
374
|
+
"Prune complete: hot->warm=%d warm->cold=%d archived=%d",
|
|
375
|
+
stats["hot_to_warm"], stats["warm_to_cold"], stats["archived"],
|
|
376
|
+
)
|
|
377
|
+
return stats
|
|
378
|
+
|
|
379
|
+
except Exception as exc:
|
|
380
|
+
logger.error("Event pruning failed: %s", exc)
|
|
381
|
+
return {"error": str(exc)}
|