superlocalmemory 4.1.13 → 4.1.15
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/.claude-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +103 -0
- package/README.md +3 -3
- package/package.json +1 -8
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/agents/slm-memory-advisor.md +1 -1
- package/plugin-src/agents/slm-optimize-advisor.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +1 -1
- package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
- package/plugin-src/skills/slm-profile/SKILL.md +1 -1
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-scope/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/scripts/postinstall.js +71 -2
- package/pyproject.toml +0 -250
- package/src/superlocalmemory/__init__.py +0 -82
- package/src/superlocalmemory/access/__init__.py +0 -3
- package/src/superlocalmemory/access/rbac.py +0 -575
- package/src/superlocalmemory/attribution/__init__.py +0 -9
- package/src/superlocalmemory/attribution/signer.py +0 -173
- package/src/superlocalmemory/attribution/watermark.py +0 -189
- package/src/superlocalmemory/brain/__init__.py +0 -5
- package/src/superlocalmemory/brain/truth.py +0 -418
- package/src/superlocalmemory/cli/__init__.py +0 -5
- package/src/superlocalmemory/cli/__main__.py +0 -17
- package/src/superlocalmemory/cli/_lazy_init.py +0 -115
- package/src/superlocalmemory/cli/cache_cmd.py +0 -198
- package/src/superlocalmemory/cli/commands.py +0 -4710
- package/src/superlocalmemory/cli/compress_cmd.py +0 -151
- package/src/superlocalmemory/cli/context_commands.py +0 -193
- package/src/superlocalmemory/cli/daemon.py +0 -909
- package/src/superlocalmemory/cli/db_migrate.py +0 -150
- package/src/superlocalmemory/cli/diagnostics_cmd.py +0 -101
- package/src/superlocalmemory/cli/escape_hatch.py +0 -220
- package/src/superlocalmemory/cli/evidence_cmd.py +0 -103
- package/src/superlocalmemory/cli/gdpr_cmd.py +0 -792
- package/src/superlocalmemory/cli/gdpr_io.py +0 -109
- package/src/superlocalmemory/cli/help_cmd.py +0 -197
- package/src/superlocalmemory/cli/host_upgrades.py +0 -189
- package/src/superlocalmemory/cli/ingest_cmd.py +0 -327
- package/src/superlocalmemory/cli/json_output.py +0 -81
- package/src/superlocalmemory/cli/loop_cmd.py +0 -187
- package/src/superlocalmemory/cli/main.py +0 -1145
- package/src/superlocalmemory/cli/mesh_cmd.py +0 -38
- package/src/superlocalmemory/cli/migrate_cmd.py +0 -55
- package/src/superlocalmemory/cli/ops_cmd.py +0 -281
- package/src/superlocalmemory/cli/optimize_cmd.py +0 -179
- package/src/superlocalmemory/cli/optimize_constants.py +0 -31
- package/src/superlocalmemory/cli/pending_store.py +0 -296
- package/src/superlocalmemory/cli/proxy_cmd.py +0 -108
- package/src/superlocalmemory/cli/scale_engine_cmd.py +0 -56
- package/src/superlocalmemory/cli/service_installer.py +0 -373
- package/src/superlocalmemory/cli/setup_wizard.py +0 -1162
- package/src/superlocalmemory/cli/summary_cmd.py +0 -215
- package/src/superlocalmemory/cli/version_banner.py +0 -202
- package/src/superlocalmemory/cli/wizard_v3426_options.py +0 -129
- package/src/superlocalmemory/code_graph/__init__.py +0 -46
- package/src/superlocalmemory/code_graph/blast_radius.py +0 -177
- package/src/superlocalmemory/code_graph/bridge/__init__.py +0 -36
- package/src/superlocalmemory/code_graph/bridge/entity_resolver.py +0 -490
- package/src/superlocalmemory/code_graph/bridge/event_listeners.py +0 -206
- package/src/superlocalmemory/code_graph/bridge/fact_enricher.py +0 -159
- package/src/superlocalmemory/code_graph/bridge/hebbian_linker.py +0 -170
- package/src/superlocalmemory/code_graph/bridge/maintenance.py +0 -220
- package/src/superlocalmemory/code_graph/bridge/temporal_checker.py +0 -152
- package/src/superlocalmemory/code_graph/changes.py +0 -363
- package/src/superlocalmemory/code_graph/communities.py +0 -299
- package/src/superlocalmemory/code_graph/config.py +0 -154
- package/src/superlocalmemory/code_graph/database.py +0 -526
- package/src/superlocalmemory/code_graph/extractors/__init__.py +0 -95
- package/src/superlocalmemory/code_graph/extractors/python.py +0 -413
- package/src/superlocalmemory/code_graph/extractors/typescript.py +0 -556
- package/src/superlocalmemory/code_graph/flows.py +0 -350
- package/src/superlocalmemory/code_graph/git_hooks.py +0 -226
- package/src/superlocalmemory/code_graph/graph_engine.py +0 -295
- package/src/superlocalmemory/code_graph/graph_store.py +0 -335
- package/src/superlocalmemory/code_graph/incremental.py +0 -200
- package/src/superlocalmemory/code_graph/models.py +0 -130
- package/src/superlocalmemory/code_graph/parser.py +0 -687
- package/src/superlocalmemory/code_graph/resolver.py +0 -321
- package/src/superlocalmemory/code_graph/search.py +0 -460
- package/src/superlocalmemory/code_graph/service.py +0 -95
- package/src/superlocalmemory/code_graph/watcher.py +0 -207
- package/src/superlocalmemory/compliance/__init__.py +0 -0
- package/src/superlocalmemory/compliance/abac.py +0 -204
- package/src/superlocalmemory/compliance/audit.py +0 -385
- package/src/superlocalmemory/compliance/eu_ai_act.py +0 -101
- package/src/superlocalmemory/compliance/gdpr.py +0 -1479
- package/src/superlocalmemory/compliance/lifecycle.py +0 -158
- package/src/superlocalmemory/compliance/retention.py +0 -415
- package/src/superlocalmemory/compliance/scheduler.py +0 -217
- package/src/superlocalmemory/contracts/__init__.py +0 -1
- package/src/superlocalmemory/contracts/schemas/agent-experience-v1.schema.json +0 -92
- package/src/superlocalmemory/contracts/schemas/agent-integration-contract-v2.schema.json +0 -46
- package/src/superlocalmemory/contracts/schemas/cognitive-turn-receipt-v1.schema.json +0 -59
- package/src/superlocalmemory/contracts/v402.py +0 -62
- package/src/superlocalmemory/core/__init__.py +0 -0
- package/src/superlocalmemory/core/actor_context.py +0 -166
- package/src/superlocalmemory/core/admission.py +0 -769
- package/src/superlocalmemory/core/backend_orchestrator.py +0 -637
- package/src/superlocalmemory/core/block_hygiene.py +0 -147
- package/src/superlocalmemory/core/community_summary.py +0 -267
- package/src/superlocalmemory/core/component_healer.py +0 -144
- package/src/superlocalmemory/core/component_registry.py +0 -514
- package/src/superlocalmemory/core/config.py +0 -2204
- package/src/superlocalmemory/core/consolidation_engine.py +0 -983
- package/src/superlocalmemory/core/context_cache.py +0 -574
- package/src/superlocalmemory/core/derivation_lineage.py +0 -246
- package/src/superlocalmemory/core/embedding_worker.py +0 -208
- package/src/superlocalmemory/core/embeddings.py +0 -1052
- package/src/superlocalmemory/core/engine.py +0 -1395
- package/src/superlocalmemory/core/engine_capabilities.py +0 -24
- package/src/superlocalmemory/core/engine_ingestion.py +0 -983
- package/src/superlocalmemory/core/engine_lock.py +0 -75
- package/src/superlocalmemory/core/engine_wiring.py +0 -776
- package/src/superlocalmemory/core/entity_community.py +0 -178
- package/src/superlocalmemory/core/error_envelope.py +0 -60
- package/src/superlocalmemory/core/evidence_bundle.py +0 -528
- package/src/superlocalmemory/core/fact_consolidator.py +0 -812
- package/src/superlocalmemory/core/file_lock.py +0 -92
- package/src/superlocalmemory/core/graph_analyzer.py +0 -456
- package/src/superlocalmemory/core/graph_metrics.py +0 -597
- package/src/superlocalmemory/core/graph_pruner.py +0 -939
- package/src/superlocalmemory/core/health_monitor.py +0 -338
- package/src/superlocalmemory/core/hooks.py +0 -65
- package/src/superlocalmemory/core/ingest_gate.py +0 -133
- package/src/superlocalmemory/core/ingest_policy.py +0 -38
- package/src/superlocalmemory/core/ingestion_command.py +0 -1042
- package/src/superlocalmemory/core/injection.py +0 -434
- package/src/superlocalmemory/core/install_detector.py +0 -131
- package/src/superlocalmemory/core/key_expander.py +0 -138
- package/src/superlocalmemory/core/lifecycle_state.py +0 -153
- package/src/superlocalmemory/core/maintenance.py +0 -777
- package/src/superlocalmemory/core/maintenance_scheduler.py +0 -507
- package/src/superlocalmemory/core/materialization_control.py +0 -20
- package/src/superlocalmemory/core/mcp_embedder_proxy.py +0 -89
- package/src/superlocalmemory/core/memory_health.py +0 -266
- package/src/superlocalmemory/core/mode_capability.py +0 -111
- package/src/superlocalmemory/core/modes.py +0 -168
- package/src/superlocalmemory/core/mutations.py +0 -688
- package/src/superlocalmemory/core/ollama_embedder.py +0 -266
- package/src/superlocalmemory/core/ollama_validator.py +0 -315
- package/src/superlocalmemory/core/operation_policy.py +0 -92
- package/src/superlocalmemory/core/operation_policy_registry.py +0 -542
- package/src/superlocalmemory/core/operation_request.py +0 -127
- package/src/superlocalmemory/core/ops_remediation.py +0 -542
- package/src/superlocalmemory/core/pii.py +0 -105
- package/src/superlocalmemory/core/platform_utils.py +0 -138
- package/src/superlocalmemory/core/profiles.py +0 -234
- package/src/superlocalmemory/core/progressive_abstraction.py +0 -208
- package/src/superlocalmemory/core/projection_drain.py +0 -380
- package/src/superlocalmemory/core/queue_consumer.py +0 -168
- package/src/superlocalmemory/core/ram_lock.py +0 -160
- package/src/superlocalmemory/core/rate_limit.py +0 -151
- package/src/superlocalmemory/core/recall_gate.py +0 -95
- package/src/superlocalmemory/core/recall_pipeline.py +0 -1337
- package/src/superlocalmemory/core/recall_queue.py +0 -377
- package/src/superlocalmemory/core/recall_worker.py +0 -414
- package/src/superlocalmemory/core/registry.py +0 -121
- package/src/superlocalmemory/core/remember_admission.py +0 -161
- package/src/superlocalmemory/core/remember_runtime.py +0 -1190
- package/src/superlocalmemory/core/remote_mode.py +0 -214
- package/src/superlocalmemory/core/reranker_worker.py +0 -338
- package/src/superlocalmemory/core/safe_fs.py +0 -108
- package/src/superlocalmemory/core/scale_autopromote.py +0 -196
- package/src/superlocalmemory/core/scale_engine.py +0 -915
- package/src/superlocalmemory/core/score_contract.py +0 -82
- package/src/superlocalmemory/core/security_primitives.py +0 -672
- package/src/superlocalmemory/core/session_identity.py +0 -98
- package/src/superlocalmemory/core/shadow_router.py +0 -319
- package/src/superlocalmemory/core/slm_disabled.py +0 -85
- package/src/superlocalmemory/core/status_contract.py +0 -108
- package/src/superlocalmemory/core/store_pipeline.py +0 -1404
- package/src/superlocalmemory/core/summarizer.py +0 -200
- package/src/superlocalmemory/core/tier_manager.py +0 -461
- package/src/superlocalmemory/core/topic_signature.py +0 -156
- package/src/superlocalmemory/core/transactions/__init__.py +0 -78
- package/src/superlocalmemory/core/transactions/concrete_owners.py +0 -604
- package/src/superlocalmemory/core/transactions/erasure.py +0 -825
- package/src/superlocalmemory/core/transactions/manifest.py +0 -255
- package/src/superlocalmemory/core/transactions/manifest_key.py +0 -155
- package/src/superlocalmemory/core/transactions/obligations.py +0 -272
- package/src/superlocalmemory/core/transactions/owners.py +0 -114
- package/src/superlocalmemory/core/transactions/reconciler.py +0 -285
- package/src/superlocalmemory/core/transactions/service.py +0 -330
- package/src/superlocalmemory/core/worker_pool.py +0 -377
- package/src/superlocalmemory/core/working_memory.py +0 -288
- package/src/superlocalmemory/dynamics/__init__.py +0 -0
- package/src/superlocalmemory/dynamics/activation_guided_quantization.py +0 -374
- package/src/superlocalmemory/dynamics/eap_scheduler.py +0 -294
- package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +0 -171
- package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +0 -227
- package/src/superlocalmemory/encoding/__init__.py +0 -0
- package/src/superlocalmemory/encoding/auto_linker.py +0 -308
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +0 -899
- package/src/superlocalmemory/encoding/consolidator.py +0 -472
- package/src/superlocalmemory/encoding/context_generator.py +0 -175
- package/src/superlocalmemory/encoding/emotional.py +0 -189
- package/src/superlocalmemory/encoding/entity_reflexion.py +0 -200
- package/src/superlocalmemory/encoding/entity_resolver.py +0 -687
- package/src/superlocalmemory/encoding/entropy_gate.py +0 -101
- package/src/superlocalmemory/encoding/fact_extractor.py +0 -877
- package/src/superlocalmemory/encoding/foresight.py +0 -93
- package/src/superlocalmemory/encoding/graph_builder.py +0 -346
- package/src/superlocalmemory/encoding/observation_builder.py +0 -177
- package/src/superlocalmemory/encoding/prospective_markers.py +0 -262
- package/src/superlocalmemory/encoding/scene_builder.py +0 -410
- package/src/superlocalmemory/encoding/signal_inference.py +0 -90
- package/src/superlocalmemory/encoding/temporal_parser.py +0 -432
- package/src/superlocalmemory/encoding/temporal_validator.py +0 -572
- package/src/superlocalmemory/encoding/type_router.py +0 -237
- package/src/superlocalmemory/evaluation/__init__.py +0 -13
- package/src/superlocalmemory/evaluation/calibration.py +0 -308
- package/src/superlocalmemory/evolution/__init__.py +0 -29
- package/src/superlocalmemory/evolution/blind_verifier.py +0 -122
- package/src/superlocalmemory/evolution/budget.py +0 -356
- package/src/superlocalmemory/evolution/evolution_store.py +0 -619
- package/src/superlocalmemory/evolution/llm_dispatch.py +0 -559
- package/src/superlocalmemory/evolution/model_selection.py +0 -175
- package/src/superlocalmemory/evolution/mutation_generator.py +0 -226
- package/src/superlocalmemory/evolution/skill_activator.py +0 -270
- package/src/superlocalmemory/evolution/skill_evolver.py +0 -928
- package/src/superlocalmemory/evolution/triggers.py +0 -376
- package/src/superlocalmemory/evolution/types.py +0 -114
- package/src/superlocalmemory/graph/__init__.py +0 -9
- package/src/superlocalmemory/graph/cozo_adjacency.py +0 -122
- package/src/superlocalmemory/graph/cozo_backend.py +0 -751
- package/src/superlocalmemory/hooks/__init__.py +0 -3
- package/src/superlocalmemory/hooks/_outcome_common.py +0 -523
- package/src/superlocalmemory/hooks/adapter_base.py +0 -347
- package/src/superlocalmemory/hooks/antigravity_adapter.py +0 -171
- package/src/superlocalmemory/hooks/auto_capture.py +0 -133
- package/src/superlocalmemory/hooks/auto_invoker.py +0 -521
- package/src/superlocalmemory/hooks/auto_parameterize.py +0 -147
- package/src/superlocalmemory/hooks/auto_recall.py +0 -191
- package/src/superlocalmemory/hooks/auto_recall_hook.py +0 -251
- package/src/superlocalmemory/hooks/before_web_hook.py +0 -131
- package/src/superlocalmemory/hooks/claude_code_hooks.py +0 -637
- package/src/superlocalmemory/hooks/codex_assets.py +0 -251
- package/src/superlocalmemory/hooks/codex_hooks.py +0 -186
- package/src/superlocalmemory/hooks/context_payload.py +0 -311
- package/src/superlocalmemory/hooks/copilot_adapter.py +0 -208
- package/src/superlocalmemory/hooks/cross_platform_connector.py +0 -90
- package/src/superlocalmemory/hooks/cursor_adapter.py +0 -173
- package/src/superlocalmemory/hooks/hook_daemon.py +0 -295
- package/src/superlocalmemory/hooks/hook_handlers.py +0 -822
- package/src/superlocalmemory/hooks/ide_connector.py +0 -246
- package/src/superlocalmemory/hooks/memory_protocol.py +0 -158
- package/src/superlocalmemory/hooks/portable_kit.py +0 -755
- package/src/superlocalmemory/hooks/post_tool_async_hook.py +0 -183
- package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +0 -351
- package/src/superlocalmemory/hooks/prewarm_auth.py +0 -187
- package/src/superlocalmemory/hooks/rules_engine.py +0 -99
- package/src/superlocalmemory/hooks/session_registry.py +0 -330
- package/src/superlocalmemory/hooks/stop_outcome_hook.py +0 -138
- package/src/superlocalmemory/hooks/sync_loop.py +0 -114
- package/src/superlocalmemory/hooks/topic_shift_hook.py +0 -302
- package/src/superlocalmemory/hooks/user_prompt_hook.py +0 -131
- package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +0 -210
- package/src/superlocalmemory/infra/__init__.py +0 -3
- package/src/superlocalmemory/infra/auth_middleware.py +0 -145
- package/src/superlocalmemory/infra/backup.py +0 -974
- package/src/superlocalmemory/infra/backup_obligations.py +0 -423
- package/src/superlocalmemory/infra/cache_manager.py +0 -267
- package/src/superlocalmemory/infra/cloud_backup.py +0 -788
- package/src/superlocalmemory/infra/daemon_identity.py +0 -300
- package/src/superlocalmemory/infra/data_root.py +0 -238
- package/src/superlocalmemory/infra/event_bus.py +0 -637
- package/src/superlocalmemory/infra/local_diagnostics.py +0 -327
- package/src/superlocalmemory/infra/pid_manager.py +0 -193
- package/src/superlocalmemory/infra/process_identity.py +0 -180
- package/src/superlocalmemory/infra/process_reaper.py +0 -624
- package/src/superlocalmemory/infra/rate_limiter.py +0 -228
- package/src/superlocalmemory/infra/self_heal.py +0 -401
- package/src/superlocalmemory/infra/version_integrity.py +0 -229
- package/src/superlocalmemory/ingestion/__init__.py +0 -13
- package/src/superlocalmemory/ingestion/adapter_manager.py +0 -255
- package/src/superlocalmemory/ingestion/base_adapter.py +0 -171
- package/src/superlocalmemory/ingestion/calendar_adapter.py +0 -349
- package/src/superlocalmemory/ingestion/credentials.py +0 -125
- package/src/superlocalmemory/ingestion/gmail_adapter.py +0 -378
- package/src/superlocalmemory/ingestion/parsers.py +0 -100
- package/src/superlocalmemory/ingestion/transcript_adapter.py +0 -161
- package/src/superlocalmemory/integrations/__init__.py +0 -1
- package/src/superlocalmemory/integrations/bounded_loops_mcp.py +0 -431
- package/src/superlocalmemory/integrations/bounded_loops_v051.py +0 -236
- package/src/superlocalmemory/learning/__init__.py +0 -0
- package/src/superlocalmemory/learning/adaptive.py +0 -172
- package/src/superlocalmemory/learning/arm_catalog.py +0 -97
- package/src/superlocalmemory/learning/assertion_miner.py +0 -403
- package/src/superlocalmemory/learning/bandit.py +0 -654
- package/src/superlocalmemory/learning/bandit_cache.py +0 -131
- package/src/superlocalmemory/learning/behavioral.py +0 -542
- package/src/superlocalmemory/learning/bootstrap.py +0 -298
- package/src/superlocalmemory/learning/consolidation_cycle.py +0 -398
- package/src/superlocalmemory/learning/consolidation_quantization_worker.py +0 -115
- package/src/superlocalmemory/learning/consolidation_worker.py +0 -261
- package/src/superlocalmemory/learning/cross_project.py +0 -408
- package/src/superlocalmemory/learning/database.py +0 -698
- package/src/superlocalmemory/learning/dedup_hnsw.py +0 -413
- package/src/superlocalmemory/learning/engagement.py +0 -487
- package/src/superlocalmemory/learning/engagement_features.py +0 -279
- package/src/superlocalmemory/learning/ensemble.py +0 -309
- package/src/superlocalmemory/learning/entity_compiler.py +0 -356
- package/src/superlocalmemory/learning/fact_outcome_joins.py +0 -207
- package/src/superlocalmemory/learning/features.py +0 -138
- package/src/superlocalmemory/learning/feedback.py +0 -724
- package/src/superlocalmemory/learning/forgetting_scheduler.py +0 -375
- package/src/superlocalmemory/learning/hnsw_dedup.py +0 -69
- package/src/superlocalmemory/learning/labeler.py +0 -85
- package/src/superlocalmemory/learning/legacy_migration.py +0 -316
- package/src/superlocalmemory/learning/lightgbm_subprocess.py +0 -236
- package/src/superlocalmemory/learning/memory_merge.py +0 -175
- package/src/superlocalmemory/learning/model_cache.py +0 -267
- package/src/superlocalmemory/learning/model_rollback.py +0 -281
- package/src/superlocalmemory/learning/outcome_queue.py +0 -306
- package/src/superlocalmemory/learning/outcomes.py +0 -286
- package/src/superlocalmemory/learning/pattern_miner.py +0 -465
- package/src/superlocalmemory/learning/pattern_miner_constants.py +0 -90
- package/src/superlocalmemory/learning/pcos.py +0 -291
- package/src/superlocalmemory/learning/project_context.py +0 -366
- package/src/superlocalmemory/learning/propensity.py +0 -131
- package/src/superlocalmemory/learning/ranker.py +0 -300
- package/src/superlocalmemory/learning/ranker_common.py +0 -163
- package/src/superlocalmemory/learning/ranker_retrain_legacy.py +0 -210
- package/src/superlocalmemory/learning/ranker_retrain_online.py +0 -423
- package/src/superlocalmemory/learning/reward.py +0 -888
- package/src/superlocalmemory/learning/reward_archive.py +0 -223
- package/src/superlocalmemory/learning/reward_boost.py +0 -211
- package/src/superlocalmemory/learning/reward_from_outcomes.py +0 -365
- package/src/superlocalmemory/learning/reward_model.py +0 -144
- package/src/superlocalmemory/learning/reward_proxy.py +0 -578
- package/src/superlocalmemory/learning/shadow_test.py +0 -524
- package/src/superlocalmemory/learning/signal_kinds.py +0 -79
- package/src/superlocalmemory/learning/signal_worker.py +0 -268
- package/src/superlocalmemory/learning/signals.py +0 -646
- package/src/superlocalmemory/learning/skill_performance_miner.py +0 -422
- package/src/superlocalmemory/learning/source_quality.py +0 -828
- package/src/superlocalmemory/learning/trigram_index.py +0 -548
- package/src/superlocalmemory/learning/workflows.py +0 -309
- package/src/superlocalmemory/llm/__init__.py +0 -0
- package/src/superlocalmemory/llm/backbone.py +0 -364
- package/src/superlocalmemory/loops/__init__.py +0 -58
- package/src/superlocalmemory/loops/budget.py +0 -58
- package/src/superlocalmemory/loops/engine.py +0 -174
- package/src/superlocalmemory/loops/ledger.py +0 -298
- package/src/superlocalmemory/loops/models.py +0 -152
- package/src/superlocalmemory/loops/rules.py +0 -52
- package/src/superlocalmemory/math/__init__.py +0 -0
- package/src/superlocalmemory/math/ebbinghaus.py +0 -352
- package/src/superlocalmemory/math/fisher.py +0 -356
- package/src/superlocalmemory/math/fisher_quantized.py +0 -255
- package/src/superlocalmemory/math/hopfield.py +0 -282
- package/src/superlocalmemory/math/langevin.py +0 -411
- package/src/superlocalmemory/math/polar_quant.py +0 -414
- package/src/superlocalmemory/math/qjl.py +0 -115
- package/src/superlocalmemory/math/sheaf.py +0 -261
- package/src/superlocalmemory/math/turbo_quant.py +0 -318
- package/src/superlocalmemory/mcp/__init__.py +0 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +0 -203
- package/src/superlocalmemory/mcp/_pool_adapter.py +0 -181
- package/src/superlocalmemory/mcp/_stdin_guard.py +0 -60
- package/src/superlocalmemory/mcp/agent_context.py +0 -115
- package/src/superlocalmemory/mcp/cli_fallback.py +0 -602
- package/src/superlocalmemory/mcp/http_transport.py +0 -85
- package/src/superlocalmemory/mcp/profiles.py +0 -154
- package/src/superlocalmemory/mcp/resources.py +0 -281
- package/src/superlocalmemory/mcp/server.py +0 -480
- package/src/superlocalmemory/mcp/session_binding.py +0 -98
- package/src/superlocalmemory/mcp/shared.py +0 -112
- package/src/superlocalmemory/mcp/tools.py +0 -18
- package/src/superlocalmemory/mcp/tools_active.py +0 -958
- package/src/superlocalmemory/mcp/tools_brain.py +0 -298
- package/src/superlocalmemory/mcp/tools_code_graph.py +0 -1717
- package/src/superlocalmemory/mcp/tools_context.py +0 -239
- package/src/superlocalmemory/mcp/tools_core.py +0 -1112
- package/src/superlocalmemory/mcp/tools_evolution.py +0 -343
- package/src/superlocalmemory/mcp/tools_learning.py +0 -393
- package/src/superlocalmemory/mcp/tools_loops.py +0 -345
- package/src/superlocalmemory/mcp/tools_mesh.py +0 -429
- package/src/superlocalmemory/mcp/tools_ops.py +0 -115
- package/src/superlocalmemory/mcp/tools_optimize.py +0 -322
- package/src/superlocalmemory/mcp/tools_summaries.py +0 -147
- package/src/superlocalmemory/mcp/tools_v28.py +0 -292
- package/src/superlocalmemory/mcp/tools_v3.py +0 -398
- package/src/superlocalmemory/mcp/tools_v33.py +0 -507
- package/src/superlocalmemory/mesh/__init__.py +0 -12
- package/src/superlocalmemory/mesh/broker.py +0 -812
- package/src/superlocalmemory/mesh/broker_security.py +0 -470
- package/src/superlocalmemory/mesh/discovery.py +0 -365
- package/src/superlocalmemory/mesh/lock_protocol.py +0 -313
- package/src/superlocalmemory/mesh/node_identity.py +0 -97
- package/src/superlocalmemory/mesh/outbox_remote.py +0 -429
- package/src/superlocalmemory/mesh/remote_sync.py +0 -829
- package/src/superlocalmemory/mesh/state_sync.py +0 -286
- package/src/superlocalmemory/migrations/__init__.py +0 -5
- package/src/superlocalmemory/migrations/v3_4_25_to_v3_4_26.py +0 -144
- package/src/superlocalmemory/optimize/NOTICE +0 -6
- package/src/superlocalmemory/optimize/__init__.py +0 -0
- package/src/superlocalmemory/optimize/adapters/__init__.py +0 -68
- package/src/superlocalmemory/optimize/adapters/_agent_registry.py +0 -120
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +0 -112
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +0 -122
- package/src/superlocalmemory/optimize/adapters/wrap.py +0 -228
- package/src/superlocalmemory/optimize/cache/__init__.py +0 -31
- package/src/superlocalmemory/optimize/cache/boundary_store.py +0 -488
- package/src/superlocalmemory/optimize/cache/centroid_store.py +0 -199
- package/src/superlocalmemory/optimize/cache/context_key.py +0 -67
- package/src/superlocalmemory/optimize/cache/exact.py +0 -88
- package/src/superlocalmemory/optimize/cache/invalidation.py +0 -36
- package/src/superlocalmemory/optimize/cache/key_builder.py +0 -111
- package/src/superlocalmemory/optimize/cache/manager.py +0 -737
- package/src/superlocalmemory/optimize/cache/semantic.py +0 -637
- package/src/superlocalmemory/optimize/cache/stampede.py +0 -50
- package/src/superlocalmemory/optimize/compress/__init__.py +0 -17
- package/src/superlocalmemory/optimize/compress/align.py +0 -159
- package/src/superlocalmemory/optimize/compress/ccr.py +0 -116
- package/src/superlocalmemory/optimize/compress/prose_llmlingua.py +0 -71
- package/src/superlocalmemory/optimize/compress/router.py +0 -667
- package/src/superlocalmemory/optimize/config/__init__.py +0 -56
- package/src/superlocalmemory/optimize/config/defaults.py +0 -43
- package/src/superlocalmemory/optimize/config/schema.py +0 -327
- package/src/superlocalmemory/optimize/config/store.py +0 -270
- package/src/superlocalmemory/optimize/metrics/__init__.py +0 -8
- package/src/superlocalmemory/optimize/metrics/counters.py +0 -155
- package/src/superlocalmemory/optimize/metrics/estimator.py +0 -87
- package/src/superlocalmemory/optimize/metrics/exporters.py +0 -77
- package/src/superlocalmemory/optimize/metrics/persistence.py +0 -115
- package/src/superlocalmemory/optimize/proxy/__init__.py +0 -28
- package/src/superlocalmemory/optimize/proxy/_helpers.py +0 -730
- package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +0 -375
- package/src/superlocalmemory/optimize/proxy/capture.py +0 -550
- package/src/superlocalmemory/optimize/proxy/gemini_surface.py +0 -528
- package/src/superlocalmemory/optimize/proxy/lifecycle.py +0 -126
- package/src/superlocalmemory/optimize/proxy/openai_surface.py +0 -465
- package/src/superlocalmemory/optimize/proxy/server.py +0 -199
- package/src/superlocalmemory/optimize/proxy/vertex_surface.py +0 -246
- package/src/superlocalmemory/optimize/storage/__init__.py +0 -0
- package/src/superlocalmemory/optimize/storage/db.py +0 -1185
- package/src/superlocalmemory/optimize/storage/schema.py +0 -205
- package/src/superlocalmemory/parameterization/__init__.py +0 -47
- package/src/superlocalmemory/parameterization/cross_project.py +0 -12
- package/src/superlocalmemory/parameterization/pattern_extractor.py +0 -584
- package/src/superlocalmemory/parameterization/pii_filter.py +0 -106
- package/src/superlocalmemory/parameterization/prompt_injector.py +0 -219
- package/src/superlocalmemory/parameterization/prompt_lifecycle.py +0 -281
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +0 -542
- package/src/superlocalmemory/parameterization/workflow_miner.py +0 -17
- package/src/superlocalmemory/reliability/__init__.py +0 -45
- package/src/superlocalmemory/reliability/join_liveness.py +0 -301
- package/src/superlocalmemory/reliability/prior_distance.py +0 -243
- package/src/superlocalmemory/retrieval/__init__.py +0 -0
- package/src/superlocalmemory/retrieval/agentic.py +0 -367
- package/src/superlocalmemory/retrieval/ann_index.py +0 -235
- package/src/superlocalmemory/retrieval/bm25_channel.py +0 -451
- package/src/superlocalmemory/retrieval/bridge_discovery.py +0 -253
- package/src/superlocalmemory/retrieval/channel_registry.py +0 -154
- package/src/superlocalmemory/retrieval/channel_status.py +0 -117
- package/src/superlocalmemory/retrieval/engine.py +0 -1615
- package/src/superlocalmemory/retrieval/entity_channel.py +0 -994
- package/src/superlocalmemory/retrieval/forgetting_filter.py +0 -160
- package/src/superlocalmemory/retrieval/fusion.py +0 -81
- package/src/superlocalmemory/retrieval/graph_adjacency.py +0 -219
- package/src/superlocalmemory/retrieval/hopfield_channel.py +0 -465
- package/src/superlocalmemory/retrieval/profile_channel.py +0 -105
- package/src/superlocalmemory/retrieval/quantization_aware_search.py +0 -147
- package/src/superlocalmemory/retrieval/remote_reranker.py +0 -758
- package/src/superlocalmemory/retrieval/reranker.py +0 -674
- package/src/superlocalmemory/retrieval/scope_policy.py +0 -126
- package/src/superlocalmemory/retrieval/semantic_channel.py +0 -638
- package/src/superlocalmemory/retrieval/spreading.py +0 -288
- package/src/superlocalmemory/retrieval/spreading_activation.py +0 -616
- package/src/superlocalmemory/retrieval/strategy.py +0 -248
- package/src/superlocalmemory/retrieval/temporal_channel.py +0 -433
- package/src/superlocalmemory/retrieval/temporal_frame.py +0 -102
- package/src/superlocalmemory/retrieval/temporal_utils.py +0 -122
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +0 -499
- package/src/superlocalmemory/retrieval/time_window.py +0 -181
- package/src/superlocalmemory/retrieval/vector_store.py +0 -863
- package/src/superlocalmemory/server/__init__.py +0 -1
- package/src/superlocalmemory/server/api.py +0 -310
- package/src/superlocalmemory/server/asset_versions.py +0 -171
- package/src/superlocalmemory/server/bandit_loops.py +0 -158
- package/src/superlocalmemory/server/config_file.py +0 -90
- package/src/superlocalmemory/server/consolidation_runner.py +0 -140
- package/src/superlocalmemory/server/egress_policy.py +0 -258
- package/src/superlocalmemory/server/loopback.py +0 -85
- package/src/superlocalmemory/server/middleware/__init__.py +0 -11
- package/src/superlocalmemory/server/middleware/security_headers.py +0 -144
- package/src/superlocalmemory/server/origin.py +0 -55
- package/src/superlocalmemory/server/profile_runtime.py +0 -515
- package/src/superlocalmemory/server/rbac_enforce.py +0 -194
- package/src/superlocalmemory/server/recall_health.py +0 -343
- package/src/superlocalmemory/server/recall_serializer.py +0 -320
- package/src/superlocalmemory/server/route_mutations.py +0 -104
- package/src/superlocalmemory/server/routes/__init__.py +0 -4
- package/src/superlocalmemory/server/routes/abstraction.py +0 -314
- package/src/superlocalmemory/server/routes/adapters.py +0 -63
- package/src/superlocalmemory/server/routes/agents.py +0 -303
- package/src/superlocalmemory/server/routes/backup.py +0 -869
- package/src/superlocalmemory/server/routes/behavioral.py +0 -659
- package/src/superlocalmemory/server/routes/brain.py +0 -1892
- package/src/superlocalmemory/server/routes/chat.py +0 -393
- package/src/superlocalmemory/server/routes/compliance.py +0 -533
- package/src/superlocalmemory/server/routes/config_api.py +0 -703
- package/src/superlocalmemory/server/routes/data_io.py +0 -329
- package/src/superlocalmemory/server/routes/entity.py +0 -237
- package/src/superlocalmemory/server/routes/events.py +0 -214
- package/src/superlocalmemory/server/routes/evolution.py +0 -510
- package/src/superlocalmemory/server/routes/helpers.py +0 -499
- package/src/superlocalmemory/server/routes/ingest.py +0 -137
- package/src/superlocalmemory/server/routes/insights.py +0 -366
- package/src/superlocalmemory/server/routes/learning.py +0 -834
- package/src/superlocalmemory/server/routes/learning_telemetry.py +0 -154
- package/src/superlocalmemory/server/routes/lifecycle.py +0 -184
- package/src/superlocalmemory/server/routes/memories.py +0 -1661
- package/src/superlocalmemory/server/routes/mesh.py +0 -517
- package/src/superlocalmemory/server/routes/mesh_lock.py +0 -54
- package/src/superlocalmemory/server/routes/mesh_state.py +0 -63
- package/src/superlocalmemory/server/routes/optimize.py +0 -197
- package/src/superlocalmemory/server/routes/prewarm.py +0 -173
- package/src/superlocalmemory/server/routes/profiles.py +0 -292
- package/src/superlocalmemory/server/routes/ratelimit.py +0 -132
- package/src/superlocalmemory/server/routes/rbac.py +0 -366
- package/src/superlocalmemory/server/routes/stats.py +0 -385
- package/src/superlocalmemory/server/routes/tiers.py +0 -222
- package/src/superlocalmemory/server/routes/timeline.py +0 -258
- package/src/superlocalmemory/server/routes/token.py +0 -90
- package/src/superlocalmemory/server/routes/v3_api.py +0 -3023
- package/src/superlocalmemory/server/routes/ws.py +0 -171
- package/src/superlocalmemory/server/security_middleware.py +0 -89
- package/src/superlocalmemory/server/ui.py +0 -354
- package/src/superlocalmemory/server/unified_daemon.py +0 -6326
- package/src/superlocalmemory/server/write_identity.py +0 -195
- package/src/superlocalmemory/storage/__init__.py +0 -0
- package/src/superlocalmemory/storage/_migration_internals.py +0 -638
- package/src/superlocalmemory/storage/_schema_version.py +0 -174
- package/src/superlocalmemory/storage/access_log.py +0 -170
- package/src/superlocalmemory/storage/admission_codec.py +0 -129
- package/src/superlocalmemory/storage/admission_journal.py +0 -843
- package/src/superlocalmemory/storage/agent_experience.py +0 -546
- package/src/superlocalmemory/storage/backup.py +0 -531
- package/src/superlocalmemory/storage/correction_cases.py +0 -670
- package/src/superlocalmemory/storage/database.py +0 -3180
- package/src/superlocalmemory/storage/deferred_writes.py +0 -209
- package/src/superlocalmemory/storage/embedding_codec.py +0 -200
- package/src/superlocalmemory/storage/embedding_migrator.py +0 -672
- package/src/superlocalmemory/storage/erasure_fence.py +0 -45
- package/src/superlocalmemory/storage/execution_learning.py +0 -285
- package/src/superlocalmemory/storage/external_evidence.py +0 -359
- package/src/superlocalmemory/storage/generation_fence.py +0 -63
- package/src/superlocalmemory/storage/lineage_retention.py +0 -236
- package/src/superlocalmemory/storage/logical_edges.py +0 -86
- package/src/superlocalmemory/storage/memory_write.py +0 -115
- package/src/superlocalmemory/storage/migration_runner.py +0 -895
- package/src/superlocalmemory/storage/migration_v33.py +0 -140
- package/src/superlocalmemory/storage/migrations/M001_add_signal_features_columns.py +0 -67
- package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +0 -107
- package/src/superlocalmemory/storage/migrations/M003_migration_log.py +0 -38
- package/src/superlocalmemory/storage/migrations/M004_cross_platform_sync_log.py +0 -46
- package/src/superlocalmemory/storage/migrations/M005_bandit_tables.py +0 -75
- package/src/superlocalmemory/storage/migrations/M006_action_outcomes_reward.py +0 -75
- package/src/superlocalmemory/storage/migrations/M007_pending_outcomes.py +0 -63
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +0 -94
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +0 -80
- package/src/superlocalmemory/storage/migrations/M011_archive_and_merge.py +0 -87
- package/src/superlocalmemory/storage/migrations/M012_shadow_observations.py +0 -72
- package/src/superlocalmemory/storage/migrations/M013_bi_temporal_columns.py +0 -55
- package/src/superlocalmemory/storage/migrations/M014_v345_scale_ready.py +0 -45
- package/src/superlocalmemory/storage/migrations/M015_add_pinned_column.py +0 -58
- package/src/superlocalmemory/storage/migrations/M016_add_scope_support.py +0 -120
- package/src/superlocalmemory/storage/migrations/M017_ccq_scope_column.py +0 -79
- package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +0 -120
- package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +0 -54
- package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +0 -52
- package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +0 -108
- package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +0 -86
- package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +0 -194
- package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +0 -87
- package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +0 -90
- package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +0 -136
- package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +0 -163
- package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +0 -305
- package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +0 -137
- package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +0 -93
- package/src/superlocalmemory/storage/migrations/M031_dead_letter_operations.py +0 -80
- package/src/superlocalmemory/storage/migrations/M032_write_coordinator_admission.py +0 -188
- package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +0 -148
- package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +0 -58
- package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +0 -113
- package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +0 -107
- package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +0 -162
- package/src/superlocalmemory/storage/migrations/M038_learning_feedback_channel.py +0 -77
- package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +0 -137
- package/src/superlocalmemory/storage/migrations/M040_agent_experience_receipts.py +0 -254
- package/src/superlocalmemory/storage/migrations/M041_external_evidence_receipts.py +0 -189
- package/src/superlocalmemory/storage/migrations/M042_correction_case_ledger.py +0 -245
- package/src/superlocalmemory/storage/migrations/M043_quarantine_display_summaries.py +0 -512
- package/src/superlocalmemory/storage/migrations/M044_play_carries_its_own_evidence.py +0 -127
- package/src/superlocalmemory/storage/migrations/M045_fact_outcome_score.py +0 -158
- package/src/superlocalmemory/storage/migrations/M046_prospective_memory_has_its_own_name.py +0 -620
- package/src/superlocalmemory/storage/migrations/M047_fisher_vectors_are_stored_like_every_other_vector.py +0 -306
- package/src/superlocalmemory/storage/migrations/M048_upcoming_holds_only_what_is_upcoming.py +0 -229
- package/src/superlocalmemory/storage/migrations/M049_a_schema_version_marker_is_one_row.py +0 -201
- package/src/superlocalmemory/storage/migrations/M050_execution_learning_v2.py +0 -70
- package/src/superlocalmemory/storage/migrations/__init__.py +0 -103
- package/src/superlocalmemory/storage/migrations.py +0 -333
- package/src/superlocalmemory/storage/models.py +0 -500
- package/src/superlocalmemory/storage/projection_outbox.py +0 -346
- package/src/superlocalmemory/storage/quantized_store.py +0 -280
- package/src/superlocalmemory/storage/read_connection.py +0 -115
- package/src/superlocalmemory/storage/retention_policy.py +0 -860
- package/src/superlocalmemory/storage/schema.py +0 -1108
- package/src/superlocalmemory/storage/schema_code_graph.py +0 -282
- package/src/superlocalmemory/storage/schema_v32.py +0 -382
- package/src/superlocalmemory/storage/schema_v3410.py +0 -159
- package/src/superlocalmemory/storage/schema_v3411.py +0 -149
- package/src/superlocalmemory/storage/schema_v343.py +0 -315
- package/src/superlocalmemory/storage/schema_v345.py +0 -109
- package/src/superlocalmemory/storage/schema_v347.py +0 -140
- package/src/superlocalmemory/storage/sqlite_vectors.py +0 -169
- package/src/superlocalmemory/storage/v2_migrator.py +0 -466
- package/src/superlocalmemory/storage/write_coordinator.py +0 -949
- package/src/superlocalmemory/storage/write_lock.py +0 -88
- package/src/superlocalmemory/summaries/__init__.py +0 -37
- package/src/superlocalmemory/summaries/base.py +0 -267
- package/src/superlocalmemory/summaries/daily_reflection.py +0 -340
- package/src/superlocalmemory/summaries/non_answer.py +0 -223
- package/src/superlocalmemory/summaries/project_work_log.py +0 -440
- package/src/superlocalmemory/summaries/session_summary.py +0 -311
- package/src/superlocalmemory/trust/__init__.py +0 -0
- package/src/superlocalmemory/trust/gate.py +0 -171
- package/src/superlocalmemory/trust/provenance.py +0 -124
- package/src/superlocalmemory/trust/scorer.py +0 -413
- package/src/superlocalmemory/trust/signals.py +0 -153
- package/src/superlocalmemory/ui/assets/slm-icon-white.svg +0 -64
- package/src/superlocalmemory/ui/assets/slm-icon.svg +0 -36
- package/src/superlocalmemory/ui/css/brain.css +0 -409
- package/src/superlocalmemory/ui/css/design-system.css +0 -696
- package/src/superlocalmemory/ui/css/legacy-dashboard.css +0 -663
- package/src/superlocalmemory/ui/css/neural-glass.css +0 -1599
- package/src/superlocalmemory/ui/css/od-bridge.css +0 -158
- package/src/superlocalmemory/ui/favicon.svg +0 -36
- package/src/superlocalmemory/ui/index.html +0 -1638
- package/src/superlocalmemory/ui/js/agents.js +0 -192
- package/src/superlocalmemory/ui/js/auto-settings.js +0 -624
- package/src/superlocalmemory/ui/js/brain.js +0 -1400
- package/src/superlocalmemory/ui/js/clusters.js +0 -326
- package/src/superlocalmemory/ui/js/compliance.js +0 -307
- package/src/superlocalmemory/ui/js/core.js +0 -566
- package/src/superlocalmemory/ui/js/dashboard.js +0 -503
- package/src/superlocalmemory/ui/js/event-delegation.js +0 -113
- package/src/superlocalmemory/ui/js/events.js +0 -178
- package/src/superlocalmemory/ui/js/fact-detail.js +0 -142
- package/src/superlocalmemory/ui/js/feedback.js +0 -339
- package/src/superlocalmemory/ui/js/graph-event-bus.js +0 -83
- package/src/superlocalmemory/ui/js/graph-filters.js +0 -220
- package/src/superlocalmemory/ui/js/graph-ui.js +0 -214
- package/src/superlocalmemory/ui/js/ide-status.js +0 -115
- package/src/superlocalmemory/ui/js/init.js +0 -54
- package/src/superlocalmemory/ui/js/knowledge-graph.js +0 -945
- package/src/superlocalmemory/ui/js/lifecycle.js +0 -387
- package/src/superlocalmemory/ui/js/math-health.js +0 -114
- package/src/superlocalmemory/ui/js/memories.js +0 -394
- package/src/superlocalmemory/ui/js/memory-chat.js +0 -371
- package/src/superlocalmemory/ui/js/memory-timeline.js +0 -265
- package/src/superlocalmemory/ui/js/modal.js +0 -733
- package/src/superlocalmemory/ui/js/ng-entities.js +0 -298
- package/src/superlocalmemory/ui/js/ng-health.js +0 -208
- package/src/superlocalmemory/ui/js/ng-ingestion.js +0 -203
- package/src/superlocalmemory/ui/js/ng-mesh.js +0 -374
- package/src/superlocalmemory/ui/js/ng-shell.js +0 -524
- package/src/superlocalmemory/ui/js/ng-skills.js +0 -663
- package/src/superlocalmemory/ui/js/od-agents.js +0 -588
- package/src/superlocalmemory/ui/js/od-auth-gate.js +0 -257
- package/src/superlocalmemory/ui/js/od-backup.js +0 -878
- package/src/superlocalmemory/ui/js/od-boundedloops.js +0 -324
- package/src/superlocalmemory/ui/js/od-brain.js +0 -1095
- package/src/superlocalmemory/ui/js/od-compliance-ext.js +0 -301
- package/src/superlocalmemory/ui/js/od-components.js +0 -147
- package/src/superlocalmemory/ui/js/od-entities.js +0 -622
- package/src/superlocalmemory/ui/js/od-graph.js +0 -776
- package/src/superlocalmemory/ui/js/od-health.js +0 -579
- package/src/superlocalmemory/ui/js/od-mcp.js +0 -508
- package/src/superlocalmemory/ui/js/od-memories.js +0 -1499
- package/src/superlocalmemory/ui/js/od-mesh.js +0 -645
- package/src/superlocalmemory/ui/js/od-operations.js +0 -1268
- package/src/superlocalmemory/ui/js/od-ops-health.js +0 -417
- package/src/superlocalmemory/ui/js/od-optimize.js +0 -828
- package/src/superlocalmemory/ui/js/od-settings.js +0 -1275
- package/src/superlocalmemory/ui/js/od-shell.js +0 -819
- package/src/superlocalmemory/ui/js/od-skills.js +0 -600
- package/src/superlocalmemory/ui/js/od-team.js +0 -265
- package/src/superlocalmemory/ui/js/optimize.js +0 -191
- package/src/superlocalmemory/ui/js/profiles.js +0 -362
- package/src/superlocalmemory/ui/js/quick-actions.js +0 -334
- package/src/superlocalmemory/ui/js/recall-lab.js +0 -373
- package/src/superlocalmemory/ui/js/search.js +0 -86
- package/src/superlocalmemory/ui/js/settings.js +0 -556
- package/src/superlocalmemory/ui/js/timeline.js +0 -62
- package/src/superlocalmemory/ui/js/trust-dashboard.js +0 -225
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/bootstrap-icons.css +0 -2018
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap.bundle.min.js +0 -7
- package/src/superlocalmemory/ui/vendor/bootstrap.min.css +0 -6
- package/src/superlocalmemory/ui/vendor/d3.v7.min.js +0 -2
- package/src/superlocalmemory/ui/vendor/graphology-library.min.js +0 -2
- package/src/superlocalmemory/ui/vendor/graphology.umd.min.js +0 -2
- package/src/superlocalmemory/ui/vendor/inter-ui/inter-variable.min.css +0 -8
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable-Italic.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/sigma.min.js +0 -1
- package/src/superlocalmemory/vector/__init__.py +0 -9
- package/src/superlocalmemory/vector/lancedb_backend.py +0 -366
|
@@ -1,974 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
-
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
-
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
-
"""Automated backup manager for SuperLocalMemory V3.
|
|
5
|
-
|
|
6
|
-
Provides:
|
|
7
|
-
* Configurable interval (daily / weekly)
|
|
8
|
-
* Timestamped SQLite-safe backups via the ``sqlite3.backup()`` API
|
|
9
|
-
* Retention policy (keeps last *N* backups)
|
|
10
|
-
* Restore with automatic pre-restore safety snapshot
|
|
11
|
-
|
|
12
|
-
V3 change: base directory is ``~/.superlocalmemory/`` (was ``~/.claude-memory/``).
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
import hashlib
|
|
16
|
-
import json
|
|
17
|
-
import logging
|
|
18
|
-
import shutil
|
|
19
|
-
import tempfile
|
|
20
|
-
import sqlite3
|
|
21
|
-
import time
|
|
22
|
-
import uuid
|
|
23
|
-
from contextlib import contextmanager
|
|
24
|
-
from dataclasses import asdict, dataclass
|
|
25
|
-
from datetime import datetime, timedelta, timezone
|
|
26
|
-
from pathlib import Path
|
|
27
|
-
from typing import Dict, Generator, List, Optional
|
|
28
|
-
|
|
29
|
-
from superlocalmemory.infra.backup_obligations import (
|
|
30
|
-
BackupObligationStore,
|
|
31
|
-
erase_profile_from_snapshot,
|
|
32
|
-
)
|
|
33
|
-
from superlocalmemory.infra.data_root import DynamicStatePath, canonical_data_root
|
|
34
|
-
|
|
35
|
-
logger = logging.getLogger("superlocalmemory.backup")
|
|
36
|
-
|
|
37
|
-
# ---------------------------------------------------------------------------
|
|
38
|
-
# V3 paths
|
|
39
|
-
# ---------------------------------------------------------------------------
|
|
40
|
-
MEMORY_DIR = DynamicStatePath()
|
|
41
|
-
DB_PATH = DynamicStatePath("memory.db")
|
|
42
|
-
BACKUP_DIR = DynamicStatePath("backups")
|
|
43
|
-
CONFIG_FILE = DynamicStatePath("backup_config.json")
|
|
44
|
-
|
|
45
|
-
# Defaults
|
|
46
|
-
DEFAULT_INTERVAL_HOURS = 168 # 7 days
|
|
47
|
-
DEFAULT_MAX_BACKUPS = 10
|
|
48
|
-
MIN_INTERVAL_HOURS = 1
|
|
49
|
-
|
|
50
|
-
# ---------------------------------------------------------------------------
|
|
51
|
-
# SLM Managed Database Registry
|
|
52
|
-
# ---------------------------------------------------------------------------
|
|
53
|
-
# Every database that SLM creates and manages. The backup system backs up
|
|
54
|
-
# ONLY these databases — nothing else. When a new SLM module creates a new
|
|
55
|
-
# database file, add it here so it gets included in backups.
|
|
56
|
-
#
|
|
57
|
-
# Each user may have a different subset (e.g., some don't have code_graph.db
|
|
58
|
-
# if they never used the code graph feature). The backup system checks which
|
|
59
|
-
# ones exist and only backs up what's present.
|
|
60
|
-
|
|
61
|
-
MANAGED_DATABASES: tuple[str, ...] = (
|
|
62
|
-
"memory.db", # Core: facts, entities, graph, embeddings, sessions
|
|
63
|
-
"learning.db", # Learning pipeline: signals, patterns, ranker
|
|
64
|
-
"audit_chain.db", # Audit trail: compliance, provenance chain
|
|
65
|
-
"code_graph.db", # Code knowledge graph: symbols, references
|
|
66
|
-
"pending.db", # Pending operations queue
|
|
67
|
-
"audit.db", # Legacy audit (pre-v3.4)
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
# ---------------------------------------------------------------------------
|
|
72
|
-
# Coherent multi-store backup set
|
|
73
|
-
# ---------------------------------------------------------------------------
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class BackupVerificationError(Exception):
|
|
77
|
-
"""Raised when a backup set fails checksum re-verification."""
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
class BackupRestoreError(Exception):
|
|
81
|
-
"""Raised when a restore cannot be safely completed."""
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@dataclass(frozen=True)
|
|
85
|
-
class StoreEntry:
|
|
86
|
-
"""Describes one database file within a backup set."""
|
|
87
|
-
|
|
88
|
-
store_name: str # filename, e.g. "memory.db"
|
|
89
|
-
file_path: str # absolute path inside the final backup directory
|
|
90
|
-
size_bytes: int
|
|
91
|
-
sha256: str # SHA-256 hex digest of the backup copy
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
@dataclass(frozen=True)
|
|
95
|
-
class BackupSetManifest:
|
|
96
|
-
"""Describes a coherent snapshot of all managed databases.
|
|
97
|
-
|
|
98
|
-
All stores share a single epoch so callers can detect sets assembled
|
|
99
|
-
from different points in time and reject them. Checksums allow
|
|
100
|
-
independent verification of every backup file before restore.
|
|
101
|
-
"""
|
|
102
|
-
|
|
103
|
-
set_id: str # unique identifier for this backup set
|
|
104
|
-
epoch: int # Unix timestamp when the set was created
|
|
105
|
-
stores: tuple[StoreEntry, ...] # one entry per backed-up store
|
|
106
|
-
manifest_hash: str # SHA-256 over sorted store checksums
|
|
107
|
-
verified: bool # True only after Phase-4 re-verification
|
|
108
|
-
created_at: str = "" # ISO-8601 UTC creation timestamp
|
|
109
|
-
product_version: str = "" # reserved for version tracking
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
class BackupCoordinator:
|
|
113
|
-
"""Creates and verifies coherent backup sets spanning all managed databases.
|
|
114
|
-
|
|
115
|
-
A backup set groups every managed store under a single epoch and publishes
|
|
116
|
-
an atomic manifest only when all per-store checksums pass re-verification.
|
|
117
|
-
Any mismatch detected during re-verification causes the entire staging
|
|
118
|
-
directory to be removed without publication.
|
|
119
|
-
|
|
120
|
-
Args:
|
|
121
|
-
managed_databases: Ordered tuple of DB filenames to include.
|
|
122
|
-
base_dir: Directory where the live databases reside.
|
|
123
|
-
backup_dir: Directory where backup sets are written.
|
|
124
|
-
"""
|
|
125
|
-
|
|
126
|
-
def __init__(
|
|
127
|
-
self,
|
|
128
|
-
managed_databases: tuple[str, ...],
|
|
129
|
-
base_dir: Path,
|
|
130
|
-
backup_dir: Path,
|
|
131
|
-
) -> None:
|
|
132
|
-
self._managed_databases = managed_databases
|
|
133
|
-
self._base_dir = Path(base_dir)
|
|
134
|
-
self._backup_dir = Path(backup_dir)
|
|
135
|
-
|
|
136
|
-
# ------------------------------------------------------------------
|
|
137
|
-
# Public API
|
|
138
|
-
# ------------------------------------------------------------------
|
|
139
|
-
|
|
140
|
-
def create_backup_set(self) -> BackupSetManifest:
|
|
141
|
-
"""Copy all existing managed stores and publish a verified manifest.
|
|
142
|
-
|
|
143
|
-
The algorithm has six phases:
|
|
144
|
-
1. Identify which stores exist on disk.
|
|
145
|
-
2. Create a staging directory.
|
|
146
|
-
3. Fence SQLite writers (BEGIN IMMEDIATE) then copy every store and
|
|
147
|
-
record per-file SHA-256 checksums (Phase 3). If a ``lance/``
|
|
148
|
-
directory exists under the base dir, copy it into staging as well.
|
|
149
|
-
4. Re-read every staging file and compare to Phase-3 hashes.
|
|
150
|
-
Mismatch → staging removed, BackupVerificationError raised.
|
|
151
|
-
5. Build the manifest from the verified checksums.
|
|
152
|
-
6. Atomically rename staging → final directory and write manifest.json.
|
|
153
|
-
|
|
154
|
-
The ``lance/`` directory (LanceDB vector index) is treated as an
|
|
155
|
-
out-of-manifest companion: it is copied recursively into the backup set
|
|
156
|
-
and restored alongside the SQLite stores. Its presence is noted in the
|
|
157
|
-
server log. If absent, the step is silently skipped.
|
|
158
|
-
|
|
159
|
-
Returns:
|
|
160
|
-
BackupSetManifest with verified=True.
|
|
161
|
-
|
|
162
|
-
Raises:
|
|
163
|
-
BackupVerificationError: if any staging file's content changed
|
|
164
|
-
between Phase 3 and Phase 4.
|
|
165
|
-
"""
|
|
166
|
-
set_id = uuid.uuid4().hex[:16]
|
|
167
|
-
epoch = int(time.time())
|
|
168
|
-
staging_dir = self._backup_dir / f".staging_{set_id}"
|
|
169
|
-
staging_dir.mkdir(parents=True, exist_ok=True)
|
|
170
|
-
|
|
171
|
-
existing_dbs = [
|
|
172
|
-
db for db in self._managed_databases
|
|
173
|
-
if (self._base_dir / db).exists()
|
|
174
|
-
]
|
|
175
|
-
sqlite_paths = [self._base_dir / db for db in existing_dbs]
|
|
176
|
-
|
|
177
|
-
# staging_records: (db_name, staging_path, size_bytes, phase3_sha256)
|
|
178
|
-
staging_records: list[tuple[str, Path, int, str]] = []
|
|
179
|
-
|
|
180
|
-
# Phases 2–3: fence writers, copy, hash
|
|
181
|
-
with self._writer_fence(sqlite_paths):
|
|
182
|
-
for db_name in existing_dbs:
|
|
183
|
-
src = self._base_dir / db_name
|
|
184
|
-
dest = staging_dir / db_name
|
|
185
|
-
self._sqlite_backup(src, dest)
|
|
186
|
-
sha = self._compute_entry_sha256(dest)
|
|
187
|
-
staging_records.append((db_name, dest, dest.stat().st_size, sha))
|
|
188
|
-
|
|
189
|
-
# Copy the LanceDB vector directory if present.
|
|
190
|
-
lance_src = self._base_dir / "lance"
|
|
191
|
-
if lance_src.is_dir():
|
|
192
|
-
lance_staging = staging_dir / "lance"
|
|
193
|
-
shutil.copytree(str(lance_src), str(lance_staging))
|
|
194
|
-
logger.info(
|
|
195
|
-
"Backup set %s: captured lance/ directory (%d items)",
|
|
196
|
-
set_id,
|
|
197
|
-
sum(1 for _ in lance_staging.rglob("*") if _.is_file()),
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
# Phase 4: re-verify every staging copy
|
|
201
|
-
for db_name, staging_path, _size, expected_sha in staging_records:
|
|
202
|
-
actual_sha = self._compute_entry_sha256(staging_path)
|
|
203
|
-
if actual_sha != expected_sha:
|
|
204
|
-
shutil.rmtree(str(staging_dir), ignore_errors=True)
|
|
205
|
-
raise BackupVerificationError(
|
|
206
|
-
f"Checksum mismatch for {db_name}: "
|
|
207
|
-
f"expected {expected_sha}, got {actual_sha}"
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
# Phase 5: build manifest (file_path points to where files will land)
|
|
211
|
-
final_dir = self._backup_dir / f"backup_{set_id}"
|
|
212
|
-
entries = tuple(
|
|
213
|
-
StoreEntry(
|
|
214
|
-
store_name=db_name,
|
|
215
|
-
file_path=str(final_dir / db_name),
|
|
216
|
-
size_bytes=size,
|
|
217
|
-
sha256=sha,
|
|
218
|
-
)
|
|
219
|
-
for db_name, _sp, size, sha in staging_records
|
|
220
|
-
)
|
|
221
|
-
manifest = BackupSetManifest(
|
|
222
|
-
set_id=set_id,
|
|
223
|
-
epoch=epoch,
|
|
224
|
-
stores=entries,
|
|
225
|
-
manifest_hash=self._compute_manifest_hash(entries),
|
|
226
|
-
verified=True,
|
|
227
|
-
created_at=datetime.now(timezone.utc).isoformat(),
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
# Phase 6: atomic publish
|
|
231
|
-
staging_dir.rename(final_dir)
|
|
232
|
-
(final_dir / "manifest.json").write_text(
|
|
233
|
-
json.dumps(asdict(manifest), indent=2)
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
return manifest
|
|
237
|
-
|
|
238
|
-
def restore_from_manifest(self, manifest: BackupSetManifest) -> None:
|
|
239
|
-
"""Restore all stores from a verified manifest.
|
|
240
|
-
|
|
241
|
-
The restore proceeds in three phases to guarantee cross-store atomicity:
|
|
242
|
-
|
|
243
|
-
Phase A — Verify:
|
|
244
|
-
Re-derive the manifest hash and verify every backup file's checksum.
|
|
245
|
-
No live files are touched until all checks pass. Raises
|
|
246
|
-
BackupRestoreError on any failure.
|
|
247
|
-
|
|
248
|
-
Phase B — Pre-restore snapshot:
|
|
249
|
-
Copy the current live version of every store being restored into a
|
|
250
|
-
``<store>.pre_restore`` sibling file, and the current ``lance/``
|
|
251
|
-
directory (if present) to ``lance.pre_restore/``. These snapshots
|
|
252
|
-
allow a full rollback if the write phase is interrupted.
|
|
253
|
-
|
|
254
|
-
Phase C — Staged write:
|
|
255
|
-
Copy each backup file to a ``<store>.restore_staging`` temporary,
|
|
256
|
-
then rename it into place. If any step raises, all pre-restore
|
|
257
|
-
snapshots are copied back to their live locations before the error
|
|
258
|
-
is re-raised, returning the live set to its original coherent state.
|
|
259
|
-
On full success, all snapshot files are removed.
|
|
260
|
-
|
|
261
|
-
The ``lance/`` directory companion is handled with the same snapshot and
|
|
262
|
-
rollback discipline: if the backup set contains a ``lance/`` subdirectory
|
|
263
|
-
it is restored recursively; absence is silently skipped on both sides.
|
|
264
|
-
|
|
265
|
-
Raises:
|
|
266
|
-
BackupRestoreError: on unverified manifest, hash mismatch, missing
|
|
267
|
-
or corrupted backup files, or if the staged write phase fails
|
|
268
|
-
and the rollback itself encounters an error.
|
|
269
|
-
"""
|
|
270
|
-
if not manifest.verified:
|
|
271
|
-
raise BackupRestoreError("Cannot restore from an unverified manifest")
|
|
272
|
-
|
|
273
|
-
# Phase A-1: Re-derive manifest_hash from the store entries and compare.
|
|
274
|
-
# This detects tampering of manifest.json where an attacker changes a
|
|
275
|
-
# store's sha256 entry without recalculating manifest_hash.
|
|
276
|
-
computed_hash = self._compute_manifest_hash(manifest.stores)
|
|
277
|
-
if computed_hash != manifest.manifest_hash:
|
|
278
|
-
raise BackupRestoreError(
|
|
279
|
-
"Manifest hash mismatch: backup set is incoherent or has been tampered"
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
# Phase A-2: Verify every backup file exists and matches its checksum.
|
|
283
|
-
for entry in manifest.stores:
|
|
284
|
-
src = Path(entry.file_path)
|
|
285
|
-
if not src.exists():
|
|
286
|
-
raise BackupRestoreError(f"Backup file missing: {entry.file_path}")
|
|
287
|
-
actual_sha = self._compute_entry_sha256(src)
|
|
288
|
-
if actual_sha != entry.sha256:
|
|
289
|
-
raise BackupRestoreError(
|
|
290
|
-
f"Corrupted backup file (checksum mismatch): {entry.store_name}"
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
# Determine the backup set directory.
|
|
294
|
-
# Primary: derive from the first store's file_path (most reliable).
|
|
295
|
-
# Fallback for empty-store manifests: reconstruct from the backup_dir +
|
|
296
|
-
# set_id, which is how create_backup_set names the final directory.
|
|
297
|
-
if manifest.stores:
|
|
298
|
-
backup_set_dir: Optional[Path] = Path(manifest.stores[0].file_path).parent
|
|
299
|
-
else:
|
|
300
|
-
candidate = self._backup_dir / f"backup_{manifest.set_id}"
|
|
301
|
-
backup_set_dir = candidate if candidate.is_dir() else None
|
|
302
|
-
|
|
303
|
-
lance_backup: Optional[Path] = (
|
|
304
|
-
backup_set_dir / "lance"
|
|
305
|
-
if backup_set_dir is not None and (backup_set_dir / "lance").is_dir()
|
|
306
|
-
else None
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
# Phase B: Snapshot the current live copies so we can roll back.
|
|
310
|
-
# pre_restore_map: live_target -> pre_restore_snapshot_path
|
|
311
|
-
pre_restore_map: dict[Path, Path] = {}
|
|
312
|
-
pre_restore_lance: Optional[Path] = None
|
|
313
|
-
try:
|
|
314
|
-
for entry in manifest.stores:
|
|
315
|
-
target = self._base_dir / entry.store_name
|
|
316
|
-
snapshot = target.parent / f"{entry.store_name}.pre_restore"
|
|
317
|
-
if target.exists():
|
|
318
|
-
shutil.copy2(str(target), str(snapshot))
|
|
319
|
-
pre_restore_map[target] = snapshot
|
|
320
|
-
|
|
321
|
-
live_lance = self._base_dir / "lance"
|
|
322
|
-
if lance_backup is not None and live_lance.is_dir():
|
|
323
|
-
pre_restore_lance = self._base_dir / "lance.pre_restore"
|
|
324
|
-
if pre_restore_lance.exists():
|
|
325
|
-
shutil.rmtree(str(pre_restore_lance))
|
|
326
|
-
shutil.copytree(str(live_lance), str(pre_restore_lance))
|
|
327
|
-
|
|
328
|
-
except Exception as exc:
|
|
329
|
-
# Snapshot creation failed — clean up any partial snapshots and abort
|
|
330
|
-
# before touching any live files.
|
|
331
|
-
self._cleanup_pre_restore_snapshots(pre_restore_map, pre_restore_lance)
|
|
332
|
-
raise BackupRestoreError(
|
|
333
|
-
f"Pre-restore snapshot failed, no live files were modified: {exc}"
|
|
334
|
-
) from exc
|
|
335
|
-
|
|
336
|
-
# Phase C: Write restored files into the live directory.
|
|
337
|
-
try:
|
|
338
|
-
for entry in manifest.stores:
|
|
339
|
-
target = self._base_dir / entry.store_name
|
|
340
|
-
staging = target.parent / f"{entry.store_name}.restore_staging"
|
|
341
|
-
shutil.copy2(entry.file_path, str(staging))
|
|
342
|
-
staging.rename(target)
|
|
343
|
-
|
|
344
|
-
if lance_backup is not None:
|
|
345
|
-
live_lance = self._base_dir / "lance"
|
|
346
|
-
lance_staging = self._base_dir / "lance.restore_staging"
|
|
347
|
-
if lance_staging.exists():
|
|
348
|
-
shutil.rmtree(str(lance_staging))
|
|
349
|
-
shutil.copytree(str(lance_backup), str(lance_staging))
|
|
350
|
-
if live_lance.exists():
|
|
351
|
-
shutil.rmtree(str(live_lance))
|
|
352
|
-
lance_staging.rename(live_lance)
|
|
353
|
-
logger.info("Restore: lance/ directory restored from backup set")
|
|
354
|
-
|
|
355
|
-
except Exception as exc:
|
|
356
|
-
# Staged write failed — roll back all live files from pre-restore
|
|
357
|
-
# snapshots so the live set returns to its original coherent state.
|
|
358
|
-
logger.error(
|
|
359
|
-
"Restore write phase failed (%s); rolling back live files to "
|
|
360
|
-
"pre-restore state.",
|
|
361
|
-
exc,
|
|
362
|
-
)
|
|
363
|
-
rollback_errors: list[str] = []
|
|
364
|
-
for target, snapshot in pre_restore_map.items():
|
|
365
|
-
if snapshot.exists():
|
|
366
|
-
try:
|
|
367
|
-
shutil.copy2(str(snapshot), str(target))
|
|
368
|
-
except Exception as rb_exc:
|
|
369
|
-
rollback_errors.append(f"{target.name}: {rb_exc}")
|
|
370
|
-
# Roll back the lance/ directory if a snapshot was taken.
|
|
371
|
-
if pre_restore_lance is not None and pre_restore_lance.exists():
|
|
372
|
-
try:
|
|
373
|
-
live_lance = self._base_dir / "lance"
|
|
374
|
-
if live_lance.exists():
|
|
375
|
-
shutil.rmtree(str(live_lance))
|
|
376
|
-
shutil.copytree(str(pre_restore_lance), str(live_lance))
|
|
377
|
-
except Exception as rb_exc:
|
|
378
|
-
rollback_errors.append(f"lance/: {rb_exc}")
|
|
379
|
-
|
|
380
|
-
self._cleanup_pre_restore_snapshots(pre_restore_map, pre_restore_lance)
|
|
381
|
-
|
|
382
|
-
if rollback_errors:
|
|
383
|
-
raise BackupRestoreError(
|
|
384
|
-
f"Restore failed and rollback encountered errors: "
|
|
385
|
-
f"{'; '.join(rollback_errors)}. Original error: {exc}"
|
|
386
|
-
) from exc
|
|
387
|
-
raise BackupRestoreError(
|
|
388
|
-
f"Restore write phase failed; live files rolled back to "
|
|
389
|
-
f"pre-restore state: {exc}"
|
|
390
|
-
) from exc
|
|
391
|
-
|
|
392
|
-
# Phase C succeeded — remove pre-restore snapshots.
|
|
393
|
-
self._cleanup_pre_restore_snapshots(pre_restore_map, pre_restore_lance)
|
|
394
|
-
|
|
395
|
-
# Phase D — GDPR obligation replay (invariant: no restore may resurrect
|
|
396
|
-
# erased personal data). Must run AFTER live files are in place and
|
|
397
|
-
# pre-restore snapshots are removed so there is no window where the
|
|
398
|
-
# erased data could be read. A replay failure raises immediately; the
|
|
399
|
-
# caller receives BackupRestoreError and must treat the restore as
|
|
400
|
-
# failed until an operator manually remediates.
|
|
401
|
-
if backup_set_dir is not None:
|
|
402
|
-
_replay_obligations_after_restore(
|
|
403
|
-
data_root=self._base_dir,
|
|
404
|
-
snapshot_key=str(backup_set_dir),
|
|
405
|
-
restored_db_paths=[
|
|
406
|
-
self._base_dir / e.store_name for e in manifest.stores
|
|
407
|
-
],
|
|
408
|
-
)
|
|
409
|
-
|
|
410
|
-
# ------------------------------------------------------------------
|
|
411
|
-
# Internal helpers (factored out for subclass testability)
|
|
412
|
-
# ------------------------------------------------------------------
|
|
413
|
-
|
|
414
|
-
def _compute_entry_sha256(self, path: Path) -> str:
|
|
415
|
-
"""Return the SHA-256 hex digest of a file's raw bytes."""
|
|
416
|
-
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
417
|
-
|
|
418
|
-
def _compute_manifest_hash(
|
|
419
|
-
self, entries: tuple[StoreEntry, ...]
|
|
420
|
-
) -> str:
|
|
421
|
-
"""Deterministic hash of all store checksums (sorted for stability)."""
|
|
422
|
-
sorted_checksums = sorted(e.sha256 for e in entries)
|
|
423
|
-
payload = "|".join(sorted_checksums).encode()
|
|
424
|
-
return hashlib.sha256(payload).hexdigest()
|
|
425
|
-
|
|
426
|
-
@contextmanager
|
|
427
|
-
def _writer_fence(
|
|
428
|
-
self, db_paths: list[Path]
|
|
429
|
-
) -> Generator[None, None, None]:
|
|
430
|
-
"""Hold BEGIN IMMEDIATE on every live SQLite DB during the copy window.
|
|
431
|
-
|
|
432
|
-
This blocks concurrent writers for the duration of the copy loop,
|
|
433
|
-
ensuring the source files do not change while being read by
|
|
434
|
-
sqlite3.backup(). Connections are rolled back and closed on exit.
|
|
435
|
-
"""
|
|
436
|
-
conns: list[sqlite3.Connection] = []
|
|
437
|
-
for path in db_paths:
|
|
438
|
-
if path.exists():
|
|
439
|
-
conn = sqlite3.connect(str(path))
|
|
440
|
-
conn.execute("BEGIN IMMEDIATE")
|
|
441
|
-
conns.append(conn)
|
|
442
|
-
try:
|
|
443
|
-
yield
|
|
444
|
-
finally:
|
|
445
|
-
for conn in conns:
|
|
446
|
-
try:
|
|
447
|
-
conn.rollback()
|
|
448
|
-
conn.close()
|
|
449
|
-
except Exception: # pragma: no cover – cleanup best-effort
|
|
450
|
-
pass
|
|
451
|
-
|
|
452
|
-
def _sqlite_backup(self, src: Path, dest: Path) -> None:
|
|
453
|
-
"""Copy a SQLite database using the Online Backup API (hot copy)."""
|
|
454
|
-
src_conn = sqlite3.connect(str(src))
|
|
455
|
-
dst_conn = sqlite3.connect(str(dest))
|
|
456
|
-
try:
|
|
457
|
-
src_conn.backup(dst_conn)
|
|
458
|
-
finally:
|
|
459
|
-
dst_conn.close()
|
|
460
|
-
src_conn.close()
|
|
461
|
-
|
|
462
|
-
@staticmethod
|
|
463
|
-
def _cleanup_pre_restore_snapshots(
|
|
464
|
-
snapshot_map: dict[Path, Path],
|
|
465
|
-
lance_snapshot: Optional[Path],
|
|
466
|
-
) -> None:
|
|
467
|
-
"""Remove pre-restore snapshot files and directories.
|
|
468
|
-
|
|
469
|
-
Called after a successful restore to tidy up, and on the error path
|
|
470
|
-
after rollback completes. Best-effort: individual removal failures are
|
|
471
|
-
logged but do not raise.
|
|
472
|
-
"""
|
|
473
|
-
for snapshot_path in snapshot_map.values():
|
|
474
|
-
if snapshot_path.exists():
|
|
475
|
-
try:
|
|
476
|
-
snapshot_path.unlink()
|
|
477
|
-
except OSError as exc:
|
|
478
|
-
logger.warning(
|
|
479
|
-
"Could not remove pre-restore snapshot %s: %s",
|
|
480
|
-
snapshot_path.name, exc,
|
|
481
|
-
)
|
|
482
|
-
if lance_snapshot is not None and lance_snapshot.exists():
|
|
483
|
-
try:
|
|
484
|
-
shutil.rmtree(str(lance_snapshot))
|
|
485
|
-
except OSError as exc:
|
|
486
|
-
logger.warning(
|
|
487
|
-
"Could not remove lance pre-restore snapshot %s: %s",
|
|
488
|
-
lance_snapshot, exc,
|
|
489
|
-
)
|
|
490
|
-
|
|
491
|
-
# ---------------------------------------------------------------------------
|
|
492
|
-
# Legacy per-file backup manager (preserved for backward compatibility)
|
|
493
|
-
# ---------------------------------------------------------------------------
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
class BackupManager:
|
|
497
|
-
"""Automated backup manager for SuperLocalMemory V3.
|
|
498
|
-
|
|
499
|
-
Args:
|
|
500
|
-
db_path: Path to the primary database file.
|
|
501
|
-
backup_dir: Directory where backup files are stored.
|
|
502
|
-
base_dir: Base SLM directory (used for config file + learning DB).
|
|
503
|
-
"""
|
|
504
|
-
|
|
505
|
-
def __init__(
|
|
506
|
-
self,
|
|
507
|
-
db_path: Optional[Path] = None,
|
|
508
|
-
backup_dir: Optional[Path] = None,
|
|
509
|
-
base_dir: Optional[Path] = None,
|
|
510
|
-
) -> None:
|
|
511
|
-
self.base_dir = Path(base_dir) if base_dir is not None else canonical_data_root()
|
|
512
|
-
self.db_path = db_path or (self.base_dir / "memory.db")
|
|
513
|
-
self.backup_dir = backup_dir or (self.base_dir / "backups")
|
|
514
|
-
self._config_file = self.base_dir / "backup_config.json"
|
|
515
|
-
self.config = self._load_config()
|
|
516
|
-
self._ensure_backup_dir()
|
|
517
|
-
|
|
518
|
-
# ------------------------------------------------------------------
|
|
519
|
-
# Config management
|
|
520
|
-
# ------------------------------------------------------------------
|
|
521
|
-
|
|
522
|
-
def _ensure_backup_dir(self) -> None:
|
|
523
|
-
self.backup_dir.mkdir(parents=True, exist_ok=True)
|
|
524
|
-
|
|
525
|
-
def _load_config(self) -> Dict:
|
|
526
|
-
if self._config_file.exists():
|
|
527
|
-
try:
|
|
528
|
-
raw = json.loads(self._config_file.read_text())
|
|
529
|
-
defaults = self._default_config()
|
|
530
|
-
for k in defaults:
|
|
531
|
-
raw.setdefault(k, defaults[k])
|
|
532
|
-
return raw
|
|
533
|
-
except (json.JSONDecodeError, IOError):
|
|
534
|
-
pass
|
|
535
|
-
return self._default_config()
|
|
536
|
-
|
|
537
|
-
@staticmethod
|
|
538
|
-
def _default_config() -> Dict:
|
|
539
|
-
return {
|
|
540
|
-
"enabled": True,
|
|
541
|
-
"interval_hours": DEFAULT_INTERVAL_HOURS,
|
|
542
|
-
"max_backups": DEFAULT_MAX_BACKUPS,
|
|
543
|
-
"last_backup": None,
|
|
544
|
-
"last_backup_file": None,
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
def _save_config(self) -> None:
|
|
548
|
-
try:
|
|
549
|
-
self._config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
550
|
-
self._config_file.write_text(json.dumps(self.config, indent=2))
|
|
551
|
-
except IOError as exc:
|
|
552
|
-
logger.error("Failed to save backup config: %s", exc)
|
|
553
|
-
|
|
554
|
-
def configure(
|
|
555
|
-
self,
|
|
556
|
-
interval_hours: Optional[int] = None,
|
|
557
|
-
max_backups: Optional[int] = None,
|
|
558
|
-
enabled: Optional[bool] = None,
|
|
559
|
-
) -> Dict:
|
|
560
|
-
"""Update backup configuration and return current status."""
|
|
561
|
-
if interval_hours is not None:
|
|
562
|
-
self.config["interval_hours"] = max(MIN_INTERVAL_HOURS, interval_hours)
|
|
563
|
-
if max_backups is not None:
|
|
564
|
-
self.config["max_backups"] = max(1, max_backups)
|
|
565
|
-
if enabled is not None:
|
|
566
|
-
self.config["enabled"] = enabled
|
|
567
|
-
self._save_config()
|
|
568
|
-
return self.get_status()
|
|
569
|
-
|
|
570
|
-
# ------------------------------------------------------------------
|
|
571
|
-
# Scheduling helpers
|
|
572
|
-
# ------------------------------------------------------------------
|
|
573
|
-
|
|
574
|
-
def is_backup_due(self) -> bool:
|
|
575
|
-
"""Return ``True`` when a backup should be taken."""
|
|
576
|
-
if not self.config.get("enabled", True):
|
|
577
|
-
return False
|
|
578
|
-
last = self.config.get("last_backup")
|
|
579
|
-
if not last:
|
|
580
|
-
return True
|
|
581
|
-
try:
|
|
582
|
-
last_dt = datetime.fromisoformat(last)
|
|
583
|
-
interval = timedelta(hours=self.config.get("interval_hours", DEFAULT_INTERVAL_HOURS))
|
|
584
|
-
return datetime.now() >= last_dt + interval
|
|
585
|
-
except (ValueError, TypeError):
|
|
586
|
-
return True
|
|
587
|
-
|
|
588
|
-
def check_and_backup(self) -> Optional[str]:
|
|
589
|
-
"""Create a backup only when one is due. Returns filename or ``None``."""
|
|
590
|
-
if not self.is_backup_due():
|
|
591
|
-
return None
|
|
592
|
-
return self.create_backup()
|
|
593
|
-
|
|
594
|
-
# ------------------------------------------------------------------
|
|
595
|
-
# Core backup / restore
|
|
596
|
-
# ------------------------------------------------------------------
|
|
597
|
-
|
|
598
|
-
def create_backup(self, label: Optional[str] = None) -> str:
|
|
599
|
-
"""Create a timestamped backup via the SQLite online-backup API.
|
|
600
|
-
|
|
601
|
-
Returns:
|
|
602
|
-
Backup filename on success, empty string on failure.
|
|
603
|
-
"""
|
|
604
|
-
if not self.db_path.exists():
|
|
605
|
-
logger.warning("No database to backup at %s", self.db_path)
|
|
606
|
-
return ""
|
|
607
|
-
|
|
608
|
-
self._ensure_backup_dir()
|
|
609
|
-
|
|
610
|
-
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
|
|
611
|
-
suffix = f"-{label}" if label else ""
|
|
612
|
-
backup_name = f"memory-{timestamp}{suffix}.db"
|
|
613
|
-
backup_path = self.backup_dir / backup_name
|
|
614
|
-
|
|
615
|
-
try:
|
|
616
|
-
source = sqlite3.connect(str(self.db_path))
|
|
617
|
-
dest = sqlite3.connect(str(backup_path))
|
|
618
|
-
try:
|
|
619
|
-
source.backup(dest)
|
|
620
|
-
finally:
|
|
621
|
-
dest.close()
|
|
622
|
-
source.close()
|
|
623
|
-
|
|
624
|
-
size_mb = backup_path.stat().st_size / (1024 * 1024)
|
|
625
|
-
self.config["last_backup"] = datetime.now(timezone.utc).isoformat()
|
|
626
|
-
self.config["last_backup_file"] = backup_name
|
|
627
|
-
self._save_config()
|
|
628
|
-
logger.info("Backup created: %s (%.1f MB)", backup_name, size_mb)
|
|
629
|
-
|
|
630
|
-
# v3.4.10: Backup ALL .db files in the SLM directory
|
|
631
|
-
self._backup_all_dbs(timestamp, suffix)
|
|
632
|
-
|
|
633
|
-
self._enforce_retention()
|
|
634
|
-
return backup_name
|
|
635
|
-
|
|
636
|
-
except Exception as exc:
|
|
637
|
-
logger.error("Backup failed: %s", exc)
|
|
638
|
-
if backup_path.exists():
|
|
639
|
-
backup_path.unlink()
|
|
640
|
-
return ""
|
|
641
|
-
|
|
642
|
-
def _backup_all_dbs(self, timestamp: str, suffix: str) -> None:
|
|
643
|
-
"""Backup all SLM-managed databases alongside the main memory.db.
|
|
644
|
-
|
|
645
|
-
Uses the managed database registry — only backs up databases that
|
|
646
|
-
SLM knows about. Add new databases to MANAGED_DATABASES when new
|
|
647
|
-
modules create them.
|
|
648
|
-
"""
|
|
649
|
-
slm_dir = self.db_path.parent
|
|
650
|
-
backed_up = 0
|
|
651
|
-
for db_name in MANAGED_DATABASES:
|
|
652
|
-
if db_name == "memory.db":
|
|
653
|
-
continue # Already backed up by create_backup()
|
|
654
|
-
db_file = slm_dir / db_name
|
|
655
|
-
if not db_file.exists():
|
|
656
|
-
continue # This user doesn't have this DB — skip
|
|
657
|
-
|
|
658
|
-
try:
|
|
659
|
-
prefix = db_file.stem
|
|
660
|
-
name = f"{prefix}-{timestamp}{suffix}.db"
|
|
661
|
-
path = self.backup_dir / name
|
|
662
|
-
src = sqlite3.connect(str(db_file))
|
|
663
|
-
dst = sqlite3.connect(str(path))
|
|
664
|
-
try:
|
|
665
|
-
src.backup(dst)
|
|
666
|
-
finally:
|
|
667
|
-
dst.close()
|
|
668
|
-
src.close()
|
|
669
|
-
backed_up += 1
|
|
670
|
-
logger.info(
|
|
671
|
-
"Backup: %s (%.1f MB)", name,
|
|
672
|
-
path.stat().st_size / (1024 * 1024),
|
|
673
|
-
)
|
|
674
|
-
except Exception as exc:
|
|
675
|
-
logger.warning(
|
|
676
|
-
"%s backup failed (non-critical): %s",
|
|
677
|
-
db_name, exc,
|
|
678
|
-
)
|
|
679
|
-
if backed_up:
|
|
680
|
-
logger.info("Backed up %d companion databases", backed_up)
|
|
681
|
-
|
|
682
|
-
def _enforce_retention(self) -> None:
|
|
683
|
-
"""Remove old backups exceeding the configured max."""
|
|
684
|
-
max_backups = self.config.get("max_backups", DEFAULT_MAX_BACKUPS)
|
|
685
|
-
# Build patterns from the managed database registry
|
|
686
|
-
patterns = [f"{Path(db).stem}-*.db" for db in MANAGED_DATABASES]
|
|
687
|
-
for pattern in patterns:
|
|
688
|
-
backups = sorted(
|
|
689
|
-
self.backup_dir.glob(pattern),
|
|
690
|
-
key=lambda f: f.stat().st_mtime,
|
|
691
|
-
)
|
|
692
|
-
while len(backups) > max_backups:
|
|
693
|
-
oldest = backups.pop(0)
|
|
694
|
-
try:
|
|
695
|
-
oldest.unlink()
|
|
696
|
-
logger.info("Removed old backup: %s", oldest.name)
|
|
697
|
-
except OSError as exc:
|
|
698
|
-
logger.error("Failed to remove backup %s: %s", oldest.name, exc)
|
|
699
|
-
|
|
700
|
-
def restore_backup(self, filename: str) -> bool:
|
|
701
|
-
"""Restore the database from *filename*.
|
|
702
|
-
|
|
703
|
-
A safety snapshot of the current state is taken first.
|
|
704
|
-
"""
|
|
705
|
-
# Containment: filename must be a bare .db name inside backup_dir — no
|
|
706
|
-
# path separators or traversal. Prevents restoring (and thus copying
|
|
707
|
-
# over memory.db) an arbitrary file the daemon user can read.
|
|
708
|
-
if (not filename or "/" in filename or "\\" in filename
|
|
709
|
-
or ".." in filename or not filename.endswith(".db")):
|
|
710
|
-
logger.error("Restore rejected: invalid backup filename: %r", filename)
|
|
711
|
-
return False
|
|
712
|
-
backup_dir = self.backup_dir.resolve()
|
|
713
|
-
backup_path = (self.backup_dir / filename).resolve()
|
|
714
|
-
if backup_path.parent != backup_dir:
|
|
715
|
-
logger.error("Restore rejected: path escapes backup dir: %r", filename)
|
|
716
|
-
return False
|
|
717
|
-
if not backup_path.exists():
|
|
718
|
-
logger.error("Backup not found: %s", filename)
|
|
719
|
-
return False
|
|
720
|
-
|
|
721
|
-
# Derive the target database from the backup filename stem.
|
|
722
|
-
# Backup files are named "{stem}-{timestamp}.db", where stem is the
|
|
723
|
-
# database name without extension (e.g., "audit_chain" for audit_chain.db).
|
|
724
|
-
stem_map = {Path(db).stem: db for db in MANAGED_DATABASES}
|
|
725
|
-
file_stem = filename.split("-", 1)[0]
|
|
726
|
-
target_name = stem_map.get(file_stem)
|
|
727
|
-
if target_name is None:
|
|
728
|
-
logger.error(
|
|
729
|
-
"Restore rejected: unrecognised database stem %r in %r; "
|
|
730
|
-
"expected one of %s",
|
|
731
|
-
file_stem,
|
|
732
|
-
filename,
|
|
733
|
-
list(stem_map),
|
|
734
|
-
)
|
|
735
|
-
return False
|
|
736
|
-
target = self.db_path.parent / target_name
|
|
737
|
-
|
|
738
|
-
try:
|
|
739
|
-
# Stage the source OUTSIDE backup_dir before anything else runs.
|
|
740
|
-
#
|
|
741
|
-
# create_backup() below calls _enforce_retention(), which globs this
|
|
742
|
-
# same directory and unlinks the oldest files — including, when the
|
|
743
|
-
# backup being restored is the oldest, the source itself. A plain
|
|
744
|
-
# sqlite3.connect() on that now-missing path RECREATES it as an
|
|
745
|
-
# empty database, which is then copied over the live store while the
|
|
746
|
-
# call returns True, leaving a zero-byte file under the original
|
|
747
|
-
# name so a second attempt also appears to succeed.
|
|
748
|
-
#
|
|
749
|
-
# Reproduced before this fix: 11 snapshots, max_backups=10, a
|
|
750
|
-
# 500-fact store restored to 0 tables, restore_backup() -> True.
|
|
751
|
-
with tempfile.TemporaryDirectory(prefix="slm-restore-") as staging:
|
|
752
|
-
staged = Path(staging) / filename
|
|
753
|
-
|
|
754
|
-
# mode=ro fails loudly on a missing file instead of creating one.
|
|
755
|
-
src_ro = sqlite3.connect(f"file:{backup_path}?mode=ro", uri=True)
|
|
756
|
-
try:
|
|
757
|
-
if not [r[0] for r in src_ro.execute(
|
|
758
|
-
"SELECT name FROM sqlite_master WHERE type='table'")]:
|
|
759
|
-
logger.error(
|
|
760
|
-
"Restore rejected: %s contains no tables — refusing to "
|
|
761
|
-
"overwrite %s with an empty database",
|
|
762
|
-
filename, target_name,
|
|
763
|
-
)
|
|
764
|
-
return False
|
|
765
|
-
staged_dst = sqlite3.connect(str(staged))
|
|
766
|
-
try:
|
|
767
|
-
src_ro.backup(staged_dst)
|
|
768
|
-
finally:
|
|
769
|
-
staged_dst.close()
|
|
770
|
-
finally:
|
|
771
|
-
src_ro.close()
|
|
772
|
-
|
|
773
|
-
self.create_backup(label="pre-restore")
|
|
774
|
-
|
|
775
|
-
# Restore from the staged copy, which retention cannot reach.
|
|
776
|
-
src = sqlite3.connect(f"file:{staged}?mode=ro", uri=True)
|
|
777
|
-
dst = sqlite3.connect(str(target))
|
|
778
|
-
try:
|
|
779
|
-
src.backup(dst)
|
|
780
|
-
finally:
|
|
781
|
-
dst.close()
|
|
782
|
-
src.close()
|
|
783
|
-
|
|
784
|
-
logger.info("Restored: %s -> %s", filename, target.name)
|
|
785
|
-
|
|
786
|
-
# GDPR obligation replay — prevent a restore from resurrecting
|
|
787
|
-
# previously erased personal data. Failure is fatal: return False
|
|
788
|
-
# so the caller knows the restore is not clean.
|
|
789
|
-
try:
|
|
790
|
-
_replay_obligations_after_restore(
|
|
791
|
-
data_root=self.db_path.parent,
|
|
792
|
-
snapshot_key=str(backup_path),
|
|
793
|
-
restored_db_paths=[target],
|
|
794
|
-
)
|
|
795
|
-
except BackupRestoreError as exc:
|
|
796
|
-
logger.error(
|
|
797
|
-
"Restore obligation replay failed for %s: %s — "
|
|
798
|
-
"restore is NOT clean; erased data may be present",
|
|
799
|
-
filename, exc,
|
|
800
|
-
)
|
|
801
|
-
return False
|
|
802
|
-
|
|
803
|
-
return True
|
|
804
|
-
|
|
805
|
-
except Exception as exc:
|
|
806
|
-
logger.error("Restore failed: %s", exc)
|
|
807
|
-
return False
|
|
808
|
-
|
|
809
|
-
# ------------------------------------------------------------------
|
|
810
|
-
# Listing / status
|
|
811
|
-
# ------------------------------------------------------------------
|
|
812
|
-
|
|
813
|
-
def list_backups(self) -> List[Dict]:
|
|
814
|
-
"""Return metadata for all available backups (newest first)."""
|
|
815
|
-
if not self.backup_dir.exists():
|
|
816
|
-
return []
|
|
817
|
-
|
|
818
|
-
result: List[Dict] = []
|
|
819
|
-
for pattern in ("memory-*.db", "learning-*.db"):
|
|
820
|
-
for f in sorted(self.backup_dir.glob(pattern), key=lambda p: p.stat().st_mtime, reverse=True):
|
|
821
|
-
st = f.stat()
|
|
822
|
-
db_type = "learning" if f.name.startswith("learning-") else "memory"
|
|
823
|
-
result.append({
|
|
824
|
-
"filename": f.name,
|
|
825
|
-
"path": str(f),
|
|
826
|
-
"size_mb": round(st.st_size / (1024 * 1024), 2),
|
|
827
|
-
"created": datetime.fromtimestamp(st.st_mtime).isoformat(),
|
|
828
|
-
"age_hours": round(
|
|
829
|
-
(datetime.now() - datetime.fromtimestamp(st.st_mtime)).total_seconds() / 3600, 1
|
|
830
|
-
),
|
|
831
|
-
"type": db_type,
|
|
832
|
-
})
|
|
833
|
-
result.sort(key=lambda b: b["created"], reverse=True)
|
|
834
|
-
return result
|
|
835
|
-
|
|
836
|
-
def get_status(self) -> Dict:
|
|
837
|
-
"""Return a status summary of the backup system."""
|
|
838
|
-
backups = self.list_backups()
|
|
839
|
-
next_backup = None
|
|
840
|
-
|
|
841
|
-
if self.config.get("enabled") and self.config.get("last_backup"):
|
|
842
|
-
try:
|
|
843
|
-
last_dt = datetime.fromisoformat(self.config["last_backup"])
|
|
844
|
-
interval = timedelta(hours=self.config.get("interval_hours", DEFAULT_INTERVAL_HOURS))
|
|
845
|
-
nxt = last_dt + interval
|
|
846
|
-
next_backup = nxt.isoformat() if nxt > datetime.now() else "overdue"
|
|
847
|
-
except (ValueError, TypeError):
|
|
848
|
-
next_backup = "unknown"
|
|
849
|
-
|
|
850
|
-
hours = self.config.get("interval_hours", DEFAULT_INTERVAL_HOURS)
|
|
851
|
-
if hours >= 168:
|
|
852
|
-
display = f"{hours // 168} week(s)"
|
|
853
|
-
elif hours >= 24:
|
|
854
|
-
display = f"{hours // 24} day(s)"
|
|
855
|
-
else:
|
|
856
|
-
display = f"{hours} hour(s)"
|
|
857
|
-
|
|
858
|
-
mem_bk = [b for b in backups if b.get("type") == "memory"]
|
|
859
|
-
learn_bk = [b for b in backups if b.get("type") == "learning"]
|
|
860
|
-
|
|
861
|
-
return {
|
|
862
|
-
"enabled": self.config.get("enabled", True),
|
|
863
|
-
"interval_hours": hours,
|
|
864
|
-
"interval_display": display,
|
|
865
|
-
"max_backups": self.config.get("max_backups", DEFAULT_MAX_BACKUPS),
|
|
866
|
-
"last_backup": self.config.get("last_backup"),
|
|
867
|
-
"last_backup_file": self.config.get("last_backup_file"),
|
|
868
|
-
"next_backup": next_backup,
|
|
869
|
-
"backup_count": len(mem_bk),
|
|
870
|
-
"learning_backup_count": len(learn_bk),
|
|
871
|
-
"total_size_mb": round(sum(b["size_mb"] for b in backups), 2),
|
|
872
|
-
"backups": backups,
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
# ---------------------------------------------------------------------------
|
|
877
|
-
# GDPR obligation replay — module-level so both backup classes can call it
|
|
878
|
-
# ---------------------------------------------------------------------------
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
def _replay_obligations_after_restore(
|
|
882
|
-
data_root: Path,
|
|
883
|
-
snapshot_key: str,
|
|
884
|
-
restored_db_paths: list[Path],
|
|
885
|
-
) -> None:
|
|
886
|
-
"""Re-apply pending erasure obligations to freshly restored database files.
|
|
887
|
-
|
|
888
|
-
This function is the enforcement point for the restore-replay invariant:
|
|
889
|
-
no restore may resurface data that was Art.17-erased from the live stores.
|
|
890
|
-
|
|
891
|
-
``snapshot_key`` is the string path that was recorded as the obligation's
|
|
892
|
-
``snapshot_path`` when the obligation was created (typically the backup-set
|
|
893
|
-
directory for new-style backups, or the per-file `.db` path for legacy
|
|
894
|
-
backups).
|
|
895
|
-
|
|
896
|
-
Raises:
|
|
897
|
-
BackupRestoreError: if any obligation cannot be replayed. The caller
|
|
898
|
-
must treat the restore as failed and surface this to the operator.
|
|
899
|
-
"""
|
|
900
|
-
store = BackupObligationStore(data_root)
|
|
901
|
-
obligations = store.list_pending_for_snapshot(snapshot_key)
|
|
902
|
-
|
|
903
|
-
# Belt-and-suspenders: even when path matching fails (e.g. backup moved),
|
|
904
|
-
# detect erased profiles by scanning the restored DB and checking whether
|
|
905
|
-
# any of the profiles present have pending obligations.
|
|
906
|
-
if not obligations:
|
|
907
|
-
for db_path in restored_db_paths:
|
|
908
|
-
if not db_path.exists():
|
|
909
|
-
continue
|
|
910
|
-
try:
|
|
911
|
-
import sqlite3 as _sq3
|
|
912
|
-
conn = _sq3.connect(str(db_path))
|
|
913
|
-
try:
|
|
914
|
-
tables = {
|
|
915
|
-
r[0] for r in
|
|
916
|
-
conn.execute(
|
|
917
|
-
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
918
|
-
).fetchall()
|
|
919
|
-
}
|
|
920
|
-
for tbl in ("profiles", "atomic_facts"):
|
|
921
|
-
if tbl not in tables:
|
|
922
|
-
continue
|
|
923
|
-
cols = {
|
|
924
|
-
r[1] for r in
|
|
925
|
-
conn.execute(f"PRAGMA table_info({tbl})").fetchall()
|
|
926
|
-
}
|
|
927
|
-
if "profile_id" not in cols:
|
|
928
|
-
continue
|
|
929
|
-
for (pid,) in conn.execute(
|
|
930
|
-
f"SELECT DISTINCT profile_id FROM {tbl}"
|
|
931
|
-
).fetchall():
|
|
932
|
-
if pid:
|
|
933
|
-
obligations.extend(
|
|
934
|
-
store.list_pending_for_profile(pid)
|
|
935
|
-
)
|
|
936
|
-
finally:
|
|
937
|
-
conn.close()
|
|
938
|
-
except Exception as exc: # noqa: BLE001
|
|
939
|
-
logger.warning(
|
|
940
|
-
"_replay_obligations_after_restore: scan failed for %s: %s",
|
|
941
|
-
db_path.name, exc,
|
|
942
|
-
)
|
|
943
|
-
|
|
944
|
-
if not obligations:
|
|
945
|
-
return
|
|
946
|
-
|
|
947
|
-
profile_ids = {o["profile_id"] for o in obligations}
|
|
948
|
-
|
|
949
|
-
for profile_id in profile_ids:
|
|
950
|
-
for db_path in restored_db_paths:
|
|
951
|
-
if not db_path.exists():
|
|
952
|
-
continue
|
|
953
|
-
try:
|
|
954
|
-
deleted = erase_profile_from_snapshot(db_path, profile_id)
|
|
955
|
-
if deleted:
|
|
956
|
-
logger.info(
|
|
957
|
-
"Restore replay: erased profile %r from %s: %s",
|
|
958
|
-
profile_id, db_path.name, deleted,
|
|
959
|
-
)
|
|
960
|
-
except Exception as exc: # noqa: BLE001
|
|
961
|
-
raise BackupRestoreError(
|
|
962
|
-
f"Erasure obligation replay failed for profile {profile_id!r} "
|
|
963
|
-
f"in {db_path.name}: {exc}. "
|
|
964
|
-
"The restored database may contain previously erased personal "
|
|
965
|
-
"data. Manual remediation required."
|
|
966
|
-
) from exc
|
|
967
|
-
|
|
968
|
-
# Discharge obligations for this specific snapshot so they do not block
|
|
969
|
-
# the completeness flag for future erasures of the same profile.
|
|
970
|
-
discharged = store.discharge_for_snapshot(snapshot_key, "replayed_on_restore")
|
|
971
|
-
logger.info(
|
|
972
|
-
"Obligation replay: discharged %d obligations for snapshot %s",
|
|
973
|
-
discharged, snapshot_key,
|
|
974
|
-
)
|