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,594 @@
|
|
|
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
|
+
|
|
5
|
+
"""SuperLocalMemory V3 — Database Manager.
|
|
6
|
+
|
|
7
|
+
SQLite with WAL, profile-scoped CRUD, FTS5 search, BM25 persistence.
|
|
8
|
+
All connections use try/finally. Only ``except sqlite3.Error``.
|
|
9
|
+
|
|
10
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json, logging, sqlite3, threading
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from types import ModuleType
|
|
18
|
+
from typing import Any, Generator
|
|
19
|
+
|
|
20
|
+
from superlocalmemory.storage.models import (
|
|
21
|
+
AtomicFact, CanonicalEntity, ConsolidationAction, ConsolidationActionType,
|
|
22
|
+
EdgeType, EntityAlias, EntityProfile, FactType, GraphEdge,
|
|
23
|
+
MemoryLifecycle, MemoryRecord, MemoryScene, SignalType, TemporalEvent,
|
|
24
|
+
TrustScore,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
def _jl(raw: Any, default: Any = None) -> Any:
|
|
30
|
+
"""JSON-load a value, returning *default* on None/empty."""
|
|
31
|
+
if raw is None or raw == "":
|
|
32
|
+
return default if default is not None else []
|
|
33
|
+
return json.loads(raw)
|
|
34
|
+
|
|
35
|
+
def _jd(val: Any) -> str | None:
|
|
36
|
+
"""JSON-dump a list/dict, or return None."""
|
|
37
|
+
return json.dumps(val) if val is not None else None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DatabaseManager:
|
|
41
|
+
"""Thread-safe SQLite manager with WAL, profile isolation, and FTS5.
|
|
42
|
+
|
|
43
|
+
Per-call connections outside transactions; shared connection inside
|
|
44
|
+
a ``transaction()`` block. Thread-safe via threading.Lock.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, db_path: str | Path) -> None:
|
|
48
|
+
self.db_path = Path(db_path)
|
|
49
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
self._lock = threading.Lock()
|
|
51
|
+
self._txn_conn: sqlite3.Connection | None = None
|
|
52
|
+
self._enable_wal()
|
|
53
|
+
|
|
54
|
+
def _enable_wal(self) -> None:
|
|
55
|
+
conn = sqlite3.connect(str(self.db_path))
|
|
56
|
+
try:
|
|
57
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
58
|
+
conn.execute("PRAGMA foreign_keys=ON")
|
|
59
|
+
conn.commit()
|
|
60
|
+
finally:
|
|
61
|
+
conn.close()
|
|
62
|
+
|
|
63
|
+
def initialize(self, schema_module: ModuleType) -> None:
|
|
64
|
+
"""Create all tables. *schema_module* must expose ``create_all_tables(conn)``."""
|
|
65
|
+
conn = sqlite3.connect(str(self.db_path))
|
|
66
|
+
try:
|
|
67
|
+
conn.execute("PRAGMA foreign_keys=ON")
|
|
68
|
+
schema_module.create_all_tables(conn)
|
|
69
|
+
conn.commit()
|
|
70
|
+
logger.info("Schema initialized at %s", self.db_path)
|
|
71
|
+
finally:
|
|
72
|
+
conn.close()
|
|
73
|
+
|
|
74
|
+
def close(self) -> None:
|
|
75
|
+
"""No-op for per-call connection model."""
|
|
76
|
+
|
|
77
|
+
def __enter__(self) -> DatabaseManager:
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def __exit__(self, *args: Any) -> None:
|
|
81
|
+
self.close()
|
|
82
|
+
|
|
83
|
+
def _connect(self) -> sqlite3.Connection:
|
|
84
|
+
conn = sqlite3.connect(str(self.db_path))
|
|
85
|
+
conn.row_factory = sqlite3.Row
|
|
86
|
+
conn.execute("PRAGMA foreign_keys=ON")
|
|
87
|
+
return conn
|
|
88
|
+
|
|
89
|
+
@contextmanager
|
|
90
|
+
def transaction(self) -> Generator[None, None, None]:
|
|
91
|
+
"""Atomic transaction. All writes commit or rollback together."""
|
|
92
|
+
with self._lock:
|
|
93
|
+
conn = self._connect()
|
|
94
|
+
self._txn_conn = conn
|
|
95
|
+
try:
|
|
96
|
+
yield
|
|
97
|
+
conn.commit()
|
|
98
|
+
except Exception:
|
|
99
|
+
conn.rollback()
|
|
100
|
+
raise
|
|
101
|
+
finally:
|
|
102
|
+
self._txn_conn = None
|
|
103
|
+
conn.close()
|
|
104
|
+
|
|
105
|
+
def execute(self, sql: str, params: tuple[Any, ...] = ()) -> list[sqlite3.Row]:
|
|
106
|
+
"""Execute SQL. Uses shared conn inside transaction, else per-call."""
|
|
107
|
+
if self._txn_conn is not None:
|
|
108
|
+
return self._txn_conn.execute(sql, params).fetchall()
|
|
109
|
+
conn = self._connect()
|
|
110
|
+
try:
|
|
111
|
+
rows = conn.execute(sql, params).fetchall()
|
|
112
|
+
conn.commit()
|
|
113
|
+
return rows
|
|
114
|
+
finally:
|
|
115
|
+
conn.close()
|
|
116
|
+
|
|
117
|
+
def store_memory(self, record: MemoryRecord) -> str:
|
|
118
|
+
"""Persist a raw memory record. Returns memory_id."""
|
|
119
|
+
self.execute(
|
|
120
|
+
"""INSERT OR REPLACE INTO memories
|
|
121
|
+
(memory_id, profile_id, content, session_id, speaker,
|
|
122
|
+
role, session_date, created_at, metadata_json)
|
|
123
|
+
VALUES (?,?,?,?,?,?,?,?,?)""",
|
|
124
|
+
(record.memory_id, record.profile_id, record.content,
|
|
125
|
+
record.session_id, record.speaker, record.role,
|
|
126
|
+
record.session_date, record.created_at,
|
|
127
|
+
json.dumps(record.metadata)),
|
|
128
|
+
)
|
|
129
|
+
return record.memory_id
|
|
130
|
+
|
|
131
|
+
def store_fact(self, fact: AtomicFact) -> str:
|
|
132
|
+
"""Persist an atomic fact. Returns fact_id."""
|
|
133
|
+
self.execute(
|
|
134
|
+
"""INSERT OR REPLACE INTO atomic_facts
|
|
135
|
+
(fact_id, memory_id, profile_id, content, fact_type,
|
|
136
|
+
entities_json, canonical_entities_json,
|
|
137
|
+
observation_date, referenced_date, interval_start, interval_end,
|
|
138
|
+
confidence, importance, evidence_count, access_count,
|
|
139
|
+
source_turn_ids_json, session_id,
|
|
140
|
+
embedding, fisher_mean, fisher_variance,
|
|
141
|
+
lifecycle, langevin_position,
|
|
142
|
+
emotional_valence, emotional_arousal, signal_type, created_at)
|
|
143
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
144
|
+
(fact.fact_id, fact.memory_id, fact.profile_id, fact.content,
|
|
145
|
+
fact.fact_type.value,
|
|
146
|
+
json.dumps(fact.entities), json.dumps(fact.canonical_entities),
|
|
147
|
+
fact.observation_date, fact.referenced_date,
|
|
148
|
+
fact.interval_start, fact.interval_end,
|
|
149
|
+
fact.confidence, fact.importance, fact.evidence_count, fact.access_count,
|
|
150
|
+
json.dumps(fact.source_turn_ids), fact.session_id,
|
|
151
|
+
_jd(fact.embedding), _jd(fact.fisher_mean), _jd(fact.fisher_variance),
|
|
152
|
+
fact.lifecycle.value, _jd(fact.langevin_position),
|
|
153
|
+
fact.emotional_valence, fact.emotional_arousal,
|
|
154
|
+
fact.signal_type.value, fact.created_at),
|
|
155
|
+
)
|
|
156
|
+
return fact.fact_id
|
|
157
|
+
|
|
158
|
+
def _row_to_fact(self, row: sqlite3.Row) -> AtomicFact:
|
|
159
|
+
"""Deserialize a row into AtomicFact."""
|
|
160
|
+
d = dict(row)
|
|
161
|
+
return AtomicFact(
|
|
162
|
+
fact_id=d["fact_id"], memory_id=d["memory_id"],
|
|
163
|
+
profile_id=d["profile_id"], content=d["content"],
|
|
164
|
+
fact_type=FactType(d["fact_type"]),
|
|
165
|
+
entities=_jl(d.get("entities_json")),
|
|
166
|
+
canonical_entities=_jl(d.get("canonical_entities_json")),
|
|
167
|
+
observation_date=d.get("observation_date"),
|
|
168
|
+
referenced_date=d.get("referenced_date"),
|
|
169
|
+
interval_start=d.get("interval_start"),
|
|
170
|
+
interval_end=d.get("interval_end"),
|
|
171
|
+
confidence=d["confidence"], importance=d["importance"],
|
|
172
|
+
evidence_count=d["evidence_count"], access_count=d["access_count"],
|
|
173
|
+
source_turn_ids=_jl(d.get("source_turn_ids_json")),
|
|
174
|
+
session_id=d.get("session_id", ""),
|
|
175
|
+
embedding=_jl(d.get("embedding"), None),
|
|
176
|
+
fisher_mean=_jl(d.get("fisher_mean"), None),
|
|
177
|
+
fisher_variance=_jl(d.get("fisher_variance"), None),
|
|
178
|
+
lifecycle=MemoryLifecycle(d["lifecycle"]) if d.get("lifecycle") else MemoryLifecycle.ACTIVE,
|
|
179
|
+
langevin_position=_jl(d.get("langevin_position"), None),
|
|
180
|
+
emotional_valence=d.get("emotional_valence", 0.0),
|
|
181
|
+
emotional_arousal=d.get("emotional_arousal", 0.0),
|
|
182
|
+
signal_type=SignalType(d["signal_type"]) if d.get("signal_type") else SignalType.FACTUAL,
|
|
183
|
+
created_at=d["created_at"],
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def get_all_facts(self, profile_id: str) -> list[AtomicFact]:
|
|
187
|
+
"""All facts for a profile, newest first."""
|
|
188
|
+
rows = self.execute(
|
|
189
|
+
"SELECT * FROM atomic_facts WHERE profile_id = ? ORDER BY created_at DESC",
|
|
190
|
+
(profile_id,),
|
|
191
|
+
)
|
|
192
|
+
return [self._row_to_fact(r) for r in rows]
|
|
193
|
+
|
|
194
|
+
def get_facts_by_entity(self, entity_id: str, profile_id: str) -> list[AtomicFact]:
|
|
195
|
+
"""Facts whose canonical_entities JSON array contains *entity_id*."""
|
|
196
|
+
rows = self.execute(
|
|
197
|
+
"SELECT * FROM atomic_facts WHERE profile_id = ? AND canonical_entities_json LIKE ? "
|
|
198
|
+
"ORDER BY created_at DESC",
|
|
199
|
+
(profile_id, f'%"{entity_id}"%'),
|
|
200
|
+
)
|
|
201
|
+
return [self._row_to_fact(r) for r in rows]
|
|
202
|
+
|
|
203
|
+
def get_facts_by_type(self, fact_type: FactType, profile_id: str) -> list[AtomicFact]:
|
|
204
|
+
"""All facts of a given type for a profile."""
|
|
205
|
+
rows = self.execute(
|
|
206
|
+
"SELECT * FROM atomic_facts WHERE profile_id = ? AND fact_type = ? "
|
|
207
|
+
"ORDER BY created_at DESC",
|
|
208
|
+
(profile_id, fact_type.value),
|
|
209
|
+
)
|
|
210
|
+
return [self._row_to_fact(r) for r in rows]
|
|
211
|
+
|
|
212
|
+
# Allowed columns for partial updates (prevents SQL injection via dict keys)
|
|
213
|
+
_UPDATABLE_FACT_COLUMNS: frozenset[str] = frozenset({
|
|
214
|
+
"content", "fact_type", "entities_json", "canonical_entities_json",
|
|
215
|
+
"observation_date", "referenced_date", "interval_start", "interval_end",
|
|
216
|
+
"confidence", "importance", "evidence_count", "access_count",
|
|
217
|
+
"source_turn_ids_json", "session_id", "embedding",
|
|
218
|
+
"fisher_mean", "fisher_variance", "lifecycle", "langevin_position",
|
|
219
|
+
"emotional_valence", "emotional_arousal", "signal_type",
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
def update_fact(self, fact_id: str, updates: dict[str, Any]) -> None:
|
|
223
|
+
"""Partial update on a fact. JSON-serializes list/dict values."""
|
|
224
|
+
if not updates:
|
|
225
|
+
raise ValueError("updates dict must not be empty")
|
|
226
|
+
bad_keys = set(updates) - self._UPDATABLE_FACT_COLUMNS
|
|
227
|
+
if bad_keys:
|
|
228
|
+
raise ValueError(f"Disallowed column(s): {bad_keys}")
|
|
229
|
+
clean: dict[str, Any] = {}
|
|
230
|
+
for k, v in updates.items():
|
|
231
|
+
if isinstance(v, (list, dict)):
|
|
232
|
+
clean[k] = json.dumps(v)
|
|
233
|
+
elif isinstance(v, (MemoryLifecycle, FactType, SignalType)):
|
|
234
|
+
clean[k] = v.value
|
|
235
|
+
else:
|
|
236
|
+
clean[k] = v
|
|
237
|
+
set_clause = ", ".join(f"{k} = ?" for k in clean)
|
|
238
|
+
self.execute(
|
|
239
|
+
f"UPDATE atomic_facts SET {set_clause} WHERE fact_id = ?",
|
|
240
|
+
(*clean.values(), fact_id),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def delete_fact(self, fact_id: str) -> None:
|
|
244
|
+
"""Hard-delete a fact."""
|
|
245
|
+
self.execute("DELETE FROM atomic_facts WHERE fact_id = ?", (fact_id,))
|
|
246
|
+
|
|
247
|
+
def get_fact_count(self, profile_id: str) -> int:
|
|
248
|
+
"""Total fact count for a profile."""
|
|
249
|
+
rows = self.execute(
|
|
250
|
+
"SELECT COUNT(*) AS c FROM atomic_facts WHERE profile_id = ?", (profile_id,),
|
|
251
|
+
)
|
|
252
|
+
return int(rows[0]["c"]) if rows else 0
|
|
253
|
+
|
|
254
|
+
def store_entity(self, entity: CanonicalEntity) -> str:
|
|
255
|
+
"""Persist a canonical entity. Returns entity_id."""
|
|
256
|
+
self.execute(
|
|
257
|
+
"""INSERT OR REPLACE INTO canonical_entities
|
|
258
|
+
(entity_id, profile_id, canonical_name, entity_type,
|
|
259
|
+
first_seen, last_seen, fact_count)
|
|
260
|
+
VALUES (?,?,?,?,?,?,?)""",
|
|
261
|
+
(entity.entity_id, entity.profile_id, entity.canonical_name,
|
|
262
|
+
entity.entity_type, entity.first_seen, entity.last_seen,
|
|
263
|
+
entity.fact_count),
|
|
264
|
+
)
|
|
265
|
+
return entity.entity_id
|
|
266
|
+
|
|
267
|
+
def get_entity_by_name(self, name: str, profile_id: str) -> CanonicalEntity | None:
|
|
268
|
+
"""Look up entity by name (case-insensitive)."""
|
|
269
|
+
rows = self.execute(
|
|
270
|
+
"SELECT * FROM canonical_entities WHERE profile_id = ? AND LOWER(canonical_name) = LOWER(?)",
|
|
271
|
+
(profile_id, name),
|
|
272
|
+
)
|
|
273
|
+
if not rows:
|
|
274
|
+
return None
|
|
275
|
+
d = dict(rows[0])
|
|
276
|
+
return CanonicalEntity(
|
|
277
|
+
entity_id=d["entity_id"], profile_id=d["profile_id"],
|
|
278
|
+
canonical_name=d["canonical_name"], entity_type=d["entity_type"],
|
|
279
|
+
first_seen=d["first_seen"], last_seen=d["last_seen"],
|
|
280
|
+
fact_count=d["fact_count"],
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
def store_alias(self, alias: EntityAlias) -> str:
|
|
284
|
+
"""Persist an entity alias. Returns alias_id."""
|
|
285
|
+
self.execute(
|
|
286
|
+
"INSERT OR REPLACE INTO entity_aliases "
|
|
287
|
+
"(alias_id, entity_id, alias, confidence, source) VALUES (?,?,?,?,?)",
|
|
288
|
+
(alias.alias_id, alias.entity_id, alias.alias,
|
|
289
|
+
alias.confidence, alias.source),
|
|
290
|
+
)
|
|
291
|
+
return alias.alias_id
|
|
292
|
+
|
|
293
|
+
def get_aliases_for_entity(self, entity_id: str) -> list[EntityAlias]:
|
|
294
|
+
"""All aliases for a canonical entity."""
|
|
295
|
+
rows = self.execute(
|
|
296
|
+
"SELECT * FROM entity_aliases WHERE entity_id = ?", (entity_id,),
|
|
297
|
+
)
|
|
298
|
+
return [
|
|
299
|
+
EntityAlias(**{k: dict(r)[k] for k in ("alias_id", "entity_id", "alias", "confidence", "source")})
|
|
300
|
+
for r in rows
|
|
301
|
+
]
|
|
302
|
+
|
|
303
|
+
def store_edge(self, edge: GraphEdge) -> str:
|
|
304
|
+
"""Persist a graph edge. Returns edge_id."""
|
|
305
|
+
self.execute(
|
|
306
|
+
"""INSERT OR REPLACE INTO graph_edges
|
|
307
|
+
(edge_id, profile_id, source_id, target_id, edge_type, weight, created_at)
|
|
308
|
+
VALUES (?,?,?,?,?,?,?)""",
|
|
309
|
+
(edge.edge_id, edge.profile_id, edge.source_id, edge.target_id,
|
|
310
|
+
edge.edge_type.value, edge.weight, edge.created_at),
|
|
311
|
+
)
|
|
312
|
+
return edge.edge_id
|
|
313
|
+
|
|
314
|
+
def get_edges_for_node(self, node_id: str, profile_id: str) -> list[GraphEdge]:
|
|
315
|
+
"""All edges where node_id is source or target."""
|
|
316
|
+
rows = self.execute(
|
|
317
|
+
"SELECT * FROM graph_edges WHERE profile_id = ? "
|
|
318
|
+
"AND (source_id = ? OR target_id = ?)",
|
|
319
|
+
(profile_id, node_id, node_id),
|
|
320
|
+
)
|
|
321
|
+
return [
|
|
322
|
+
GraphEdge(
|
|
323
|
+
edge_id=(d := dict(r))["edge_id"], profile_id=d["profile_id"],
|
|
324
|
+
source_id=d["source_id"], target_id=d["target_id"],
|
|
325
|
+
edge_type=EdgeType(d["edge_type"]), weight=d["weight"],
|
|
326
|
+
created_at=d["created_at"],
|
|
327
|
+
)
|
|
328
|
+
for r in rows
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
def store_temporal_event(self, event: TemporalEvent) -> str:
|
|
332
|
+
"""Persist a temporal event. Returns event_id."""
|
|
333
|
+
self.execute(
|
|
334
|
+
"""INSERT OR REPLACE INTO temporal_events
|
|
335
|
+
(event_id, profile_id, entity_id, fact_id,
|
|
336
|
+
observation_date, referenced_date, interval_start, interval_end,
|
|
337
|
+
description)
|
|
338
|
+
VALUES (?,?,?,?,?,?,?,?,?)""",
|
|
339
|
+
(event.event_id, event.profile_id, event.entity_id, event.fact_id,
|
|
340
|
+
event.observation_date, event.referenced_date,
|
|
341
|
+
event.interval_start, event.interval_end, event.description),
|
|
342
|
+
)
|
|
343
|
+
return event.event_id
|
|
344
|
+
|
|
345
|
+
def get_temporal_events(self, entity_id: str, profile_id: str) -> list[TemporalEvent]:
|
|
346
|
+
"""All temporal events for an entity, newest first."""
|
|
347
|
+
rows = self.execute(
|
|
348
|
+
"SELECT * FROM temporal_events WHERE profile_id = ? AND entity_id = ? "
|
|
349
|
+
"ORDER BY observation_date DESC",
|
|
350
|
+
(profile_id, entity_id),
|
|
351
|
+
)
|
|
352
|
+
return [
|
|
353
|
+
TemporalEvent(
|
|
354
|
+
event_id=(d := dict(r))["event_id"], profile_id=d["profile_id"],
|
|
355
|
+
entity_id=d["entity_id"], fact_id=d["fact_id"],
|
|
356
|
+
observation_date=d.get("observation_date"),
|
|
357
|
+
referenced_date=d.get("referenced_date"),
|
|
358
|
+
interval_start=d.get("interval_start"),
|
|
359
|
+
interval_end=d.get("interval_end"),
|
|
360
|
+
description=d.get("description", ""),
|
|
361
|
+
)
|
|
362
|
+
for r in rows
|
|
363
|
+
]
|
|
364
|
+
|
|
365
|
+
def store_bm25_tokens(self, fact_id: str, profile_id: str, tokens: list[str]) -> None:
|
|
366
|
+
"""Persist BM25 tokens for a fact (survives restart)."""
|
|
367
|
+
self.execute(
|
|
368
|
+
"INSERT OR REPLACE INTO bm25_tokens (fact_id, profile_id, tokens) VALUES (?,?,?)",
|
|
369
|
+
(fact_id, profile_id, json.dumps(tokens)),
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def get_all_bm25_tokens(self, profile_id: str) -> dict[str, list[str]]:
|
|
373
|
+
"""Load full BM25 index: fact_id -> token list."""
|
|
374
|
+
rows = self.execute(
|
|
375
|
+
"SELECT fact_id, tokens FROM bm25_tokens WHERE profile_id = ?",
|
|
376
|
+
(profile_id,),
|
|
377
|
+
)
|
|
378
|
+
return {dict(r)["fact_id"]: json.loads(dict(r)["tokens"]) for r in rows}
|
|
379
|
+
|
|
380
|
+
def search_facts_fts(self, query: str, profile_id: str, limit: int = 20) -> list[AtomicFact]:
|
|
381
|
+
"""Full-text search via FTS5, joined to facts table for reconstruction."""
|
|
382
|
+
rows = self.execute(
|
|
383
|
+
"""SELECT f.* FROM atomic_facts_fts AS fts
|
|
384
|
+
JOIN atomic_facts AS f ON f.fact_id = fts.fact_id
|
|
385
|
+
WHERE fts.atomic_facts_fts MATCH ? AND f.profile_id = ?
|
|
386
|
+
ORDER BY fts.rank LIMIT ?""",
|
|
387
|
+
(query, profile_id, limit),
|
|
388
|
+
)
|
|
389
|
+
return [self._row_to_fact(r) for r in rows]
|
|
390
|
+
|
|
391
|
+
def list_tables(self) -> set[str]:
|
|
392
|
+
"""All table names in the database."""
|
|
393
|
+
rows = self.execute(
|
|
394
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
|
395
|
+
)
|
|
396
|
+
return {dict(r)["name"] for r in rows}
|
|
397
|
+
|
|
398
|
+
def get_config(self, key: str) -> str | None:
|
|
399
|
+
"""Read a config value by key."""
|
|
400
|
+
rows = self.execute("SELECT value FROM config WHERE key = ?", (key,))
|
|
401
|
+
return str(rows[0]["value"]) if rows else None
|
|
402
|
+
|
|
403
|
+
def set_config(self, key: str, value: str) -> None:
|
|
404
|
+
"""Write a config value (upsert)."""
|
|
405
|
+
from datetime import UTC, datetime
|
|
406
|
+
self.execute(
|
|
407
|
+
"INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?,?,?)",
|
|
408
|
+
(key, value, datetime.now(UTC).isoformat()),
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
# ------------------------------------------------------------------
|
|
412
|
+
# Phase 0.6: Missing methods (BLOCKER / CRITICAL / HIGH)
|
|
413
|
+
# ------------------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
def get_fact(self, fact_id: str) -> AtomicFact | None:
|
|
416
|
+
"""Get a single fact by ID."""
|
|
417
|
+
rows = self.execute(
|
|
418
|
+
"SELECT * FROM atomic_facts WHERE fact_id = ?", (fact_id,),
|
|
419
|
+
)
|
|
420
|
+
return self._row_to_fact(rows[0]) if rows else None
|
|
421
|
+
|
|
422
|
+
def get_facts_by_ids(
|
|
423
|
+
self, fact_ids: list[str], profile_id: str,
|
|
424
|
+
) -> list[AtomicFact]:
|
|
425
|
+
"""Get multiple facts by their IDs, scoped to a profile."""
|
|
426
|
+
if not fact_ids:
|
|
427
|
+
return []
|
|
428
|
+
placeholders = ",".join("?" for _ in fact_ids)
|
|
429
|
+
rows = self.execute(
|
|
430
|
+
f"SELECT * FROM atomic_facts WHERE fact_id IN ({placeholders}) "
|
|
431
|
+
f"AND profile_id = ? ORDER BY created_at DESC",
|
|
432
|
+
(*fact_ids, profile_id),
|
|
433
|
+
)
|
|
434
|
+
return [self._row_to_fact(r) for r in rows]
|
|
435
|
+
|
|
436
|
+
def store_entity_profile(self, ep: EntityProfile) -> str:
|
|
437
|
+
"""Persist an entity profile. Returns profile_entry_id."""
|
|
438
|
+
self.execute(
|
|
439
|
+
"""INSERT OR REPLACE INTO entity_profiles
|
|
440
|
+
(profile_entry_id, entity_id, profile_id,
|
|
441
|
+
knowledge_summary, fact_ids_json, last_updated)
|
|
442
|
+
VALUES (?,?,?,?,?,?)""",
|
|
443
|
+
(ep.profile_entry_id, ep.entity_id, ep.profile_id,
|
|
444
|
+
ep.knowledge_summary, json.dumps(ep.fact_ids), ep.last_updated),
|
|
445
|
+
)
|
|
446
|
+
return ep.profile_entry_id
|
|
447
|
+
|
|
448
|
+
def get_entity_profiles_by_entity(
|
|
449
|
+
self, entity_id: str, profile_id: str,
|
|
450
|
+
) -> list[EntityProfile]:
|
|
451
|
+
"""All profile entries for an entity within a profile scope."""
|
|
452
|
+
rows = self.execute(
|
|
453
|
+
"SELECT * FROM entity_profiles WHERE entity_id = ? AND profile_id = ? "
|
|
454
|
+
"ORDER BY last_updated DESC",
|
|
455
|
+
(entity_id, profile_id),
|
|
456
|
+
)
|
|
457
|
+
return [
|
|
458
|
+
EntityProfile(
|
|
459
|
+
profile_entry_id=(d := dict(r))["profile_entry_id"],
|
|
460
|
+
entity_id=d["entity_id"], profile_id=d["profile_id"],
|
|
461
|
+
knowledge_summary=d["knowledge_summary"],
|
|
462
|
+
fact_ids=_jl(d.get("fact_ids_json")),
|
|
463
|
+
last_updated=d["last_updated"],
|
|
464
|
+
)
|
|
465
|
+
for r in rows
|
|
466
|
+
]
|
|
467
|
+
|
|
468
|
+
def store_scene(self, scene: MemoryScene) -> str:
|
|
469
|
+
"""Persist a memory scene. Returns scene_id."""
|
|
470
|
+
self.execute(
|
|
471
|
+
"""INSERT OR REPLACE INTO memory_scenes
|
|
472
|
+
(scene_id, profile_id, theme, fact_ids_json,
|
|
473
|
+
entity_ids_json, created_at, last_updated)
|
|
474
|
+
VALUES (?,?,?,?,?,?,?)""",
|
|
475
|
+
(scene.scene_id, scene.profile_id, scene.theme,
|
|
476
|
+
json.dumps(scene.fact_ids), json.dumps(scene.entity_ids),
|
|
477
|
+
scene.created_at, scene.last_updated),
|
|
478
|
+
)
|
|
479
|
+
return scene.scene_id
|
|
480
|
+
|
|
481
|
+
def _row_to_scene(self, row: sqlite3.Row) -> MemoryScene:
|
|
482
|
+
"""Deserialize a row into MemoryScene."""
|
|
483
|
+
d = dict(row)
|
|
484
|
+
return MemoryScene(
|
|
485
|
+
scene_id=d["scene_id"], profile_id=d["profile_id"],
|
|
486
|
+
theme=d.get("theme", ""),
|
|
487
|
+
fact_ids=_jl(d.get("fact_ids_json")),
|
|
488
|
+
entity_ids=_jl(d.get("entity_ids_json")),
|
|
489
|
+
created_at=d["created_at"], last_updated=d["last_updated"],
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
def get_scene(self, scene_id: str) -> MemoryScene | None:
|
|
493
|
+
"""Get a scene by ID."""
|
|
494
|
+
rows = self.execute(
|
|
495
|
+
"SELECT * FROM memory_scenes WHERE scene_id = ?", (scene_id,),
|
|
496
|
+
)
|
|
497
|
+
return self._row_to_scene(rows[0]) if rows else None
|
|
498
|
+
|
|
499
|
+
def get_all_scenes(self, profile_id: str) -> list[MemoryScene]:
|
|
500
|
+
"""All scenes for a profile, newest first."""
|
|
501
|
+
rows = self.execute(
|
|
502
|
+
"SELECT * FROM memory_scenes WHERE profile_id = ? "
|
|
503
|
+
"ORDER BY last_updated DESC",
|
|
504
|
+
(profile_id,),
|
|
505
|
+
)
|
|
506
|
+
return [self._row_to_scene(r) for r in rows]
|
|
507
|
+
|
|
508
|
+
def get_scenes_for_fact(
|
|
509
|
+
self, fact_id: str, profile_id: str,
|
|
510
|
+
) -> list[MemoryScene]:
|
|
511
|
+
"""All scenes whose fact_ids JSON array contains *fact_id*."""
|
|
512
|
+
rows = self.execute(
|
|
513
|
+
"SELECT * FROM memory_scenes WHERE profile_id = ? "
|
|
514
|
+
"AND fact_ids_json LIKE ? ORDER BY last_updated DESC",
|
|
515
|
+
(profile_id, f'%"{fact_id}"%'),
|
|
516
|
+
)
|
|
517
|
+
return [self._row_to_scene(r) for r in rows]
|
|
518
|
+
|
|
519
|
+
def increment_entity_fact_count(self, entity_id: str) -> None:
|
|
520
|
+
"""Atomically increment fact_count for a canonical entity."""
|
|
521
|
+
self.execute(
|
|
522
|
+
"UPDATE canonical_entities SET fact_count = fact_count + 1 "
|
|
523
|
+
"WHERE entity_id = ?",
|
|
524
|
+
(entity_id,),
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
def store_trust_score(self, ts: TrustScore) -> str:
|
|
528
|
+
"""Persist a trust score. Returns trust_id."""
|
|
529
|
+
self.execute(
|
|
530
|
+
"""INSERT OR REPLACE INTO trust_scores
|
|
531
|
+
(trust_id, profile_id, target_type, target_id,
|
|
532
|
+
trust_score, evidence_count, last_updated)
|
|
533
|
+
VALUES (?,?,?,?,?,?,?)""",
|
|
534
|
+
(ts.trust_id, ts.profile_id, ts.target_type, ts.target_id,
|
|
535
|
+
ts.trust_score, ts.evidence_count, ts.last_updated),
|
|
536
|
+
)
|
|
537
|
+
return ts.trust_id
|
|
538
|
+
|
|
539
|
+
def get_trust_score(
|
|
540
|
+
self, target_type: str, target_id: str, profile_id: str,
|
|
541
|
+
) -> TrustScore | None:
|
|
542
|
+
"""Look up trust score for a specific target."""
|
|
543
|
+
rows = self.execute(
|
|
544
|
+
"SELECT * FROM trust_scores WHERE target_type = ? "
|
|
545
|
+
"AND target_id = ? AND profile_id = ?",
|
|
546
|
+
(target_type, target_id, profile_id),
|
|
547
|
+
)
|
|
548
|
+
if not rows:
|
|
549
|
+
return None
|
|
550
|
+
d = dict(rows[0])
|
|
551
|
+
return TrustScore(
|
|
552
|
+
trust_id=d["trust_id"], profile_id=d["profile_id"],
|
|
553
|
+
target_type=d["target_type"], target_id=d["target_id"],
|
|
554
|
+
trust_score=d["trust_score"], evidence_count=d["evidence_count"],
|
|
555
|
+
last_updated=d["last_updated"],
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
def store_consolidation_action(self, action: ConsolidationAction) -> str:
|
|
559
|
+
"""Log a consolidation decision. Returns action_id."""
|
|
560
|
+
self.execute(
|
|
561
|
+
"""INSERT OR REPLACE INTO consolidation_log
|
|
562
|
+
(action_id, profile_id, action_type, new_fact_id,
|
|
563
|
+
existing_fact_id, reason, timestamp)
|
|
564
|
+
VALUES (?,?,?,?,?,?,?)""",
|
|
565
|
+
(action.action_id, action.profile_id,
|
|
566
|
+
action.action_type.value, action.new_fact_id,
|
|
567
|
+
action.existing_fact_id, action.reason, action.timestamp),
|
|
568
|
+
)
|
|
569
|
+
return action.action_id
|
|
570
|
+
|
|
571
|
+
def get_temporal_events_by_range(
|
|
572
|
+
self, profile_id: str, start_date: str, end_date: str,
|
|
573
|
+
) -> list[TemporalEvent]:
|
|
574
|
+
"""Temporal events within a date range (inclusive)."""
|
|
575
|
+
rows = self.execute(
|
|
576
|
+
"SELECT * FROM temporal_events WHERE profile_id = ? "
|
|
577
|
+
"AND (referenced_date BETWEEN ? AND ? "
|
|
578
|
+
" OR observation_date BETWEEN ? AND ?) "
|
|
579
|
+
"ORDER BY observation_date DESC",
|
|
580
|
+
(profile_id, start_date, end_date, start_date, end_date),
|
|
581
|
+
)
|
|
582
|
+
return [
|
|
583
|
+
TemporalEvent(
|
|
584
|
+
event_id=(d := dict(r))["event_id"],
|
|
585
|
+
profile_id=d["profile_id"],
|
|
586
|
+
entity_id=d["entity_id"], fact_id=d["fact_id"],
|
|
587
|
+
observation_date=d.get("observation_date"),
|
|
588
|
+
referenced_date=d.get("referenced_date"),
|
|
589
|
+
interval_start=d.get("interval_start"),
|
|
590
|
+
interval_end=d.get("interval_end"),
|
|
591
|
+
description=d.get("description", ""),
|
|
592
|
+
)
|
|
593
|
+
for r in rows
|
|
594
|
+
]
|