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,1185 +0,0 @@
|
|
|
1
|
-
"""CacheDB — wraps DatabaseManager for llmcache.db operations.
|
|
2
|
-
|
|
3
|
-
INTERFACE-CONTRACT §1 CONFORMANCE: Every method below is the canonical API.
|
|
4
|
-
No implementer may add aliases. Consumers (LLD-02/03/04/08) import this exact API.
|
|
5
|
-
|
|
6
|
-
REUSE: DatabaseManager from superlocalmemory/src/superlocalmemory/storage/database.py
|
|
7
|
-
- WAL mode (database.py:66-74)
|
|
8
|
-
- busy_timeout=10000ms (database.py:41)
|
|
9
|
-
- _MAX_RETRIES=5 exponential backoff (database.py:127-148)
|
|
10
|
-
- _connect() with row_factory (database.py:95-100)
|
|
11
|
-
- transaction() context manager (database.py:102-116)
|
|
12
|
-
- execute() with retry (database.py:118-148)
|
|
13
|
-
|
|
14
|
-
ENCRYPTION (resolves SEC-C-01 / CWE-312, NEW-M-01, NEW-M-02):
|
|
15
|
-
- All value BLOBs (llmcache_entries.value_blob) are AES-256-GCM encrypted.
|
|
16
|
-
- CCR original_blob is ALSO AES-256-GCM encrypted.
|
|
17
|
-
- Key storage: a single MACHINE-WIDE key file (~/.superlocalmemory/opt-key.bin,
|
|
18
|
-
0o600) is generated once and reused for all cache DBs on the machine. (The
|
|
19
|
-
per-DB salt below is persisted for provenance but does NOT make the AES key
|
|
20
|
-
per-DB — a single install has one llmcache.db, so a machine-wide key is the
|
|
21
|
-
intended model. A tampered/rotated key now degrades to a cache MISS, not a
|
|
22
|
-
crash — see _decrypt fail-open, v3.6.12 cache-1.)
|
|
23
|
-
- Salt: os.urandom(32) generated ONCE at DB creation, stored in
|
|
24
|
-
llmcache_schema_version.description='salt:<hex>'. NO hardcoded salt.
|
|
25
|
-
- Nonce (12 bytes random) prepended to each ciphertext.
|
|
26
|
-
- llmcache.db file permissions set to 0o600 at creation.
|
|
27
|
-
|
|
28
|
-
ISOLATION GUARANTEE: CacheDB MUST NOT import from superlocalmemory.storage.models.
|
|
29
|
-
Enforced by test_no_memory_db_import() in test_db.py.
|
|
30
|
-
|
|
31
|
-
FAIL-OPEN CONTRACT: Every public method catches sqlite3.Error, logs at WARNING,
|
|
32
|
-
and returns a safe default (None, [], False, 0) rather than raising.
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
from __future__ import annotations
|
|
36
|
-
|
|
37
|
-
import dataclasses
|
|
38
|
-
import json
|
|
39
|
-
import logging
|
|
40
|
-
import os
|
|
41
|
-
import platform
|
|
42
|
-
import re
|
|
43
|
-
import sqlite3
|
|
44
|
-
import time
|
|
45
|
-
import uuid
|
|
46
|
-
import zlib
|
|
47
|
-
from dataclasses import dataclass, field
|
|
48
|
-
from pathlib import Path
|
|
49
|
-
from typing import Any
|
|
50
|
-
|
|
51
|
-
from cryptography.exceptions import InvalidTag
|
|
52
|
-
from cryptography.hazmat.primitives import hashes
|
|
53
|
-
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
54
|
-
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
55
|
-
|
|
56
|
-
from superlocalmemory.infra.data_root import DynamicStatePath, state_path
|
|
57
|
-
from superlocalmemory.optimize.storage import schema as _schema
|
|
58
|
-
from superlocalmemory.storage.database import DatabaseManager
|
|
59
|
-
|
|
60
|
-
logger = logging.getLogger(__name__)
|
|
61
|
-
|
|
62
|
-
_DEFAULT_TENANT: str = "default"
|
|
63
|
-
_TENANT_HEX64 = re.compile(r"[0-9a-f]{64}")
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _normalize_tenant_id(tenant_id: str) -> str:
|
|
67
|
-
"""Match CacheManager.build_key tenant normalization (v3.6.10 fix).
|
|
68
|
-
|
|
69
|
-
The proxy stores entries under SHA-256(tenant) when the tenant is not
|
|
70
|
-
already a 64-char hex digest. Public tenant-scoped helpers (entry_count,
|
|
71
|
-
clear_tenant, entry_exists) MUST apply the same hashing or they query the
|
|
72
|
-
wrong tenant — the cause of the dashboard reporting "0 entries" while the
|
|
73
|
-
cache is in fact populated.
|
|
74
|
-
"""
|
|
75
|
-
tid = tenant_id or _DEFAULT_TENANT
|
|
76
|
-
if _TENANT_HEX64.fullmatch(tid):
|
|
77
|
-
return tid
|
|
78
|
-
import hashlib as _hashlib
|
|
79
|
-
return _hashlib.sha256(tid.encode()).hexdigest()
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
_ZLIB_LEVEL: int = 6
|
|
83
|
-
_AES_NONCE_BYTES: int = 12
|
|
84
|
-
_PBKDF2_ITERATIONS: int = 100_000
|
|
85
|
-
|
|
86
|
-
LLMCACHE_DIRNAME: str = ".superlocalmemory"
|
|
87
|
-
LLMCACHE_DBNAME: str = "llmcache.db"
|
|
88
|
-
MID_FILENAME: str = ".llmcache_key"
|
|
89
|
-
SALT_PREFIX: str = "salt:"
|
|
90
|
-
|
|
91
|
-
# C-06: persisted AES key — survives machine-id changes after first run
|
|
92
|
-
_KEY_FILE = DynamicStatePath("opt-key.bin")
|
|
93
|
-
|
|
94
|
-
_FORBIDDEN_MEMORY_TABLES: frozenset[str] = frozenset({
|
|
95
|
-
"memories", "atomic_facts", "profiles", "canonical_entities",
|
|
96
|
-
"entity_aliases", "consolidation_log", "trust_scores", "bm25_tokens",
|
|
97
|
-
"fact_retention", "core_memory_blocks", "ccq_consolidated_blocks",
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
# ---------------------------------------------------------------------------
|
|
102
|
-
# MetricsSnapshot — single definition (INTERFACE-CONTRACT v2.2 §6)
|
|
103
|
-
# ---------------------------------------------------------------------------
|
|
104
|
-
|
|
105
|
-
@dataclass
|
|
106
|
-
class MetricsSnapshot:
|
|
107
|
-
"""Mirror of llmcache_metrics columns — names MUST match exactly.
|
|
108
|
-
|
|
109
|
-
S-03 / M-03 note: compress_bytes_original and compress_bytes_after store
|
|
110
|
-
WORD-COUNT proxy values (len(text.split())), NOT byte counts. The column
|
|
111
|
-
names use "bytes" for DB schema backward compatibility. Treat these fields
|
|
112
|
-
as token-count proxies, not literal byte measurements.
|
|
113
|
-
"""
|
|
114
|
-
id: int = 1
|
|
115
|
-
hits: int = 0
|
|
116
|
-
misses: int = 0
|
|
117
|
-
calls_skipped: int = 0
|
|
118
|
-
tokens_saved_input: int = 0
|
|
119
|
-
tokens_saved_output: int = 0
|
|
120
|
-
tokens_saved_compress: int = 0
|
|
121
|
-
evictions: int = 0
|
|
122
|
-
latency_overhead_ms_sum: float = 0.0
|
|
123
|
-
latency_samples: int = 0
|
|
124
|
-
compress_runs: int = 0
|
|
125
|
-
compress_bytes_original: int = 0 # unit: word-count proxy (see S-03 note above)
|
|
126
|
-
compress_bytes_after: int = 0 # unit: word-count proxy (see S-03 note above)
|
|
127
|
-
cache_size_bytes: int = 0
|
|
128
|
-
cache_entry_count: int = 0
|
|
129
|
-
updated_at: float = 0.0
|
|
130
|
-
|
|
131
|
-
@property
|
|
132
|
-
def hit_rate(self) -> float:
|
|
133
|
-
total = self.hits + self.misses
|
|
134
|
-
return self.hits / total if total > 0 else 0.0
|
|
135
|
-
|
|
136
|
-
@property
|
|
137
|
-
def avg_latency_overhead_ms(self) -> float:
|
|
138
|
-
return (
|
|
139
|
-
self.latency_overhead_ms_sum / self.latency_samples
|
|
140
|
-
if self.latency_samples > 0 else 0.0
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
@property
|
|
144
|
-
def compression_ratio(self) -> float:
|
|
145
|
-
if self.compress_bytes_original == 0:
|
|
146
|
-
return 1.0
|
|
147
|
-
return self.compress_bytes_after / self.compress_bytes_original
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
# ---------------------------------------------------------------------------
|
|
151
|
-
# Supporting dataclasses
|
|
152
|
-
# ---------------------------------------------------------------------------
|
|
153
|
-
|
|
154
|
-
@dataclass
|
|
155
|
-
class CacheRow:
|
|
156
|
-
"""Return type of CacheDB.get() and get_by_id().
|
|
157
|
-
|
|
158
|
-
All fields are optional via default so that SELECT * on a table with
|
|
159
|
-
a different schema version does not blow up the constructor. The
|
|
160
|
-
caller is expected to read attributes they need; missing fields
|
|
161
|
-
return the default value.
|
|
162
|
-
"""
|
|
163
|
-
entry_id: str = ""
|
|
164
|
-
cache_key: str = ""
|
|
165
|
-
tenant_id: str = "default"
|
|
166
|
-
model: str = ""
|
|
167
|
-
provider: str = ""
|
|
168
|
-
value: bytes = b""
|
|
169
|
-
created_at: str = ""
|
|
170
|
-
last_hit_at: str | None = None
|
|
171
|
-
ttl_expires: float | None = None
|
|
172
|
-
hit_count: int = 0
|
|
173
|
-
byte_size: int = 0
|
|
174
|
-
tag_json: str = "[]"
|
|
175
|
-
cache_tier: str = "exact"
|
|
176
|
-
compressed: int = 1
|
|
177
|
-
tags: list[str] = field(default_factory=list)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
@dataclass
|
|
181
|
-
class BoundaryRow:
|
|
182
|
-
"""Per-item vCache MLE boundary record."""
|
|
183
|
-
entry_id: str
|
|
184
|
-
logistic_t: float = 0.95
|
|
185
|
-
logistic_gamma: float = 10.0
|
|
186
|
-
sample_count: int = 0
|
|
187
|
-
updated_at: float = 0.0
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
# ---------------------------------------------------------------------------
|
|
191
|
-
# CacheDB
|
|
192
|
-
# ---------------------------------------------------------------------------
|
|
193
|
-
|
|
194
|
-
class CacheDB:
|
|
195
|
-
"""SQLite cache store backed by DatabaseManager. Canonical API per INTERFACE-CONTRACT §1."""
|
|
196
|
-
|
|
197
|
-
_default_instance: "CacheDB | None" = None
|
|
198
|
-
_default_lock: Any = None # lazy-import threading.Lock on first use
|
|
199
|
-
|
|
200
|
-
def __init__(self, db_path: Path | None = None) -> None:
|
|
201
|
-
if db_path is None:
|
|
202
|
-
db_path = state_path(LLMCACHE_DBNAME)
|
|
203
|
-
self._db_path = Path(db_path)
|
|
204
|
-
self._db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
205
|
-
# If the file exists but is not a valid SQLite database (e.g. user
|
|
206
|
-
# accidentally pointed us at a binary blob), delete it and start fresh
|
|
207
|
-
# so that the proxy never gets wedged on init. The proxy is the
|
|
208
|
-
# critical path — we must be able to start even if the cache file
|
|
209
|
-
# is corrupted. The previous file is left in a `.corrupt` sidecar
|
|
210
|
-
# for forensic inspection.
|
|
211
|
-
if self._db_path.exists():
|
|
212
|
-
try:
|
|
213
|
-
import sqlite3 as _sq
|
|
214
|
-
test_conn = _sq.connect(str(self._db_path))
|
|
215
|
-
try:
|
|
216
|
-
test_conn.execute("PRAGMA schema_version")
|
|
217
|
-
finally:
|
|
218
|
-
# Windows will not rename an open SQLite file. Always
|
|
219
|
-
# release the probe before corrupt-file recovery runs.
|
|
220
|
-
test_conn.close()
|
|
221
|
-
except Exception as exc:
|
|
222
|
-
corrupt_sidecar = self._db_path.with_suffix(
|
|
223
|
-
self._db_path.suffix + ".corrupt"
|
|
224
|
-
)
|
|
225
|
-
try:
|
|
226
|
-
if corrupt_sidecar.exists():
|
|
227
|
-
corrupt_sidecar.unlink()
|
|
228
|
-
self._db_path.replace(corrupt_sidecar)
|
|
229
|
-
try:
|
|
230
|
-
os.chmod(corrupt_sidecar, 0o600)
|
|
231
|
-
except OSError:
|
|
232
|
-
pass
|
|
233
|
-
logger.error(
|
|
234
|
-
"CacheDB: %s was not a SQLite file (%s) — moved to %s. "
|
|
235
|
-
"Starting with a fresh cache.",
|
|
236
|
-
self._db_path, exc, corrupt_sidecar,
|
|
237
|
-
)
|
|
238
|
-
except OSError as move_exc:
|
|
239
|
-
logger.error(
|
|
240
|
-
"CacheDB: %s is not a SQLite file and could not be moved: %s",
|
|
241
|
-
self._db_path, move_exc,
|
|
242
|
-
)
|
|
243
|
-
raise
|
|
244
|
-
self._db = DatabaseManager(self._db_path)
|
|
245
|
-
self._db.initialize(_schema)
|
|
246
|
-
# chmod 600 (SEC-C-01)
|
|
247
|
-
try:
|
|
248
|
-
os.chmod(self._db_path, 0o600)
|
|
249
|
-
except OSError as exc:
|
|
250
|
-
logger.warning("CacheDB: could not chmod 600 on %s: %s", self._db_path, exc)
|
|
251
|
-
self._salt = self._load_or_create_salt()
|
|
252
|
-
self._aes_key = self._get_or_persist_aes_key(self._salt)
|
|
253
|
-
self.assert_no_memory_db_tables()
|
|
254
|
-
|
|
255
|
-
# ---- context manager ----
|
|
256
|
-
def __enter__(self) -> "CacheDB":
|
|
257
|
-
return self
|
|
258
|
-
|
|
259
|
-
def __exit__(self, *args: Any) -> None:
|
|
260
|
-
self.close()
|
|
261
|
-
|
|
262
|
-
def close(self) -> None:
|
|
263
|
-
"""No-op (per-call connection model)."""
|
|
264
|
-
self._db.close()
|
|
265
|
-
|
|
266
|
-
@property
|
|
267
|
-
def db_path(self) -> str:
|
|
268
|
-
return str(self._db_path)
|
|
269
|
-
|
|
270
|
-
# ---- encryption helpers ----
|
|
271
|
-
|
|
272
|
-
def _load_or_create_salt(self) -> bytes:
|
|
273
|
-
rows = self._db.execute(
|
|
274
|
-
"SELECT description FROM llmcache_schema_version "
|
|
275
|
-
"WHERE description LIKE 'salt:%' LIMIT 1"
|
|
276
|
-
)
|
|
277
|
-
if rows:
|
|
278
|
-
hex_salt = dict(rows[0])["description"][len(SALT_PREFIX):]
|
|
279
|
-
if len(hex_salt) != 64:
|
|
280
|
-
raise RuntimeError(
|
|
281
|
-
f"llmcache.db: salt row is malformed (len={len(hex_salt)}) — DB may be corrupted."
|
|
282
|
-
)
|
|
283
|
-
return bytes.fromhex(hex_salt)
|
|
284
|
-
salt = os.urandom(32)
|
|
285
|
-
self._db.execute(
|
|
286
|
-
"INSERT INTO llmcache_schema_version (version, description) VALUES (?, ?)",
|
|
287
|
-
(1, f"{SALT_PREFIX}{salt.hex()}"),
|
|
288
|
-
)
|
|
289
|
-
return salt
|
|
290
|
-
|
|
291
|
-
def _get_machine_id(self) -> str:
|
|
292
|
-
"""Return a stable machine identifier for AES key derivation."""
|
|
293
|
-
system = platform.system()
|
|
294
|
-
mid: str | None = None
|
|
295
|
-
if system == "Darwin":
|
|
296
|
-
try:
|
|
297
|
-
import subprocess
|
|
298
|
-
out = subprocess.run(
|
|
299
|
-
["ioreg", "-rd1", "-c", "IOPlatformExpertDevice"],
|
|
300
|
-
capture_output=True, timeout=2,
|
|
301
|
-
)
|
|
302
|
-
if out.returncode == 0:
|
|
303
|
-
text = out.stdout.decode("utf-8", errors="ignore")
|
|
304
|
-
for line in text.splitlines():
|
|
305
|
-
if "IOPlatformUUID" in line and "=" in line:
|
|
306
|
-
mid = line.split("=", 1)[1].strip().strip('"')
|
|
307
|
-
if mid:
|
|
308
|
-
break
|
|
309
|
-
except Exception:
|
|
310
|
-
mid = None
|
|
311
|
-
elif system == "Linux":
|
|
312
|
-
try:
|
|
313
|
-
mid = Path("/etc/machine-id").read_text(encoding="utf-8").strip()
|
|
314
|
-
except OSError:
|
|
315
|
-
mid = None
|
|
316
|
-
if not mid:
|
|
317
|
-
mid_file = state_path(MID_FILENAME)
|
|
318
|
-
if mid_file.exists():
|
|
319
|
-
try:
|
|
320
|
-
mid = mid_file.read_text(encoding="utf-8").strip()
|
|
321
|
-
except OSError:
|
|
322
|
-
mid = None
|
|
323
|
-
if not mid:
|
|
324
|
-
mid = uuid.uuid4().hex
|
|
325
|
-
try:
|
|
326
|
-
mid_file.parent.mkdir(parents=True, exist_ok=True)
|
|
327
|
-
mid_file.write_text(mid, encoding="utf-8")
|
|
328
|
-
os.chmod(mid_file, 0o600)
|
|
329
|
-
except OSError as exc:
|
|
330
|
-
logger.warning("CacheDB: could not persist machine id: %s", exc)
|
|
331
|
-
return mid
|
|
332
|
-
|
|
333
|
-
def _get_or_persist_aes_key(self, salt: bytes) -> bytes:
|
|
334
|
-
"""C-06: Load persisted key from disk, or derive + persist on first run.
|
|
335
|
-
|
|
336
|
-
Surviving a machine-id change: after first derivation the key is saved to
|
|
337
|
-
opt-key.bin (0600). On subsequent starts the file is read directly,
|
|
338
|
-
so changing the underlying machine-id string cannot invalidate existing
|
|
339
|
-
cache entries.
|
|
340
|
-
"""
|
|
341
|
-
key_file = Path(_KEY_FILE)
|
|
342
|
-
try:
|
|
343
|
-
if key_file.exists():
|
|
344
|
-
key = key_file.read_bytes()
|
|
345
|
-
if len(key) == 32:
|
|
346
|
-
return key
|
|
347
|
-
except Exception as exc:
|
|
348
|
-
logger.warning("CacheDB: could not read persisted AES key: %s", exc)
|
|
349
|
-
|
|
350
|
-
# First run (or corrupted file): derive from machine-id and persist.
|
|
351
|
-
machine_id = self._get_machine_id()
|
|
352
|
-
key = self._derive_aes_key(machine_id, salt)
|
|
353
|
-
try:
|
|
354
|
-
key_file.parent.mkdir(parents=True, exist_ok=True)
|
|
355
|
-
key_file.write_bytes(key)
|
|
356
|
-
os.chmod(key_file, 0o600)
|
|
357
|
-
except Exception as exc:
|
|
358
|
-
logger.warning("CacheDB: could not persist AES key (fail-open): %s", exc)
|
|
359
|
-
return key
|
|
360
|
-
|
|
361
|
-
def _derive_aes_key(self, machine_id: str, salt: bytes) -> bytes:
|
|
362
|
-
kdf = PBKDF2HMAC(
|
|
363
|
-
algorithm=hashes.SHA256(),
|
|
364
|
-
length=32,
|
|
365
|
-
salt=salt,
|
|
366
|
-
iterations=_PBKDF2_ITERATIONS,
|
|
367
|
-
)
|
|
368
|
-
return kdf.derive(machine_id.encode("utf-8"))
|
|
369
|
-
|
|
370
|
-
def _encrypt(self, plaintext: bytes) -> bytes:
|
|
371
|
-
nonce = os.urandom(_AES_NONCE_BYTES)
|
|
372
|
-
aesgcm = AESGCM(self._aes_key)
|
|
373
|
-
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data=None)
|
|
374
|
-
return nonce + ciphertext
|
|
375
|
-
|
|
376
|
-
def _decrypt(self, blob: bytes) -> bytes:
|
|
377
|
-
if len(blob) <= _AES_NONCE_BYTES:
|
|
378
|
-
raise ValueError("Encrypted blob too short")
|
|
379
|
-
nonce = blob[:_AES_NONCE_BYTES]
|
|
380
|
-
ciphertext = blob[_AES_NONCE_BYTES:]
|
|
381
|
-
aesgcm = AESGCM(self._aes_key)
|
|
382
|
-
# v3.6.12 (cache-1): AES-GCM raises cryptography.exceptions.InvalidTag
|
|
383
|
-
# (NOT a ValueError subclass) on a tampered/wrong-key blob. Every caller
|
|
384
|
-
# catches ValueError to fail-open; convert InvalidTag -> ValueError here
|
|
385
|
-
# at the single chokepoint so a corrupt/rotated-key cache entry degrades
|
|
386
|
-
# to a miss instead of raising out of get()/get_value()/ccr_get().
|
|
387
|
-
try:
|
|
388
|
-
return aesgcm.decrypt(nonce, ciphertext, associated_data=None)
|
|
389
|
-
except InvalidTag as exc:
|
|
390
|
-
raise ValueError(f"AES-GCM authentication failed: {exc}") from exc
|
|
391
|
-
|
|
392
|
-
# ---- assertion ----
|
|
393
|
-
|
|
394
|
-
def assert_no_memory_db_tables(self) -> None:
|
|
395
|
-
"""Raise RuntimeError if memory.db tables are present."""
|
|
396
|
-
rows = self._db.execute(
|
|
397
|
-
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
398
|
-
)
|
|
399
|
-
names = {dict(r)["name"] for r in rows}
|
|
400
|
-
found = _FORBIDDEN_MEMORY_TABLES & names
|
|
401
|
-
if found:
|
|
402
|
-
raise RuntimeError(
|
|
403
|
-
f"ISOLATION VIOLATION: llmcache.db contains memory.db tables: {sorted(found)}. "
|
|
404
|
-
"Wrong DB file opened. Aborting."
|
|
405
|
-
)
|
|
406
|
-
|
|
407
|
-
# ---- exact CRUD ----
|
|
408
|
-
|
|
409
|
-
def get(self, key: str, tenant_id: str) -> CacheRow | None:
|
|
410
|
-
try:
|
|
411
|
-
rows = self._db.execute(
|
|
412
|
-
"SELECT * FROM llmcache_entries "
|
|
413
|
-
"WHERE cache_key = ? AND tenant_id = ? "
|
|
414
|
-
"AND (ttl_expires IS NULL OR ttl_expires > ?) "
|
|
415
|
-
"LIMIT 1",
|
|
416
|
-
(key, tenant_id, time.time()),
|
|
417
|
-
)
|
|
418
|
-
if not rows:
|
|
419
|
-
return None
|
|
420
|
-
row = dict(rows[0])
|
|
421
|
-
# increment hit stats
|
|
422
|
-
self._db.execute(
|
|
423
|
-
"UPDATE llmcache_entries SET hit_count = hit_count + 1, "
|
|
424
|
-
"last_hit_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') "
|
|
425
|
-
"WHERE cache_key = ? AND tenant_id = ?",
|
|
426
|
-
(key, tenant_id),
|
|
427
|
-
)
|
|
428
|
-
row["hit_count"] = row["hit_count"] + 1
|
|
429
|
-
# decrypt + decompress value
|
|
430
|
-
try:
|
|
431
|
-
plaintext = self._decrypt(row["value_blob"])
|
|
432
|
-
if row.get("compressed", 0):
|
|
433
|
-
plaintext = zlib.decompress(plaintext)
|
|
434
|
-
row["value"] = plaintext
|
|
435
|
-
except (ValueError, zlib.error) as exc:
|
|
436
|
-
logger.warning("CacheDB.get decrypt failed (cache miss): %s", exc)
|
|
437
|
-
return None
|
|
438
|
-
return self._row_to_cacherow(row)
|
|
439
|
-
except sqlite3.Error as exc:
|
|
440
|
-
logger.warning("CacheDB.get failed (cache miss): %s", exc)
|
|
441
|
-
return None
|
|
442
|
-
|
|
443
|
-
def get_value(self, cache_key: str, tenant_id: str) -> bytes | None:
|
|
444
|
-
"""Pure value lookup — no hit_count increment (unlike get()).
|
|
445
|
-
|
|
446
|
-
Used by MCP KV tools which manage their own hit/miss counters.
|
|
447
|
-
Caller must have already normalized tenant_id. Fail-open: returns None on error.
|
|
448
|
-
"""
|
|
449
|
-
try:
|
|
450
|
-
rows = self._db.execute(
|
|
451
|
-
"SELECT value_blob, compressed FROM llmcache_entries "
|
|
452
|
-
"WHERE cache_key = ? AND tenant_id = ? "
|
|
453
|
-
"AND (ttl_expires IS NULL OR ttl_expires > ?) LIMIT 1",
|
|
454
|
-
(cache_key, tenant_id, time.time()),
|
|
455
|
-
)
|
|
456
|
-
if not rows:
|
|
457
|
-
return None
|
|
458
|
-
row = dict(rows[0])
|
|
459
|
-
plaintext = self._decrypt(row["value_blob"])
|
|
460
|
-
if row.get("compressed", 0):
|
|
461
|
-
plaintext = zlib.decompress(plaintext)
|
|
462
|
-
return plaintext
|
|
463
|
-
except (sqlite3.Error, ValueError, zlib.error) as exc:
|
|
464
|
-
logger.warning("CacheDB.get_value failed (fail-open): %s", exc)
|
|
465
|
-
return None
|
|
466
|
-
|
|
467
|
-
def set(
|
|
468
|
-
self,
|
|
469
|
-
key: str,
|
|
470
|
-
tenant_id: str,
|
|
471
|
-
value: bytes,
|
|
472
|
-
*,
|
|
473
|
-
model: str,
|
|
474
|
-
ttl_expires: float | None,
|
|
475
|
-
tags: list[str],
|
|
476
|
-
) -> None:
|
|
477
|
-
try:
|
|
478
|
-
# 1) zlib-compress
|
|
479
|
-
compressed_blob = zlib.compress(value, level=_ZLIB_LEVEL)
|
|
480
|
-
# 2) AES-256-GCM encrypt
|
|
481
|
-
encrypted_blob = self._encrypt(compressed_blob)
|
|
482
|
-
entry_id = uuid.uuid4().hex
|
|
483
|
-
self._db.execute(
|
|
484
|
-
"INSERT INTO llmcache_entries "
|
|
485
|
-
"(entry_id, cache_key, tenant_id, model, provider, value_blob, compressed, "
|
|
486
|
-
" ttl_expires, tag_json, byte_size) "
|
|
487
|
-
"VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, ?) "
|
|
488
|
-
"ON CONFLICT(cache_key, tenant_id) DO UPDATE SET "
|
|
489
|
-
" value_blob=excluded.value_blob, model=excluded.model, provider=excluded.provider, "
|
|
490
|
-
" ttl_expires=excluded.ttl_expires, tag_json=excluded.tag_json, "
|
|
491
|
-
" byte_size=excluded.byte_size, hit_count=0, last_hit_at=NULL, "
|
|
492
|
-
" created_at=strftime('%Y-%m-%dT%H:%M:%fZ','now')",
|
|
493
|
-
(
|
|
494
|
-
entry_id, key, tenant_id, model, "",
|
|
495
|
-
encrypted_blob, ttl_expires,
|
|
496
|
-
json.dumps(tags or []), len(encrypted_blob),
|
|
497
|
-
),
|
|
498
|
-
)
|
|
499
|
-
except sqlite3.Error as exc:
|
|
500
|
-
logger.warning("CacheDB.set failed (fail-open): %s", exc)
|
|
501
|
-
|
|
502
|
-
def set_with_entry_id(
|
|
503
|
-
self,
|
|
504
|
-
key: str,
|
|
505
|
-
tenant_id: str,
|
|
506
|
-
value: bytes,
|
|
507
|
-
entry_id: str,
|
|
508
|
-
ttl_seconds: int | None = None,
|
|
509
|
-
tags: list[str] | None = None,
|
|
510
|
-
) -> None:
|
|
511
|
-
"""Like set(), but writes a caller-supplied entry_id instead of auto-generating one.
|
|
512
|
-
|
|
513
|
-
Used by semantic layer (index_entry) which pre-computes the entry_id from
|
|
514
|
-
the embedding vector to enable O(1) semantic-to-cache cross-reference.
|
|
515
|
-
"""
|
|
516
|
-
try:
|
|
517
|
-
compressed_blob = zlib.compress(value, level=_ZLIB_LEVEL)
|
|
518
|
-
encrypted_blob = self._encrypt(compressed_blob)
|
|
519
|
-
ttl_expires = (time.time() + ttl_seconds) if ttl_seconds else None
|
|
520
|
-
self._db.execute(
|
|
521
|
-
"INSERT INTO llmcache_entries "
|
|
522
|
-
"(entry_id, cache_key, tenant_id, model, provider, value_blob, compressed, "
|
|
523
|
-
" ttl_expires, tag_json, byte_size) "
|
|
524
|
-
"VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, ?) "
|
|
525
|
-
"ON CONFLICT(cache_key, tenant_id) DO UPDATE SET "
|
|
526
|
-
" entry_id=excluded.entry_id, value_blob=excluded.value_blob, "
|
|
527
|
-
" model=excluded.model, provider=excluded.provider, "
|
|
528
|
-
" ttl_expires=excluded.ttl_expires, tag_json=excluded.tag_json, "
|
|
529
|
-
" byte_size=excluded.byte_size, hit_count=0, last_hit_at=NULL, "
|
|
530
|
-
" created_at=strftime('%Y-%m-%dT%H:%M:%fZ','now')",
|
|
531
|
-
(
|
|
532
|
-
entry_id, key, tenant_id, "", "",
|
|
533
|
-
encrypted_blob, ttl_expires,
|
|
534
|
-
json.dumps(tags or []), len(encrypted_blob),
|
|
535
|
-
),
|
|
536
|
-
)
|
|
537
|
-
except sqlite3.Error as exc:
|
|
538
|
-
logger.warning("CacheDB.set_with_entry_id failed (fail-open): %s", exc)
|
|
539
|
-
|
|
540
|
-
def get_by_id(self, entry_id: str) -> CacheRow | None:
|
|
541
|
-
try:
|
|
542
|
-
rows = self._db.execute(
|
|
543
|
-
"SELECT * FROM llmcache_entries WHERE entry_id = ? LIMIT 1",
|
|
544
|
-
(entry_id,),
|
|
545
|
-
)
|
|
546
|
-
if not rows:
|
|
547
|
-
return None
|
|
548
|
-
row = dict(rows[0])
|
|
549
|
-
try:
|
|
550
|
-
plaintext = self._decrypt(row["value_blob"])
|
|
551
|
-
if row.get("compressed", 0):
|
|
552
|
-
plaintext = zlib.decompress(plaintext)
|
|
553
|
-
row["value"] = plaintext
|
|
554
|
-
except (ValueError, zlib.error) as exc:
|
|
555
|
-
logger.warning("CacheDB.get_by_id decrypt failed: %s", exc)
|
|
556
|
-
return None
|
|
557
|
-
return self._row_to_cacherow(row)
|
|
558
|
-
except sqlite3.Error as exc:
|
|
559
|
-
logger.warning("CacheDB.get_by_id failed: %s", exc)
|
|
560
|
-
return None
|
|
561
|
-
|
|
562
|
-
def delete(self, key: str, tenant_id: str) -> None:
|
|
563
|
-
try:
|
|
564
|
-
with self._db.transaction():
|
|
565
|
-
rows = self._db.execute(
|
|
566
|
-
"SELECT entry_id FROM llmcache_entries WHERE cache_key = ? AND tenant_id = ?",
|
|
567
|
-
(key, tenant_id),
|
|
568
|
-
)
|
|
569
|
-
entry_ids = [dict(r)["entry_id"] for r in rows]
|
|
570
|
-
if entry_ids:
|
|
571
|
-
placeholders = ",".join("?" for _ in entry_ids)
|
|
572
|
-
self._db.execute(
|
|
573
|
-
f"DELETE FROM llmcache_semantic_vectors "
|
|
574
|
-
f"WHERE entry_id IN ({placeholders})",
|
|
575
|
-
tuple(entry_ids),
|
|
576
|
-
)
|
|
577
|
-
self._db.execute(
|
|
578
|
-
"DELETE FROM llmcache_tags WHERE cache_key = ? AND tenant_id = ?",
|
|
579
|
-
(key, tenant_id),
|
|
580
|
-
)
|
|
581
|
-
self._db.execute(
|
|
582
|
-
"DELETE FROM llmcache_entries WHERE cache_key = ? AND tenant_id = ?",
|
|
583
|
-
(key, tenant_id),
|
|
584
|
-
)
|
|
585
|
-
except sqlite3.Error as exc:
|
|
586
|
-
logger.warning("CacheDB.delete failed (fail-open): %s", exc)
|
|
587
|
-
|
|
588
|
-
def delete_batch(self, keys: list[tuple[str, str]]) -> int:
|
|
589
|
-
if not keys:
|
|
590
|
-
return 0
|
|
591
|
-
try:
|
|
592
|
-
deleted = 0
|
|
593
|
-
with self._db.transaction():
|
|
594
|
-
for key, tenant_id in keys:
|
|
595
|
-
rows = self._db.execute(
|
|
596
|
-
"SELECT entry_id FROM llmcache_entries WHERE cache_key = ? AND tenant_id = ?",
|
|
597
|
-
(key, tenant_id),
|
|
598
|
-
)
|
|
599
|
-
entry_ids = [dict(r)["entry_id"] for r in rows]
|
|
600
|
-
if entry_ids:
|
|
601
|
-
placeholders = ",".join("?" for _ in entry_ids)
|
|
602
|
-
self._db.execute(
|
|
603
|
-
f"DELETE FROM llmcache_semantic_vectors "
|
|
604
|
-
f"WHERE entry_id IN ({placeholders})",
|
|
605
|
-
tuple(entry_ids),
|
|
606
|
-
)
|
|
607
|
-
self._db.execute(
|
|
608
|
-
"DELETE FROM llmcache_tags WHERE cache_key = ? AND tenant_id = ?",
|
|
609
|
-
(key, tenant_id),
|
|
610
|
-
)
|
|
611
|
-
self._db.execute(
|
|
612
|
-
"DELETE FROM llmcache_entries WHERE cache_key = ? AND tenant_id = ?",
|
|
613
|
-
(key, tenant_id),
|
|
614
|
-
)
|
|
615
|
-
deleted += 1
|
|
616
|
-
return deleted
|
|
617
|
-
except sqlite3.Error as exc:
|
|
618
|
-
logger.warning("CacheDB.delete_batch failed: %s", exc)
|
|
619
|
-
return 0
|
|
620
|
-
|
|
621
|
-
def sweep_expired(self, now: float) -> int:
|
|
622
|
-
try:
|
|
623
|
-
with self._db.transaction():
|
|
624
|
-
rows = self._db.execute(
|
|
625
|
-
"SELECT entry_id FROM llmcache_entries "
|
|
626
|
-
"WHERE ttl_expires IS NOT NULL AND ttl_expires < ?",
|
|
627
|
-
(now,),
|
|
628
|
-
)
|
|
629
|
-
entry_ids = [dict(r)["entry_id"] for r in rows]
|
|
630
|
-
entries_deleted = len(entry_ids)
|
|
631
|
-
if entry_ids:
|
|
632
|
-
placeholders = ",".join("?" for _ in entry_ids)
|
|
633
|
-
self._db.execute(
|
|
634
|
-
f"DELETE FROM llmcache_semantic_vectors "
|
|
635
|
-
f"WHERE entry_id IN ({placeholders})",
|
|
636
|
-
tuple(entry_ids),
|
|
637
|
-
)
|
|
638
|
-
self._db.execute(
|
|
639
|
-
f"DELETE FROM llmcache_tags "
|
|
640
|
-
f"WHERE cache_key IN ("
|
|
641
|
-
f" SELECT cache_key FROM llmcache_entries WHERE entry_id IN ({placeholders})"
|
|
642
|
-
f")",
|
|
643
|
-
tuple(entry_ids),
|
|
644
|
-
)
|
|
645
|
-
self._db.execute(
|
|
646
|
-
"DELETE FROM llmcache_entries "
|
|
647
|
-
"WHERE ttl_expires IS NOT NULL AND ttl_expires < ?",
|
|
648
|
-
(now,),
|
|
649
|
-
)
|
|
650
|
-
ccr_row = self._db.execute(
|
|
651
|
-
"SELECT COUNT(*) AS n FROM llmcache_ccr_originals "
|
|
652
|
-
"WHERE ttl_expires IS NOT NULL AND ttl_expires < ?",
|
|
653
|
-
(now,),
|
|
654
|
-
)
|
|
655
|
-
ccr_count = int(dict(ccr_row[0])["n"]) if ccr_row else 0
|
|
656
|
-
self._db.execute(
|
|
657
|
-
"DELETE FROM llmcache_ccr_originals "
|
|
658
|
-
"WHERE ttl_expires IS NOT NULL AND ttl_expires < ?",
|
|
659
|
-
(now,),
|
|
660
|
-
)
|
|
661
|
-
return entries_deleted + ccr_count
|
|
662
|
-
except sqlite3.Error as exc:
|
|
663
|
-
logger.warning("CacheDB.sweep_expired failed: %s", exc)
|
|
664
|
-
return 0
|
|
665
|
-
|
|
666
|
-
# ---- tags ----
|
|
667
|
-
|
|
668
|
-
def tag_register(self, key: str, tenant_id: str, tags: list[str]) -> None:
|
|
669
|
-
if not tags:
|
|
670
|
-
return
|
|
671
|
-
try:
|
|
672
|
-
with self._db.transaction():
|
|
673
|
-
for tag in tags:
|
|
674
|
-
self._db.execute(
|
|
675
|
-
"INSERT OR IGNORE INTO llmcache_tags (tag, cache_key, tenant_id) "
|
|
676
|
-
"VALUES (?, ?, ?)",
|
|
677
|
-
(tag, key, tenant_id),
|
|
678
|
-
)
|
|
679
|
-
except sqlite3.Error as exc:
|
|
680
|
-
logger.warning("CacheDB.tag_register failed: %s", exc)
|
|
681
|
-
|
|
682
|
-
def tag_keys(self, tag: str) -> list[tuple[str, str]]:
|
|
683
|
-
try:
|
|
684
|
-
rows = self._db.execute(
|
|
685
|
-
"SELECT cache_key, tenant_id FROM llmcache_tags WHERE tag = ?",
|
|
686
|
-
(tag,),
|
|
687
|
-
)
|
|
688
|
-
return [(dict(r)["cache_key"], dict(r)["tenant_id"]) for r in rows]
|
|
689
|
-
except sqlite3.Error as exc:
|
|
690
|
-
logger.warning("CacheDB.tag_keys failed: %s", exc)
|
|
691
|
-
return []
|
|
692
|
-
|
|
693
|
-
def invalidate_by_tag(self, tag: str) -> int:
|
|
694
|
-
try:
|
|
695
|
-
with self._db.transaction():
|
|
696
|
-
rows = self._db.execute(
|
|
697
|
-
"SELECT DISTINCT e.entry_id FROM llmcache_entries e "
|
|
698
|
-
"JOIN llmcache_tags t ON t.cache_key = e.cache_key AND t.tenant_id = e.tenant_id "
|
|
699
|
-
"WHERE t.tag = ?",
|
|
700
|
-
(tag,),
|
|
701
|
-
)
|
|
702
|
-
entry_ids = [dict(r)["entry_id"] for r in rows]
|
|
703
|
-
if not entry_ids:
|
|
704
|
-
self._db.execute("DELETE FROM llmcache_tags WHERE tag = ?", (tag,))
|
|
705
|
-
return 0
|
|
706
|
-
placeholders = ",".join("?" for _ in entry_ids)
|
|
707
|
-
self._db.execute(
|
|
708
|
-
f"DELETE FROM llmcache_semantic_vectors WHERE entry_id IN ({placeholders})",
|
|
709
|
-
tuple(entry_ids),
|
|
710
|
-
)
|
|
711
|
-
self._db.execute(
|
|
712
|
-
f"DELETE FROM llmcache_entries WHERE entry_id IN ({placeholders})",
|
|
713
|
-
tuple(entry_ids),
|
|
714
|
-
)
|
|
715
|
-
self._db.execute("DELETE FROM llmcache_tags WHERE tag = ?", (tag,))
|
|
716
|
-
return len(entry_ids)
|
|
717
|
-
except sqlite3.Error as exc:
|
|
718
|
-
logger.warning("CacheDB.invalidate_by_tag failed: %s", exc)
|
|
719
|
-
return 0
|
|
720
|
-
|
|
721
|
-
# ---- semantic vectors ----
|
|
722
|
-
|
|
723
|
-
def vec_add(self, entry_id: str, tenant_id: str, vector: bytes, meta: dict) -> None:
|
|
724
|
-
try:
|
|
725
|
-
dim = int(meta.get("dim", len(vector) // 4))
|
|
726
|
-
model_name = str(meta.get("model", "nomic-ai/nomic-embed-text-v1.5"))
|
|
727
|
-
context_fp = str(meta.get("context_fp", "")) # C-10: persist context fingerprint
|
|
728
|
-
self._db.execute(
|
|
729
|
-
"INSERT OR REPLACE INTO llmcache_semantic_vectors "
|
|
730
|
-
"(entry_id, tenant_id, vector_blob, vector_dim, model_name, context_fp) "
|
|
731
|
-
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
732
|
-
(entry_id, tenant_id, vector, dim, model_name, context_fp),
|
|
733
|
-
)
|
|
734
|
-
except sqlite3.Error as exc:
|
|
735
|
-
logger.warning("CacheDB.vec_add failed: %s", exc)
|
|
736
|
-
|
|
737
|
-
def vec_search(self, tenant_id: str, vector: bytes, top_k: int) -> list[tuple[str, float]]:
|
|
738
|
-
try:
|
|
739
|
-
rows = self._db.execute(
|
|
740
|
-
"SELECT entry_id, vector_blob FROM llmcache_semantic_vectors "
|
|
741
|
-
"WHERE tenant_id = ?",
|
|
742
|
-
(tenant_id,),
|
|
743
|
-
)
|
|
744
|
-
try:
|
|
745
|
-
import numpy as _np
|
|
746
|
-
except ImportError:
|
|
747
|
-
return []
|
|
748
|
-
q = _np.frombuffer(vector, dtype=_np.float32)
|
|
749
|
-
q_norm = float(_np.linalg.norm(q))
|
|
750
|
-
if q_norm == 0:
|
|
751
|
-
return []
|
|
752
|
-
scored: list[tuple[str, float]] = []
|
|
753
|
-
for r in rows:
|
|
754
|
-
rd = dict(r)
|
|
755
|
-
v = _np.frombuffer(rd["vector_blob"], dtype=_np.float32)
|
|
756
|
-
v_norm = float(_np.linalg.norm(v))
|
|
757
|
-
if v_norm == 0:
|
|
758
|
-
continue
|
|
759
|
-
cos = float(_np.dot(q, v) / (q_norm * v_norm))
|
|
760
|
-
scored.append((rd["entry_id"], cos))
|
|
761
|
-
scored.sort(key=lambda t: t[1], reverse=True)
|
|
762
|
-
return scored[: max(0, top_k)]
|
|
763
|
-
except sqlite3.Error as exc:
|
|
764
|
-
logger.warning("CacheDB.vec_search failed: %s", exc)
|
|
765
|
-
return []
|
|
766
|
-
|
|
767
|
-
def vec_delete(self, entry_id: str) -> None:
|
|
768
|
-
try:
|
|
769
|
-
self._db.execute(
|
|
770
|
-
"DELETE FROM llmcache_semantic_vectors WHERE entry_id = ?",
|
|
771
|
-
(entry_id,),
|
|
772
|
-
)
|
|
773
|
-
except sqlite3.Error as exc:
|
|
774
|
-
logger.warning("CacheDB.vec_delete failed: %s", exc)
|
|
775
|
-
|
|
776
|
-
# ---- vCache boundary + centroids (Phase 3 hooks, stub-implemented) ----
|
|
777
|
-
|
|
778
|
-
def boundary_get(self, entry_id: str) -> BoundaryRow | None:
|
|
779
|
-
try:
|
|
780
|
-
rows = self._db.execute(
|
|
781
|
-
"SELECT * FROM llmcache_boundaries WHERE entry_id = ? LIMIT 1",
|
|
782
|
-
(entry_id,),
|
|
783
|
-
)
|
|
784
|
-
if not rows:
|
|
785
|
-
return None
|
|
786
|
-
d = dict(rows[0])
|
|
787
|
-
return BoundaryRow(**d)
|
|
788
|
-
except sqlite3.Error as exc:
|
|
789
|
-
logger.warning("CacheDB.boundary_get failed: %s", exc)
|
|
790
|
-
return None
|
|
791
|
-
|
|
792
|
-
def boundary_upsert(self, entry_id: str, row: BoundaryRow) -> None:
|
|
793
|
-
try:
|
|
794
|
-
self._db.execute(
|
|
795
|
-
"INSERT INTO llmcache_boundaries "
|
|
796
|
-
"(entry_id, logistic_t, logistic_gamma, sample_count, updated_at) "
|
|
797
|
-
"VALUES (?, ?, ?, ?, ?) "
|
|
798
|
-
"ON CONFLICT(entry_id) DO UPDATE SET "
|
|
799
|
-
" logistic_t=excluded.logistic_t, logistic_gamma=excluded.logistic_gamma, "
|
|
800
|
-
" sample_count=excluded.sample_count, updated_at=excluded.updated_at",
|
|
801
|
-
(
|
|
802
|
-
entry_id, row.logistic_t, row.logistic_gamma,
|
|
803
|
-
row.sample_count, row.updated_at,
|
|
804
|
-
),
|
|
805
|
-
)
|
|
806
|
-
except sqlite3.Error as exc:
|
|
807
|
-
logger.warning("CacheDB.boundary_upsert failed: %s", exc)
|
|
808
|
-
|
|
809
|
-
def centroid_get(self, tenant_id: str) -> bytes | None:
|
|
810
|
-
try:
|
|
811
|
-
rows = self._db.execute(
|
|
812
|
-
"SELECT centroid_blob FROM llmcache_centroids WHERE tenant_id = ? LIMIT 1",
|
|
813
|
-
(tenant_id,),
|
|
814
|
-
)
|
|
815
|
-
if not rows:
|
|
816
|
-
return None
|
|
817
|
-
return dict(rows[0])["centroid_blob"]
|
|
818
|
-
except sqlite3.Error as exc:
|
|
819
|
-
logger.warning("CacheDB.centroid_get failed: %s", exc)
|
|
820
|
-
return None
|
|
821
|
-
|
|
822
|
-
def centroid_update(self, tenant_id: str, centroid: bytes, n: int) -> None:
|
|
823
|
-
try:
|
|
824
|
-
self._db.execute(
|
|
825
|
-
"INSERT INTO llmcache_centroids (tenant_id, centroid_blob, n) "
|
|
826
|
-
"VALUES (?, ?, ?) "
|
|
827
|
-
"ON CONFLICT(tenant_id) DO UPDATE SET "
|
|
828
|
-
" centroid_blob=excluded.centroid_blob, n=excluded.n, "
|
|
829
|
-
" updated_at=strftime('%Y-%m-%dT%H:%M:%fZ','now')",
|
|
830
|
-
(tenant_id, centroid, n),
|
|
831
|
-
)
|
|
832
|
-
except sqlite3.Error as exc:
|
|
833
|
-
logger.warning("CacheDB.centroid_update failed: %s", exc)
|
|
834
|
-
|
|
835
|
-
# ---- v2.2 supplementary boundary methods (LLD-03 Phase 3) ----
|
|
836
|
-
|
|
837
|
-
def get_all_boundaries(self) -> list[dict[str, Any]]:
|
|
838
|
-
"""Return all vCache MLE boundary records (warm-start / rebuild).
|
|
839
|
-
|
|
840
|
-
LLD-03 §4.4: called at VCacheSemantic startup to populate the
|
|
841
|
-
in-memory boundary cache.
|
|
842
|
-
|
|
843
|
-
Returns:
|
|
844
|
-
List of dicts with keys: entry_id, t_hat, gamma_hat,
|
|
845
|
-
sample_count, updated_at. [] on error.
|
|
846
|
-
"""
|
|
847
|
-
try:
|
|
848
|
-
rows = self._db.execute(
|
|
849
|
-
"SELECT entry_id, logistic_t, logistic_gamma, sample_count, updated_at "
|
|
850
|
-
"FROM llmcache_boundaries"
|
|
851
|
-
)
|
|
852
|
-
return [dict(r) for r in rows]
|
|
853
|
-
except sqlite3.Error as exc:
|
|
854
|
-
logger.warning("CacheDB.get_all_boundaries failed: %s", exc)
|
|
855
|
-
return []
|
|
856
|
-
|
|
857
|
-
def delete_boundary(self, entry_id: str) -> None:
|
|
858
|
-
"""Delete a vCache boundary record. Fail-open."""
|
|
859
|
-
try:
|
|
860
|
-
self._db.execute(
|
|
861
|
-
"DELETE FROM llmcache_boundaries WHERE entry_id = ?",
|
|
862
|
-
(entry_id,),
|
|
863
|
-
)
|
|
864
|
-
except sqlite3.Error as exc:
|
|
865
|
-
logger.warning("CacheDB.delete_boundary failed: %s", exc)
|
|
866
|
-
|
|
867
|
-
def get_entry_by_id(self, entry_id: str) -> dict[str, Any] | None:
|
|
868
|
-
"""Fetch a cache entry by entry_id and return its decoded response dict.
|
|
869
|
-
|
|
870
|
-
LLD-03 §4.5 _fetch_response: needed to retrieve the response for a
|
|
871
|
-
semantically matched entry_id. Decrypts + decompresses the value_blob.
|
|
872
|
-
|
|
873
|
-
Returns:
|
|
874
|
-
Response dict (parsed JSON), or None on miss / error.
|
|
875
|
-
"""
|
|
876
|
-
try:
|
|
877
|
-
rows = self._db.execute(
|
|
878
|
-
"SELECT value_blob, compressed FROM llmcache_entries "
|
|
879
|
-
"WHERE entry_id = ? LIMIT 1",
|
|
880
|
-
(entry_id,),
|
|
881
|
-
)
|
|
882
|
-
if not rows:
|
|
883
|
-
return None
|
|
884
|
-
row = dict(rows[0])
|
|
885
|
-
try:
|
|
886
|
-
plaintext = self._decrypt(row["value_blob"])
|
|
887
|
-
if row.get("compressed", 0):
|
|
888
|
-
plaintext = zlib.decompress(plaintext)
|
|
889
|
-
except (ValueError, zlib.error) as exc:
|
|
890
|
-
logger.warning("CacheDB.get_entry_by_id decrypt failed: %s", exc)
|
|
891
|
-
return None
|
|
892
|
-
try:
|
|
893
|
-
import json as _json
|
|
894
|
-
return _json.loads(plaintext.decode("utf-8"))
|
|
895
|
-
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
896
|
-
return {"raw_bytes": plaintext.hex()}
|
|
897
|
-
except sqlite3.Error as exc:
|
|
898
|
-
logger.warning("CacheDB.get_entry_by_id failed: %s", exc)
|
|
899
|
-
return None
|
|
900
|
-
|
|
901
|
-
# ---- CCR originals ----
|
|
902
|
-
|
|
903
|
-
def ccr_put(
|
|
904
|
-
self,
|
|
905
|
-
ccr_id: str,
|
|
906
|
-
original: bytes,
|
|
907
|
-
ttl_expires: float | None = None,
|
|
908
|
-
*,
|
|
909
|
-
tenant_id: str = "default",
|
|
910
|
-
) -> None:
|
|
911
|
-
import hashlib
|
|
912
|
-
try:
|
|
913
|
-
compressed = zlib.compress(original, level=_ZLIB_LEVEL)
|
|
914
|
-
encrypted = self._encrypt(compressed)
|
|
915
|
-
self._db.execute(
|
|
916
|
-
"INSERT OR REPLACE INTO llmcache_ccr_originals "
|
|
917
|
-
"(ccr_id, tenant_id, original_blob, compressed_hash, "
|
|
918
|
-
" byte_size_orig, byte_size_comp, ttl_expires) "
|
|
919
|
-
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
920
|
-
(
|
|
921
|
-
ccr_id, tenant_id, encrypted,
|
|
922
|
-
hashlib.sha256(original).hexdigest(),
|
|
923
|
-
len(original), len(compressed),
|
|
924
|
-
ttl_expires,
|
|
925
|
-
),
|
|
926
|
-
)
|
|
927
|
-
except sqlite3.Error as exc:
|
|
928
|
-
logger.warning("CacheDB.ccr_put failed: %s", exc)
|
|
929
|
-
|
|
930
|
-
def ccr_get(self, ccr_id: str, *, tenant_id: str = "default") -> bytes | None:
|
|
931
|
-
try:
|
|
932
|
-
rows = self._db.execute(
|
|
933
|
-
"SELECT original_blob FROM llmcache_ccr_originals "
|
|
934
|
-
"WHERE ccr_id = ? "
|
|
935
|
-
"AND tenant_id = ? "
|
|
936
|
-
"AND (ttl_expires IS NULL OR ttl_expires > ?)",
|
|
937
|
-
(ccr_id, tenant_id, time.time()),
|
|
938
|
-
)
|
|
939
|
-
if not rows:
|
|
940
|
-
return None
|
|
941
|
-
blob = dict(rows[0])["original_blob"]
|
|
942
|
-
plaintext = self._decrypt(blob)
|
|
943
|
-
return zlib.decompress(plaintext)
|
|
944
|
-
except (sqlite3.Error, ValueError, zlib.error) as exc:
|
|
945
|
-
logger.warning("CacheDB.ccr_get failed: %s", exc)
|
|
946
|
-
return None
|
|
947
|
-
|
|
948
|
-
def ccr_update_compressed(self, ccr_id: str, compressed: bytes) -> None:
|
|
949
|
-
import hashlib
|
|
950
|
-
try:
|
|
951
|
-
self._db.execute(
|
|
952
|
-
"UPDATE llmcache_ccr_originals "
|
|
953
|
-
"SET compressed_hash = ?, byte_size_comp = ? WHERE ccr_id = ?",
|
|
954
|
-
(hashlib.sha256(compressed).hexdigest(), len(compressed), ccr_id),
|
|
955
|
-
)
|
|
956
|
-
except sqlite3.Error as exc:
|
|
957
|
-
logger.warning("CacheDB.ccr_update_compressed failed: %s", exc)
|
|
958
|
-
|
|
959
|
-
def ccr_delete(self, ccr_id: str, *, tenant_id: str = "default") -> None:
|
|
960
|
-
"""Delete a CCR row by ccr_id scoped to tenant. Idempotent — warns on sqlite error, never raises.
|
|
961
|
-
|
|
962
|
-
Defensive delete — idempotent: deleting a non-existent row is a no-op.
|
|
963
|
-
H-02: tenant_id guard prevents a tenant from deleting another tenant's CCR.
|
|
964
|
-
"""
|
|
965
|
-
try:
|
|
966
|
-
self._db.execute(
|
|
967
|
-
"DELETE FROM llmcache_ccr_originals WHERE ccr_id = ? AND tenant_id = ?",
|
|
968
|
-
(ccr_id, tenant_id),
|
|
969
|
-
)
|
|
970
|
-
except sqlite3.Error as exc:
|
|
971
|
-
logger.warning("CacheDB.ccr_delete failed (non-fatal): %s", exc)
|
|
972
|
-
|
|
973
|
-
def ccr_count(self) -> int:
|
|
974
|
-
"""Return UNFILTERED count of rows in llmcache_ccr_originals.
|
|
975
|
-
|
|
976
|
-
Do NOT reuse TTL-filtered count from the query above. A fresh no-expiry row
|
|
977
|
-
has ttl_expires=None, so the TTL filter returns 0 and D6 orphan tests would
|
|
978
|
-
falsely pass. This unfiltered count is test infrastructure only.
|
|
979
|
-
"""
|
|
980
|
-
try:
|
|
981
|
-
rows = self._db.execute(
|
|
982
|
-
"SELECT COUNT(*) AS n FROM llmcache_ccr_originals",
|
|
983
|
-
(),
|
|
984
|
-
)
|
|
985
|
-
return int(dict(rows[0])["n"]) if rows else 0
|
|
986
|
-
except sqlite3.Error as exc:
|
|
987
|
-
logger.warning("CacheDB.ccr_count failed: %s", exc)
|
|
988
|
-
return 0
|
|
989
|
-
|
|
990
|
-
# ---- v2 additions ----
|
|
991
|
-
|
|
992
|
-
def get_all_vectors(self, tenant_id: str) -> list[tuple[str, bytes, str]]:
|
|
993
|
-
"""Return (entry_id, vector_blob, context_fp) for all vectors in a tenant.
|
|
994
|
-
|
|
995
|
-
C-10: context_fp is included so _lazy_warm_tenant() can restore it without
|
|
996
|
-
recomputing embeddings from messages that may no longer be in scope.
|
|
997
|
-
"""
|
|
998
|
-
try:
|
|
999
|
-
rows = self._db.execute(
|
|
1000
|
-
"SELECT entry_id, vector_blob, context_fp FROM llmcache_semantic_vectors "
|
|
1001
|
-
"WHERE tenant_id = ?",
|
|
1002
|
-
(tenant_id,),
|
|
1003
|
-
)
|
|
1004
|
-
return [
|
|
1005
|
-
(dict(r)["entry_id"], dict(r)["vector_blob"], dict(r).get("context_fp", ""))
|
|
1006
|
-
for r in rows
|
|
1007
|
-
]
|
|
1008
|
-
except sqlite3.Error as exc:
|
|
1009
|
-
logger.warning("CacheDB.get_all_vectors failed: %s", exc)
|
|
1010
|
-
return []
|
|
1011
|
-
|
|
1012
|
-
# ---- metrics ----
|
|
1013
|
-
|
|
1014
|
-
def metrics_load(self) -> MetricsSnapshot:
|
|
1015
|
-
try:
|
|
1016
|
-
rows = self._db.execute("SELECT * FROM llmcache_metrics WHERE id = 1")
|
|
1017
|
-
if not rows:
|
|
1018
|
-
return MetricsSnapshot()
|
|
1019
|
-
d = dict(rows[0])
|
|
1020
|
-
return MetricsSnapshot(**d)
|
|
1021
|
-
except sqlite3.Error as exc:
|
|
1022
|
-
logger.warning("CacheDB.metrics_load failed: %s", exc)
|
|
1023
|
-
return MetricsSnapshot()
|
|
1024
|
-
|
|
1025
|
-
def metrics_flush(self, snap: MetricsSnapshot) -> None:
|
|
1026
|
-
try:
|
|
1027
|
-
params = dataclasses.asdict(snap)
|
|
1028
|
-
sql = (
|
|
1029
|
-
"INSERT INTO llmcache_metrics ("
|
|
1030
|
-
" id, hits, misses, calls_skipped, "
|
|
1031
|
-
" tokens_saved_input, tokens_saved_output, tokens_saved_compress, "
|
|
1032
|
-
" evictions, latency_overhead_ms_sum, latency_samples, "
|
|
1033
|
-
" compress_runs, compress_bytes_original, compress_bytes_after, "
|
|
1034
|
-
" cache_size_bytes, cache_entry_count, updated_at) "
|
|
1035
|
-
"VALUES ("
|
|
1036
|
-
" :id, :hits, :misses, :calls_skipped, "
|
|
1037
|
-
" :tokens_saved_input, :tokens_saved_output, :tokens_saved_compress, "
|
|
1038
|
-
" :evictions, :latency_overhead_ms_sum, :latency_samples, "
|
|
1039
|
-
" :compress_runs, :compress_bytes_original, :compress_bytes_after, "
|
|
1040
|
-
" :cache_size_bytes, :cache_entry_count, :updated_at) "
|
|
1041
|
-
"ON CONFLICT(id) DO UPDATE SET "
|
|
1042
|
-
" hits=excluded.hits, misses=excluded.misses, calls_skipped=excluded.calls_skipped, "
|
|
1043
|
-
" tokens_saved_input=excluded.tokens_saved_input, "
|
|
1044
|
-
" tokens_saved_output=excluded.tokens_saved_output, "
|
|
1045
|
-
" tokens_saved_compress=excluded.tokens_saved_compress, "
|
|
1046
|
-
" evictions=excluded.evictions, "
|
|
1047
|
-
" latency_overhead_ms_sum=excluded.latency_overhead_ms_sum, "
|
|
1048
|
-
" latency_samples=excluded.latency_samples, "
|
|
1049
|
-
" compress_runs=excluded.compress_runs, "
|
|
1050
|
-
" compress_bytes_original=excluded.compress_bytes_original, "
|
|
1051
|
-
" compress_bytes_after=excluded.compress_bytes_after, "
|
|
1052
|
-
" cache_size_bytes=excluded.cache_size_bytes, "
|
|
1053
|
-
" cache_entry_count=excluded.cache_entry_count, "
|
|
1054
|
-
" updated_at=excluded.updated_at"
|
|
1055
|
-
)
|
|
1056
|
-
with self._db.transaction():
|
|
1057
|
-
self._db.execute(sql, params)
|
|
1058
|
-
except sqlite3.Error as exc:
|
|
1059
|
-
logger.warning("CacheDB.metrics_flush failed: %s", exc)
|
|
1060
|
-
|
|
1061
|
-
# ---- KV counters (M2 — durable slm_cache_* hit/miss stats) ----
|
|
1062
|
-
|
|
1063
|
-
def kv_counter_incr(self, name: str, delta: int = 1) -> None:
|
|
1064
|
-
"""Atomically add ``delta`` to a named KV counter (fail-open)."""
|
|
1065
|
-
try:
|
|
1066
|
-
with self._db.transaction():
|
|
1067
|
-
self._db.execute(
|
|
1068
|
-
"INSERT INTO llmcache_kv_counters(name, count) "
|
|
1069
|
-
"VALUES (:name, :delta) "
|
|
1070
|
-
"ON CONFLICT(name) DO UPDATE SET count = count + :delta",
|
|
1071
|
-
{"name": name, "delta": delta},
|
|
1072
|
-
)
|
|
1073
|
-
except sqlite3.Error as exc:
|
|
1074
|
-
logger.debug("CacheDB.kv_counter_incr(%s) failed (non-fatal): %s", name, exc)
|
|
1075
|
-
|
|
1076
|
-
def kv_counters_load(self) -> dict[str, int]:
|
|
1077
|
-
"""Return all persisted KV counters as {name: count} (fail-open)."""
|
|
1078
|
-
try:
|
|
1079
|
-
rows = self._db.execute("SELECT name, count FROM llmcache_kv_counters")
|
|
1080
|
-
return {r["name"]: r["count"] for r in rows}
|
|
1081
|
-
except sqlite3.Error as exc:
|
|
1082
|
-
logger.debug("CacheDB.kv_counters_load failed: %s", exc)
|
|
1083
|
-
return {}
|
|
1084
|
-
|
|
1085
|
-
# ---- convenience / non-contract helpers ----
|
|
1086
|
-
|
|
1087
|
-
def entry_exists(self, cache_key: str, tenant_id: str = _DEFAULT_TENANT) -> bool:
|
|
1088
|
-
tenant_id = _normalize_tenant_id(tenant_id)
|
|
1089
|
-
try:
|
|
1090
|
-
rows = self._db.execute(
|
|
1091
|
-
"SELECT 1 FROM llmcache_entries WHERE cache_key = ? AND tenant_id = ? LIMIT 1",
|
|
1092
|
-
(cache_key, tenant_id),
|
|
1093
|
-
)
|
|
1094
|
-
return bool(rows)
|
|
1095
|
-
except sqlite3.Error:
|
|
1096
|
-
return False
|
|
1097
|
-
|
|
1098
|
-
def clear_tenant(self, tenant_id: str) -> int:
|
|
1099
|
-
tenant_id = _normalize_tenant_id(tenant_id)
|
|
1100
|
-
try:
|
|
1101
|
-
with self._db.transaction():
|
|
1102
|
-
rows = self._db.execute(
|
|
1103
|
-
"SELECT entry_id FROM llmcache_entries WHERE tenant_id = ?",
|
|
1104
|
-
(tenant_id,),
|
|
1105
|
-
)
|
|
1106
|
-
entry_ids = [dict(r)["entry_id"] for r in rows]
|
|
1107
|
-
if entry_ids:
|
|
1108
|
-
placeholders = ",".join("?" for _ in entry_ids)
|
|
1109
|
-
self._db.execute(
|
|
1110
|
-
f"DELETE FROM llmcache_semantic_vectors "
|
|
1111
|
-
f"WHERE entry_id IN ({placeholders})",
|
|
1112
|
-
tuple(entry_ids),
|
|
1113
|
-
)
|
|
1114
|
-
self._db.execute(
|
|
1115
|
-
"DELETE FROM llmcache_tags WHERE tenant_id = ?", (tenant_id,),
|
|
1116
|
-
)
|
|
1117
|
-
self._db.execute(
|
|
1118
|
-
"DELETE FROM llmcache_entries WHERE tenant_id = ?", (tenant_id,),
|
|
1119
|
-
)
|
|
1120
|
-
return len(entry_ids)
|
|
1121
|
-
except sqlite3.Error as exc:
|
|
1122
|
-
logger.warning("CacheDB.clear_tenant failed: %s", exc)
|
|
1123
|
-
return 0
|
|
1124
|
-
|
|
1125
|
-
def entry_count(self, tenant_id: str = _DEFAULT_TENANT) -> int:
|
|
1126
|
-
tenant_id = _normalize_tenant_id(tenant_id)
|
|
1127
|
-
try:
|
|
1128
|
-
rows = self._db.execute(
|
|
1129
|
-
"SELECT COUNT(*) AS n FROM llmcache_entries "
|
|
1130
|
-
"WHERE tenant_id = ? AND (ttl_expires IS NULL OR ttl_expires > ?)",
|
|
1131
|
-
(tenant_id, time.time()),
|
|
1132
|
-
)
|
|
1133
|
-
return int(dict(rows[0])["n"]) if rows else 0
|
|
1134
|
-
except sqlite3.Error:
|
|
1135
|
-
return 0
|
|
1136
|
-
|
|
1137
|
-
def db_size_bytes(self) -> int:
|
|
1138
|
-
try:
|
|
1139
|
-
rows = self._db.execute("PRAGMA page_count")
|
|
1140
|
-
page_count = int(dict(rows[0])["page_count"]) if rows else 0
|
|
1141
|
-
rows = self._db.execute("PRAGMA page_size")
|
|
1142
|
-
page_size = int(dict(rows[0])["page_size"]) if rows else 0
|
|
1143
|
-
return page_count * page_size
|
|
1144
|
-
except sqlite3.Error:
|
|
1145
|
-
return 0
|
|
1146
|
-
|
|
1147
|
-
# ---- internal helpers ----
|
|
1148
|
-
|
|
1149
|
-
def _row_to_cacherow(self, row: dict) -> "CacheRow":
|
|
1150
|
-
"""Build a CacheRow from a sqlite3.Row dict, robust to extra/missing keys."""
|
|
1151
|
-
kwargs: dict[str, Any] = {}
|
|
1152
|
-
for fname in CacheRow.__dataclass_fields__:
|
|
1153
|
-
if fname == "value":
|
|
1154
|
-
continue
|
|
1155
|
-
if fname in row:
|
|
1156
|
-
kwargs[fname] = row[fname]
|
|
1157
|
-
kwargs["value"] = row.get("value", b"")
|
|
1158
|
-
try:
|
|
1159
|
-
kwargs["tags"] = json.loads(row.get("tag_json") or "[]")
|
|
1160
|
-
except (json.JSONDecodeError, TypeError):
|
|
1161
|
-
kwargs["tags"] = []
|
|
1162
|
-
return CacheRow(**kwargs)
|
|
1163
|
-
|
|
1164
|
-
# ---- singleton (INTERFACE-CONTRACT v2.2 §1) ----
|
|
1165
|
-
|
|
1166
|
-
@classmethod
|
|
1167
|
-
def get_default(cls) -> "CacheDB":
|
|
1168
|
-
if cls._default_instance is None:
|
|
1169
|
-
import threading as _t
|
|
1170
|
-
if cls._default_lock is None:
|
|
1171
|
-
cls._default_lock = _t.Lock()
|
|
1172
|
-
with cls._default_lock:
|
|
1173
|
-
if cls._default_instance is None:
|
|
1174
|
-
cls._default_instance = cls()
|
|
1175
|
-
return cls._default_instance
|
|
1176
|
-
|
|
1177
|
-
@classmethod
|
|
1178
|
-
def reset_default(cls) -> None:
|
|
1179
|
-
"""Reset the singleton (testing only)."""
|
|
1180
|
-
if cls._default_instance is not None:
|
|
1181
|
-
try:
|
|
1182
|
-
cls._default_instance.close()
|
|
1183
|
-
except Exception:
|
|
1184
|
-
pass
|
|
1185
|
-
cls._default_instance = None
|