superlocalmemory 2.8.6 → 3.0.0
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 +1 -1
- 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
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
-
# Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
|
|
3
|
-
"""Background scheduler for periodic lifecycle evaluation and enforcement.
|
|
4
|
-
|
|
5
|
-
Runs on a configurable interval (default: 6 hours) to:
|
|
6
|
-
1. Evaluate all memories for lifecycle transitions
|
|
7
|
-
2. Execute recommended transitions
|
|
8
|
-
3. Enforce bounded growth limits
|
|
9
|
-
|
|
10
|
-
Uses daemon threading — does not prevent process exit.
|
|
11
|
-
"""
|
|
12
|
-
import threading
|
|
13
|
-
from datetime import datetime
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
from typing import Optional, Dict, Any, List
|
|
16
|
-
|
|
17
|
-
from .lifecycle_engine import LifecycleEngine
|
|
18
|
-
from .lifecycle_evaluator import LifecycleEvaluator
|
|
19
|
-
from .bounded_growth import BoundedGrowthEnforcer
|
|
20
|
-
|
|
21
|
-
# Default interval: 6 hours
|
|
22
|
-
DEFAULT_INTERVAL_SECONDS = 21600
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class LifecycleScheduler:
|
|
26
|
-
"""Background scheduler for periodic lifecycle evaluation.
|
|
27
|
-
|
|
28
|
-
Orchestrates the evaluator, engine, and bounded growth enforcer
|
|
29
|
-
on a configurable timer interval.
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
def __init__(
|
|
33
|
-
self,
|
|
34
|
-
db_path: Optional[str] = None,
|
|
35
|
-
config_path: Optional[str] = None,
|
|
36
|
-
interval_seconds: int = DEFAULT_INTERVAL_SECONDS,
|
|
37
|
-
):
|
|
38
|
-
if db_path is None:
|
|
39
|
-
db_path = str(Path.home() / ".claude-memory" / "memory.db")
|
|
40
|
-
self._db_path = str(db_path)
|
|
41
|
-
self._config_path = config_path
|
|
42
|
-
self.interval_seconds = interval_seconds
|
|
43
|
-
|
|
44
|
-
self._engine = LifecycleEngine(self._db_path, config_path=config_path)
|
|
45
|
-
self._evaluator = LifecycleEvaluator(self._db_path, config_path=config_path)
|
|
46
|
-
self._enforcer = BoundedGrowthEnforcer(self._db_path, config_path=config_path)
|
|
47
|
-
|
|
48
|
-
self._timer: Optional[threading.Timer] = None
|
|
49
|
-
self._running = False
|
|
50
|
-
self._lock = threading.Lock()
|
|
51
|
-
|
|
52
|
-
@property
|
|
53
|
-
def is_running(self) -> bool:
|
|
54
|
-
"""Whether the scheduler is currently running."""
|
|
55
|
-
return self._running
|
|
56
|
-
|
|
57
|
-
def start(self) -> None:
|
|
58
|
-
"""Start the background scheduler."""
|
|
59
|
-
with self._lock:
|
|
60
|
-
if self._running:
|
|
61
|
-
return
|
|
62
|
-
self._running = True
|
|
63
|
-
self._schedule_next()
|
|
64
|
-
|
|
65
|
-
def stop(self) -> None:
|
|
66
|
-
"""Stop the background scheduler."""
|
|
67
|
-
with self._lock:
|
|
68
|
-
self._running = False
|
|
69
|
-
if self._timer is not None:
|
|
70
|
-
self._timer.cancel()
|
|
71
|
-
self._timer = None
|
|
72
|
-
|
|
73
|
-
def run_now(self) -> Dict[str, Any]:
|
|
74
|
-
"""Execute a lifecycle evaluation cycle immediately.
|
|
75
|
-
|
|
76
|
-
Returns:
|
|
77
|
-
Dict with evaluation results, enforcement results, and timestamp
|
|
78
|
-
"""
|
|
79
|
-
return self._execute_cycle()
|
|
80
|
-
|
|
81
|
-
def _schedule_next(self) -> None:
|
|
82
|
-
"""Schedule the next evaluation cycle."""
|
|
83
|
-
self._timer = threading.Timer(self.interval_seconds, self._run_cycle)
|
|
84
|
-
self._timer.daemon = True
|
|
85
|
-
self._timer.start()
|
|
86
|
-
|
|
87
|
-
def _run_cycle(self) -> None:
|
|
88
|
-
"""Run one evaluation cycle, then schedule the next."""
|
|
89
|
-
try:
|
|
90
|
-
self._execute_cycle()
|
|
91
|
-
except Exception:
|
|
92
|
-
pass # Scheduler must not crash
|
|
93
|
-
finally:
|
|
94
|
-
with self._lock:
|
|
95
|
-
if self._running:
|
|
96
|
-
self._schedule_next()
|
|
97
|
-
|
|
98
|
-
def _execute_cycle(self) -> Dict[str, Any]:
|
|
99
|
-
"""Core evaluation + enforcement logic.
|
|
100
|
-
|
|
101
|
-
1. Evaluate all memories for potential transitions
|
|
102
|
-
2. Execute recommended transitions via the engine
|
|
103
|
-
3. Enforce bounded growth limits
|
|
104
|
-
"""
|
|
105
|
-
# Step 1: Evaluate
|
|
106
|
-
recommendations = self._evaluator.evaluate_memories()
|
|
107
|
-
|
|
108
|
-
# Step 2: Execute transitions
|
|
109
|
-
transitioned = 0
|
|
110
|
-
transition_results: List[Dict] = []
|
|
111
|
-
for rec in recommendations:
|
|
112
|
-
result = self._engine.transition_memory(
|
|
113
|
-
rec["memory_id"], rec["to_state"], reason=rec["reason"]
|
|
114
|
-
)
|
|
115
|
-
if result.get("success"):
|
|
116
|
-
transitioned += 1
|
|
117
|
-
transition_results.append(result)
|
|
118
|
-
|
|
119
|
-
# Step 3: Enforce bounds
|
|
120
|
-
enforcement = self._enforcer.enforce_bounds()
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
"timestamp": datetime.now().isoformat(),
|
|
124
|
-
"evaluation": {
|
|
125
|
-
"recommendations": recommendations,
|
|
126
|
-
"transitioned": transitioned,
|
|
127
|
-
"transition_results": transition_results,
|
|
128
|
-
},
|
|
129
|
-
"enforcement": enforcement,
|
|
130
|
-
}
|
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
-
# Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
|
|
3
|
-
"""Retention policy loading, evaluation, and enforcement.
|
|
4
|
-
|
|
5
|
-
Manages retention policies that determine how long memories must be kept
|
|
6
|
-
in specific states. Supports GDPR (right to erasure), EU AI Act (audit
|
|
7
|
-
retention), and HIPAA (medical record retention) compliance frameworks.
|
|
8
|
-
|
|
9
|
-
Policies are stored in a `retention_policies` table alongside the memories
|
|
10
|
-
database. Each policy specifies criteria (tags, project_name) for matching
|
|
11
|
-
memories and an action (retain, archive, tombstone) with a retention period.
|
|
12
|
-
"""
|
|
13
|
-
import json
|
|
14
|
-
import logging
|
|
15
|
-
import sqlite3
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
from typing import Any, Dict, List, Optional, Set
|
|
18
|
-
|
|
19
|
-
logger = logging.getLogger(__name__)
|
|
20
|
-
|
|
21
|
-
_POLICIES_TABLE_SQL = """
|
|
22
|
-
CREATE TABLE IF NOT EXISTS retention_policies (
|
|
23
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
24
|
-
name TEXT NOT NULL,
|
|
25
|
-
retention_days INTEGER NOT NULL,
|
|
26
|
-
framework TEXT NOT NULL,
|
|
27
|
-
action TEXT NOT NULL,
|
|
28
|
-
applies_to TEXT NOT NULL,
|
|
29
|
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
30
|
-
)
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class RetentionPolicyManager:
|
|
35
|
-
"""Manages retention policies for lifecycle enforcement.
|
|
36
|
-
|
|
37
|
-
Evaluates which compliance policies apply to each memory based on
|
|
38
|
-
tag and project_name matching. When multiple policies match, the
|
|
39
|
-
strictest (shortest retention_days) wins.
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
def __init__(self, db_path: Optional[str] = None):
|
|
43
|
-
self._db_path = db_path
|
|
44
|
-
if db_path:
|
|
45
|
-
self._ensure_table()
|
|
46
|
-
|
|
47
|
-
# ------------------------------------------------------------------
|
|
48
|
-
# Internal helpers
|
|
49
|
-
# ------------------------------------------------------------------
|
|
50
|
-
|
|
51
|
-
def _connect(self) -> sqlite3.Connection:
|
|
52
|
-
"""Open a connection to the database."""
|
|
53
|
-
conn = sqlite3.connect(self._db_path)
|
|
54
|
-
conn.row_factory = sqlite3.Row
|
|
55
|
-
return conn
|
|
56
|
-
|
|
57
|
-
def _ensure_table(self) -> None:
|
|
58
|
-
"""Create the retention_policies table if it doesn't exist."""
|
|
59
|
-
conn = self._connect()
|
|
60
|
-
try:
|
|
61
|
-
conn.execute(_POLICIES_TABLE_SQL)
|
|
62
|
-
conn.commit()
|
|
63
|
-
finally:
|
|
64
|
-
conn.close()
|
|
65
|
-
|
|
66
|
-
# ------------------------------------------------------------------
|
|
67
|
-
# Public API
|
|
68
|
-
# ------------------------------------------------------------------
|
|
69
|
-
|
|
70
|
-
def create_policy(
|
|
71
|
-
self,
|
|
72
|
-
name: str,
|
|
73
|
-
retention_days: int,
|
|
74
|
-
framework: str,
|
|
75
|
-
action: str,
|
|
76
|
-
applies_to: Dict[str, Any],
|
|
77
|
-
) -> int:
|
|
78
|
-
"""Create a new retention policy.
|
|
79
|
-
|
|
80
|
-
Args:
|
|
81
|
-
name: Human-readable policy name.
|
|
82
|
-
retention_days: Minimum days to retain (0 = immediate action).
|
|
83
|
-
framework: Compliance framework (gdpr, hipaa, eu_ai_act, internal).
|
|
84
|
-
action: What to do (retain, archive, tombstone).
|
|
85
|
-
applies_to: Criteria dict with optional keys: tags, project_name.
|
|
86
|
-
|
|
87
|
-
Returns:
|
|
88
|
-
The auto-generated policy ID.
|
|
89
|
-
"""
|
|
90
|
-
conn = self._connect()
|
|
91
|
-
try:
|
|
92
|
-
cursor = conn.execute(
|
|
93
|
-
"INSERT INTO retention_policies (name, retention_days, framework, action, applies_to) "
|
|
94
|
-
"VALUES (?, ?, ?, ?, ?)",
|
|
95
|
-
(name, retention_days, framework, action, json.dumps(applies_to)),
|
|
96
|
-
)
|
|
97
|
-
conn.commit()
|
|
98
|
-
return cursor.lastrowid
|
|
99
|
-
finally:
|
|
100
|
-
conn.close()
|
|
101
|
-
|
|
102
|
-
def list_policies(self) -> List[Dict[str, Any]]:
|
|
103
|
-
"""Return all retention policies as a list of dicts."""
|
|
104
|
-
conn = self._connect()
|
|
105
|
-
try:
|
|
106
|
-
rows = conn.execute("SELECT * FROM retention_policies ORDER BY id").fetchall()
|
|
107
|
-
return [self._row_to_dict(r) for r in rows]
|
|
108
|
-
finally:
|
|
109
|
-
conn.close()
|
|
110
|
-
|
|
111
|
-
def load_policies(self, path: str) -> int:
|
|
112
|
-
"""Load retention policies from a JSON file.
|
|
113
|
-
|
|
114
|
-
The file must contain a JSON array of policy objects, each with
|
|
115
|
-
keys: name, retention_days, framework, action, applies_to.
|
|
116
|
-
|
|
117
|
-
Args:
|
|
118
|
-
path: Absolute or relative path to the JSON policy file.
|
|
119
|
-
|
|
120
|
-
Returns:
|
|
121
|
-
Number of policies loaded. Returns 0 if file is missing or
|
|
122
|
-
contains invalid data, without raising an exception.
|
|
123
|
-
"""
|
|
124
|
-
policy_path = Path(path)
|
|
125
|
-
if not policy_path.exists():
|
|
126
|
-
logger.debug("Policy file not found: %s", path)
|
|
127
|
-
return 0
|
|
128
|
-
|
|
129
|
-
try:
|
|
130
|
-
data = json.loads(policy_path.read_text(encoding="utf-8"))
|
|
131
|
-
except (json.JSONDecodeError, OSError) as exc:
|
|
132
|
-
logger.warning("Failed to read policy file %s: %s", path, exc)
|
|
133
|
-
return 0
|
|
134
|
-
|
|
135
|
-
if not isinstance(data, list):
|
|
136
|
-
logger.warning("Policy file must contain a JSON array: %s", path)
|
|
137
|
-
return 0
|
|
138
|
-
|
|
139
|
-
count = 0
|
|
140
|
-
for entry in data:
|
|
141
|
-
try:
|
|
142
|
-
self.create_policy(
|
|
143
|
-
name=entry["name"],
|
|
144
|
-
retention_days=entry["retention_days"],
|
|
145
|
-
framework=entry["framework"],
|
|
146
|
-
action=entry["action"],
|
|
147
|
-
applies_to=entry.get("applies_to", {}),
|
|
148
|
-
)
|
|
149
|
-
count += 1
|
|
150
|
-
except (KeyError, TypeError) as exc:
|
|
151
|
-
logger.warning("Skipping invalid policy entry: %s", exc)
|
|
152
|
-
|
|
153
|
-
return count
|
|
154
|
-
|
|
155
|
-
def evaluate_memory(self, memory_id: int) -> Optional[Dict[str, Any]]:
|
|
156
|
-
"""Determine which retention policy applies to a memory.
|
|
157
|
-
|
|
158
|
-
Loads the memory's tags and project_name, then checks every
|
|
159
|
-
policy's ``applies_to`` criteria. If multiple policies match,
|
|
160
|
-
the **strictest** one wins (lowest ``retention_days``).
|
|
161
|
-
|
|
162
|
-
Args:
|
|
163
|
-
memory_id: The memory row ID.
|
|
164
|
-
|
|
165
|
-
Returns:
|
|
166
|
-
A dict with ``policy_name``, ``action``, ``retention_days``,
|
|
167
|
-
and ``framework``; or ``None`` if no policy matches.
|
|
168
|
-
"""
|
|
169
|
-
conn = self._connect()
|
|
170
|
-
try:
|
|
171
|
-
mem_row = conn.execute(
|
|
172
|
-
"SELECT tags, project_name FROM memories WHERE id = ?",
|
|
173
|
-
(memory_id,),
|
|
174
|
-
).fetchone()
|
|
175
|
-
if mem_row is None:
|
|
176
|
-
return None
|
|
177
|
-
|
|
178
|
-
mem_tags = self._parse_json_field(mem_row["tags"])
|
|
179
|
-
mem_project = mem_row["project_name"]
|
|
180
|
-
|
|
181
|
-
policies = conn.execute(
|
|
182
|
-
"SELECT * FROM retention_policies ORDER BY retention_days ASC"
|
|
183
|
-
).fetchall()
|
|
184
|
-
|
|
185
|
-
for policy in policies:
|
|
186
|
-
criteria = self._parse_json_field(policy["applies_to"])
|
|
187
|
-
if self._policy_matches(criteria, mem_tags, mem_project):
|
|
188
|
-
return {
|
|
189
|
-
"policy_name": policy["name"],
|
|
190
|
-
"action": policy["action"],
|
|
191
|
-
"retention_days": policy["retention_days"],
|
|
192
|
-
"framework": policy["framework"],
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return None
|
|
196
|
-
finally:
|
|
197
|
-
conn.close()
|
|
198
|
-
|
|
199
|
-
def get_protected_memory_ids(self) -> Set[int]:
|
|
200
|
-
"""Return the set of memory IDs protected by any ``retain`` policy.
|
|
201
|
-
|
|
202
|
-
A memory is protected if at least one policy with
|
|
203
|
-
``action='retain'`` matches its tags or project_name.
|
|
204
|
-
"""
|
|
205
|
-
conn = self._connect()
|
|
206
|
-
try:
|
|
207
|
-
retain_policies = conn.execute(
|
|
208
|
-
"SELECT * FROM retention_policies WHERE action = 'retain'"
|
|
209
|
-
).fetchall()
|
|
210
|
-
if not retain_policies:
|
|
211
|
-
return set()
|
|
212
|
-
|
|
213
|
-
memories = conn.execute(
|
|
214
|
-
"SELECT id, tags, project_name FROM memories"
|
|
215
|
-
).fetchall()
|
|
216
|
-
|
|
217
|
-
protected: Set[int] = set()
|
|
218
|
-
for mem in memories:
|
|
219
|
-
mem_tags = self._parse_json_field(mem["tags"])
|
|
220
|
-
mem_project = mem["project_name"]
|
|
221
|
-
for policy in retain_policies:
|
|
222
|
-
criteria = self._parse_json_field(policy["applies_to"])
|
|
223
|
-
if self._policy_matches(criteria, mem_tags, mem_project):
|
|
224
|
-
protected.add(mem["id"])
|
|
225
|
-
break # One matching retain policy is enough
|
|
226
|
-
|
|
227
|
-
return protected
|
|
228
|
-
finally:
|
|
229
|
-
conn.close()
|
|
230
|
-
|
|
231
|
-
# ------------------------------------------------------------------
|
|
232
|
-
# Private helpers
|
|
233
|
-
# ------------------------------------------------------------------
|
|
234
|
-
|
|
235
|
-
@staticmethod
|
|
236
|
-
def _row_to_dict(row: sqlite3.Row) -> Dict[str, Any]:
|
|
237
|
-
"""Convert a sqlite3.Row to a plain dict with parsed applies_to."""
|
|
238
|
-
d = dict(row)
|
|
239
|
-
if "applies_to" in d and isinstance(d["applies_to"], str):
|
|
240
|
-
try:
|
|
241
|
-
d["applies_to"] = json.loads(d["applies_to"])
|
|
242
|
-
except (json.JSONDecodeError, TypeError):
|
|
243
|
-
d["applies_to"] = {}
|
|
244
|
-
return d
|
|
245
|
-
|
|
246
|
-
@staticmethod
|
|
247
|
-
def _parse_json_field(value: Any) -> Any:
|
|
248
|
-
"""Parse a JSON string field; return as-is if already parsed."""
|
|
249
|
-
if isinstance(value, str):
|
|
250
|
-
try:
|
|
251
|
-
return json.loads(value)
|
|
252
|
-
except (json.JSONDecodeError, TypeError):
|
|
253
|
-
return value
|
|
254
|
-
return value if value is not None else []
|
|
255
|
-
|
|
256
|
-
@staticmethod
|
|
257
|
-
def _policy_matches(
|
|
258
|
-
criteria: Any, mem_tags: Any, mem_project: Optional[str]
|
|
259
|
-
) -> bool:
|
|
260
|
-
"""Check if a policy's applies_to criteria match a memory.
|
|
261
|
-
|
|
262
|
-
Matching rules:
|
|
263
|
-
- If criteria has ``tags``: memory must have at least one
|
|
264
|
-
overlapping tag.
|
|
265
|
-
- If criteria has ``project_name``: memory's project_name
|
|
266
|
-
must equal the criteria value.
|
|
267
|
-
- If criteria is empty (``{}``): the policy does NOT match
|
|
268
|
-
any memory (opt-in only).
|
|
269
|
-
"""
|
|
270
|
-
if not isinstance(criteria, dict) or not criteria:
|
|
271
|
-
return False
|
|
272
|
-
|
|
273
|
-
matched = True # Assume match; any failing criterion flips to False
|
|
274
|
-
|
|
275
|
-
if "tags" in criteria:
|
|
276
|
-
policy_tags = set(criteria["tags"]) if criteria["tags"] else set()
|
|
277
|
-
memory_tags = set(mem_tags) if isinstance(mem_tags, list) else set()
|
|
278
|
-
if not policy_tags & memory_tags:
|
|
279
|
-
matched = False
|
|
280
|
-
|
|
281
|
-
if "project_name" in criteria:
|
|
282
|
-
if mem_project != criteria["project_name"]:
|
|
283
|
-
matched = False
|
|
284
|
-
|
|
285
|
-
return matched
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
-
# Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
|
|
3
|
-
"""Tests for bounded growth enforcement — memory count limits.
|
|
4
|
-
"""
|
|
5
|
-
import sqlite3
|
|
6
|
-
import tempfile
|
|
7
|
-
import os
|
|
8
|
-
import sys
|
|
9
|
-
import json
|
|
10
|
-
from datetime import datetime, timedelta
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
|
|
13
|
-
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class TestBoundedGrowth:
|
|
17
|
-
"""Test bounded growth enforcement and memory scoring."""
|
|
18
|
-
|
|
19
|
-
def setup_method(self):
|
|
20
|
-
self.tmp_dir = tempfile.mkdtemp()
|
|
21
|
-
self.db_path = os.path.join(self.tmp_dir, "test.db")
|
|
22
|
-
conn = sqlite3.connect(self.db_path)
|
|
23
|
-
conn.execute("""
|
|
24
|
-
CREATE TABLE memories (
|
|
25
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26
|
-
content TEXT NOT NULL,
|
|
27
|
-
importance INTEGER DEFAULT 5,
|
|
28
|
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
29
|
-
last_accessed TIMESTAMP,
|
|
30
|
-
access_count INTEGER DEFAULT 0,
|
|
31
|
-
lifecycle_state TEXT DEFAULT 'active',
|
|
32
|
-
lifecycle_updated_at TIMESTAMP,
|
|
33
|
-
lifecycle_history TEXT DEFAULT '[]',
|
|
34
|
-
access_level TEXT DEFAULT 'public',
|
|
35
|
-
profile TEXT DEFAULT 'default'
|
|
36
|
-
)
|
|
37
|
-
""")
|
|
38
|
-
now = datetime.now()
|
|
39
|
-
|
|
40
|
-
# Memory 1: HIGH value — importance 9, accessed today, frequently used
|
|
41
|
-
conn.execute(
|
|
42
|
-
"INSERT INTO memories (content, importance, lifecycle_state, last_accessed, created_at, access_count) VALUES (?, ?, ?, ?, ?, ?)",
|
|
43
|
-
("high value memory", 9, "active", now.isoformat(), (now - timedelta(days=30)).isoformat(), 20),
|
|
44
|
-
)
|
|
45
|
-
# Memory 2: MEDIUM-HIGH — importance 7, accessed 5d ago
|
|
46
|
-
conn.execute(
|
|
47
|
-
"INSERT INTO memories (content, importance, lifecycle_state, last_accessed, created_at, access_count) VALUES (?, ?, ?, ?, ?, ?)",
|
|
48
|
-
("medium high memory", 7, "active", (now - timedelta(days=5)).isoformat(), (now - timedelta(days=60)).isoformat(), 10),
|
|
49
|
-
)
|
|
50
|
-
# Memory 3: MEDIUM — importance 5, accessed 10d ago
|
|
51
|
-
conn.execute(
|
|
52
|
-
"INSERT INTO memories (content, importance, lifecycle_state, last_accessed, created_at, access_count) VALUES (?, ?, ?, ?, ?, ?)",
|
|
53
|
-
("medium memory", 5, "active", (now - timedelta(days=10)).isoformat(), (now - timedelta(days=90)).isoformat(), 5),
|
|
54
|
-
)
|
|
55
|
-
# Memory 4: LOW — importance 3, accessed 20d ago, rarely used
|
|
56
|
-
conn.execute(
|
|
57
|
-
"INSERT INTO memories (content, importance, lifecycle_state, last_accessed, created_at, access_count) VALUES (?, ?, ?, ?, ?, ?)",
|
|
58
|
-
("low value memory", 3, "active", (now - timedelta(days=20)).isoformat(), (now - timedelta(days=120)).isoformat(), 2),
|
|
59
|
-
)
|
|
60
|
-
# Memory 5: LOWEST — importance 1, accessed 40d ago, never reused
|
|
61
|
-
conn.execute(
|
|
62
|
-
"INSERT INTO memories (content, importance, lifecycle_state, last_accessed, created_at, access_count) VALUES (?, ?, ?, ?, ?, ?)",
|
|
63
|
-
("lowest value memory", 1, "active", (now - timedelta(days=40)).isoformat(), (now - timedelta(days=150)).isoformat(), 0),
|
|
64
|
-
)
|
|
65
|
-
# Memory 6: Warm state (for warm bounds test) — importance 2, stale
|
|
66
|
-
conn.execute(
|
|
67
|
-
"INSERT INTO memories (content, importance, lifecycle_state, last_accessed, created_at, access_count) VALUES (?, ?, ?, ?, ?, ?)",
|
|
68
|
-
("warm memory A", 2, "warm", (now - timedelta(days=50)).isoformat(), (now - timedelta(days=200)).isoformat(), 1),
|
|
69
|
-
)
|
|
70
|
-
# Memory 7: Warm state — importance 4
|
|
71
|
-
conn.execute(
|
|
72
|
-
"INSERT INTO memories (content, importance, lifecycle_state, last_accessed, created_at, access_count) VALUES (?, ?, ?, ?, ?, ?)",
|
|
73
|
-
("warm memory B", 4, "warm", (now - timedelta(days=30)).isoformat(), (now - timedelta(days=100)).isoformat(), 3),
|
|
74
|
-
)
|
|
75
|
-
conn.commit()
|
|
76
|
-
conn.close()
|
|
77
|
-
|
|
78
|
-
def teardown_method(self):
|
|
79
|
-
import shutil
|
|
80
|
-
shutil.rmtree(self.tmp_dir, ignore_errors=True)
|
|
81
|
-
|
|
82
|
-
def test_no_action_under_limit(self):
|
|
83
|
-
"""No transitions when counts are within bounds."""
|
|
84
|
-
from lifecycle.bounded_growth import BoundedGrowthEnforcer
|
|
85
|
-
enforcer = BoundedGrowthEnforcer(self.db_path)
|
|
86
|
-
result = enforcer.enforce_bounds()
|
|
87
|
-
assert result["enforced"] is False
|
|
88
|
-
assert len(result["transitions"]) == 0
|
|
89
|
-
|
|
90
|
-
def test_enforce_active_limit(self):
|
|
91
|
-
"""When active_count > max_active, excess memories transition to warm."""
|
|
92
|
-
from lifecycle.bounded_growth import BoundedGrowthEnforcer
|
|
93
|
-
config_path = os.path.join(self.tmp_dir, "lifecycle_config.json")
|
|
94
|
-
with open(config_path, "w") as f:
|
|
95
|
-
json.dump({"bounds": {"max_active": 3, "max_warm": 100}}, f)
|
|
96
|
-
enforcer = BoundedGrowthEnforcer(self.db_path, config_path=config_path)
|
|
97
|
-
result = enforcer.enforce_bounds()
|
|
98
|
-
assert result["enforced"] is True
|
|
99
|
-
# 5 active, limit 3 -> 2 should transition
|
|
100
|
-
assert len(result["transitions"]) == 2
|
|
101
|
-
|
|
102
|
-
def test_lowest_scoring_evicted_first(self):
|
|
103
|
-
"""The lowest-scoring memories should be the ones transitioned."""
|
|
104
|
-
from lifecycle.bounded_growth import BoundedGrowthEnforcer
|
|
105
|
-
config_path = os.path.join(self.tmp_dir, "lifecycle_config.json")
|
|
106
|
-
with open(config_path, "w") as f:
|
|
107
|
-
json.dump({"bounds": {"max_active": 3, "max_warm": 100}}, f)
|
|
108
|
-
enforcer = BoundedGrowthEnforcer(self.db_path, config_path=config_path)
|
|
109
|
-
result = enforcer.enforce_bounds()
|
|
110
|
-
evicted_ids = {t["memory_id"] for t in result["transitions"]}
|
|
111
|
-
# Memory 5 (importance 1, stale 40d) and Memory 4 (importance 3, stale 20d)
|
|
112
|
-
# should be evicted — lowest scores
|
|
113
|
-
assert 5 in evicted_ids
|
|
114
|
-
assert 4 in evicted_ids
|
|
115
|
-
# Top 3 memories (1, 2, 3) should survive
|
|
116
|
-
assert 1 not in evicted_ids
|
|
117
|
-
assert 2 not in evicted_ids
|
|
118
|
-
assert 3 not in evicted_ids
|
|
119
|
-
|
|
120
|
-
def test_evicted_memories_now_warm(self):
|
|
121
|
-
"""Evicted memories should now be in 'warm' state in the database."""
|
|
122
|
-
from lifecycle.bounded_growth import BoundedGrowthEnforcer
|
|
123
|
-
config_path = os.path.join(self.tmp_dir, "lifecycle_config.json")
|
|
124
|
-
with open(config_path, "w") as f:
|
|
125
|
-
json.dump({"bounds": {"max_active": 3, "max_warm": 100}}, f)
|
|
126
|
-
enforcer = BoundedGrowthEnforcer(self.db_path, config_path=config_path)
|
|
127
|
-
enforcer.enforce_bounds()
|
|
128
|
-
conn = sqlite3.connect(self.db_path)
|
|
129
|
-
row4 = conn.execute("SELECT lifecycle_state FROM memories WHERE id=4").fetchone()
|
|
130
|
-
row5 = conn.execute("SELECT lifecycle_state FROM memories WHERE id=5").fetchone()
|
|
131
|
-
conn.close()
|
|
132
|
-
assert row4[0] == "warm"
|
|
133
|
-
assert row5[0] == "warm"
|
|
134
|
-
|
|
135
|
-
def test_enforce_warm_limit(self):
|
|
136
|
-
"""When warm_count > max_warm, excess warm memories transition to cold."""
|
|
137
|
-
from lifecycle.bounded_growth import BoundedGrowthEnforcer
|
|
138
|
-
config_path = os.path.join(self.tmp_dir, "lifecycle_config.json")
|
|
139
|
-
with open(config_path, "w") as f:
|
|
140
|
-
json.dump({"bounds": {"max_active": 100, "max_warm": 1}}, f)
|
|
141
|
-
enforcer = BoundedGrowthEnforcer(self.db_path, config_path=config_path)
|
|
142
|
-
result = enforcer.enforce_bounds()
|
|
143
|
-
assert result["enforced"] is True
|
|
144
|
-
# 2 warm (ids 6, 7), limit 1 -> 1 transition
|
|
145
|
-
warm_transitions = [t for t in result["transitions"] if t["from_state"] == "warm"]
|
|
146
|
-
assert len(warm_transitions) == 1
|
|
147
|
-
# Memory 6 (importance 2, stale 50d) should be evicted before Memory 7 (importance 4, stale 30d)
|
|
148
|
-
assert warm_transitions[0]["memory_id"] == 6
|
|
149
|
-
|
|
150
|
-
def test_score_memory_importance_matters(self):
|
|
151
|
-
"""Higher importance -> higher score, all else equal."""
|
|
152
|
-
from lifecycle.bounded_growth import BoundedGrowthEnforcer
|
|
153
|
-
enforcer = BoundedGrowthEnforcer(self.db_path)
|
|
154
|
-
scores = enforcer.score_all_memories()
|
|
155
|
-
# Memory 1 (importance 9) should score higher than Memory 5 (importance 1)
|
|
156
|
-
score_map = {s["memory_id"]: s["score"] for s in scores}
|
|
157
|
-
assert score_map[1] > score_map[5]
|
|
158
|
-
|
|
159
|
-
def test_score_memory_recency_matters(self):
|
|
160
|
-
"""More recently accessed -> higher score."""
|
|
161
|
-
from lifecycle.bounded_growth import BoundedGrowthEnforcer
|
|
162
|
-
enforcer = BoundedGrowthEnforcer(self.db_path)
|
|
163
|
-
scores = enforcer.score_all_memories()
|
|
164
|
-
score_map = {s["memory_id"]: s["score"] for s in scores}
|
|
165
|
-
# Memory 1 (accessed today) should score higher than Memory 3 (accessed 10d ago)
|
|
166
|
-
# (both active, Memory 1 also has higher importance, so this should hold)
|
|
167
|
-
assert score_map[1] > score_map[3]
|
|
168
|
-
|
|
169
|
-
def test_score_all_returns_all_active(self):
|
|
170
|
-
"""score_all_memories returns scores for all memories in given state."""
|
|
171
|
-
from lifecycle.bounded_growth import BoundedGrowthEnforcer
|
|
172
|
-
enforcer = BoundedGrowthEnforcer(self.db_path)
|
|
173
|
-
scores = enforcer.score_all_memories(state="active")
|
|
174
|
-
assert len(scores) == 5 # 5 active memories
|
|
175
|
-
|
|
176
|
-
def test_result_structure(self):
|
|
177
|
-
"""enforce_bounds returns properly structured result dict."""
|
|
178
|
-
from lifecycle.bounded_growth import BoundedGrowthEnforcer
|
|
179
|
-
enforcer = BoundedGrowthEnforcer(self.db_path)
|
|
180
|
-
result = enforcer.enforce_bounds()
|
|
181
|
-
assert "enforced" in result
|
|
182
|
-
assert "active_count" in result
|
|
183
|
-
assert "active_limit" in result
|
|
184
|
-
assert "warm_count" in result
|
|
185
|
-
assert "warm_limit" in result
|
|
186
|
-
assert "transitions" in result
|
|
187
|
-
assert isinstance(result["transitions"], list)
|
|
188
|
-
|
|
189
|
-
def test_default_bounds(self):
|
|
190
|
-
"""Default bounds should be max_active=10000, max_warm=5000."""
|
|
191
|
-
from lifecycle.bounded_growth import DEFAULT_BOUNDS
|
|
192
|
-
assert DEFAULT_BOUNDS["max_active"] == 10000
|
|
193
|
-
assert DEFAULT_BOUNDS["max_warm"] == 5000
|