superlocalmemory 3.7.7 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +85 -0
- package/README.md +199 -29
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/CLAUDE.md +8 -8
- package/plugin/agents/slm-governance-advisor.md +80 -0
- package/plugin/agents/slm-loop-runner.md +71 -0
- package/plugin/agents/slm-memory-advisor.md +10 -5
- package/plugin/agents/slm-optimize-advisor.md +9 -3
- package/plugin/commands/slm-loop.md +31 -0
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/requirements.txt +1 -1
- package/plugin/scripts/slm-launch +46 -7
- package/plugin/settings.json +9 -0
- package/plugin/skills/slm-cache/SKILL.md +9 -1
- package/plugin/skills/slm-compress/SKILL.md +8 -1
- package/plugin/skills/slm-governance/SKILL.md +248 -0
- package/plugin/skills/slm-graph/SKILL.md +17 -3
- package/plugin/skills/slm-loop/SKILL.md +99 -0
- package/plugin/skills/slm-mesh/SKILL.md +282 -0
- package/plugin/skills/slm-profile/SKILL.md +148 -0
- package/plugin/skills/slm-recall/SKILL.md +46 -10
- package/plugin/skills/slm-remember/SKILL.md +48 -1
- package/plugin/skills/slm-scope/SKILL.md +176 -0
- package/plugin/skills/slm-session/SKILL.md +24 -1
- package/plugin/skills/slm-status/SKILL.md +18 -1
- package/plugin-src/agents/slm-governance-advisor.md +80 -0
- package/plugin-src/agents/slm-loop-runner.md +71 -0
- package/plugin-src/agents/slm-memory-advisor.md +10 -5
- package/plugin-src/agents/slm-optimize-advisor.md +9 -3
- package/plugin-src/commands/slm-loop.md +31 -0
- package/plugin-src/hooks/hooks.json +79 -0
- package/plugin-src/manifest.json +7 -2
- package/plugin-src/requirements.txt +1 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/rules/CLAUDE.md.fragment +8 -8
- package/plugin-src/scripts/slm-launch +46 -7
- package/plugin-src/settings.json +9 -0
- package/plugin-src/skills/slm-cache/SKILL.md +9 -1
- package/plugin-src/skills/slm-compress/SKILL.md +8 -1
- package/plugin-src/skills/slm-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- package/plugin-src/skills/slm-recall/SKILL.md +46 -10
- package/plugin-src/skills/slm-remember/SKILL.md +48 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +1 -2
- package/scripts/postinstall/validation.js +2 -0
- package/scripts/postinstall-interactive.js +74 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/__init__.py +3 -0
- package/src/superlocalmemory/access/rbac.py +477 -0
- package/src/superlocalmemory/cli/commands.py +96 -12
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/loop_cmd.py +192 -0
- package/src/superlocalmemory/cli/main.py +39 -4
- package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
- package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
- package/src/superlocalmemory/cli/pending_store.py +49 -13
- package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
- package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
- package/src/superlocalmemory/cli/setup_wizard.py +22 -13
- package/src/superlocalmemory/compliance/audit.py +6 -0
- package/src/superlocalmemory/compliance/gdpr.py +128 -138
- package/src/superlocalmemory/compliance/retention.py +176 -45
- package/src/superlocalmemory/core/backend_orchestrator.py +5 -43
- package/src/superlocalmemory/core/community_summary.py +267 -0
- package/src/superlocalmemory/core/config.py +216 -3
- package/src/superlocalmemory/core/consolidation_engine.py +95 -22
- package/src/superlocalmemory/core/context_cache.py +61 -18
- package/src/superlocalmemory/core/embedding_worker.py +17 -2
- package/src/superlocalmemory/core/embeddings.py +12 -1
- package/src/superlocalmemory/core/engine.py +17 -1
- package/src/superlocalmemory/core/engine_ingestion.py +29 -0
- package/src/superlocalmemory/core/engine_wiring.py +13 -0
- package/src/superlocalmemory/core/entity_community.py +178 -0
- package/src/superlocalmemory/core/graph_analyzer.py +39 -2
- package/src/superlocalmemory/core/graph_pruner.py +13 -8
- package/src/superlocalmemory/core/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- package/src/superlocalmemory/core/modes.py +1 -1
- package/src/superlocalmemory/core/mutations.py +2 -2
- package/src/superlocalmemory/core/pii.py +105 -0
- package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
- package/src/superlocalmemory/core/recall_pipeline.py +2 -0
- package/src/superlocalmemory/core/recall_worker.py +20 -6
- package/src/superlocalmemory/core/scale_engine.py +60 -1
- package/src/superlocalmemory/core/security_primitives.py +40 -2
- package/src/superlocalmemory/core/store_pipeline.py +35 -11
- package/src/superlocalmemory/core/worker_pool.py +21 -6
- package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
- package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
- package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
- package/src/superlocalmemory/evolution/evolution_store.py +122 -45
- package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
- package/src/superlocalmemory/evolution/model_selection.py +160 -0
- package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
- package/src/superlocalmemory/evolution/triggers.py +22 -13
- package/src/superlocalmemory/graph/cozo_backend.py +43 -20
- package/src/superlocalmemory/hooks/adapter_base.py +5 -1
- package/src/superlocalmemory/hooks/auto_recall.py +13 -1
- package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
- package/src/superlocalmemory/hooks/codex_assets.py +64 -5
- package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
- package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +114 -1
- package/src/superlocalmemory/infra/auth_middleware.py +28 -0
- package/src/superlocalmemory/infra/backup.py +12 -1
- package/src/superlocalmemory/infra/daemon_identity.py +40 -4
- package/src/superlocalmemory/infra/data_root.py +43 -4
- package/src/superlocalmemory/infra/event_bus.py +107 -24
- package/src/superlocalmemory/infra/rate_limiter.py +93 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
- package/src/superlocalmemory/ingestion/credentials.py +1 -1
- package/src/superlocalmemory/learning/cross_project.py +28 -19
- package/src/superlocalmemory/learning/reward_proxy.py +42 -9
- package/src/superlocalmemory/loops/__init__.py +56 -0
- package/src/superlocalmemory/loops/budget.py +58 -0
- package/src/superlocalmemory/loops/engine.py +164 -0
- package/src/superlocalmemory/loops/ledger.py +243 -0
- package/src/superlocalmemory/loops/models.py +152 -0
- package/src/superlocalmemory/loops/rules.py +52 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
- package/src/superlocalmemory/mcp/profiles.py +103 -0
- package/src/superlocalmemory/mcp/server.py +21 -49
- package/src/superlocalmemory/mcp/tools_active.py +4 -7
- package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
- package/src/superlocalmemory/mcp/tools_core.py +50 -5
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- package/src/superlocalmemory/mcp/tools_loops.py +300 -0
- package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
- package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
- package/src/superlocalmemory/mesh/broker.py +237 -129
- package/src/superlocalmemory/mesh/remote_sync.py +50 -8
- package/src/superlocalmemory/optimize/NOTICE +1 -6
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
- package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
- package/src/superlocalmemory/optimize/compress/align.py +32 -26
- package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
- package/src/superlocalmemory/optimize/compress/router.py +105 -22
- package/src/superlocalmemory/optimize/config/defaults.py +1 -1
- package/src/superlocalmemory/optimize/config/schema.py +87 -4
- package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
- package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
- package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
- package/src/superlocalmemory/optimize/storage/db.py +38 -9
- package/src/superlocalmemory/optimize/storage/schema.py +10 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
- package/src/superlocalmemory/retrieval/engine.py +168 -26
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
- package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
- package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
- package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
- package/src/superlocalmemory/retrieval/time_window.py +181 -0
- package/src/superlocalmemory/server/api.py +21 -4
- package/src/superlocalmemory/server/profile_runtime.py +125 -8
- package/src/superlocalmemory/server/rbac_enforce.py +142 -0
- package/src/superlocalmemory/server/recall_health.py +24 -3
- package/src/superlocalmemory/server/recall_serializer.py +19 -1
- package/src/superlocalmemory/server/routes/abstraction.py +115 -0
- package/src/superlocalmemory/server/routes/agents.py +128 -38
- package/src/superlocalmemory/server/routes/backup.py +34 -10
- package/src/superlocalmemory/server/routes/behavioral.py +13 -12
- package/src/superlocalmemory/server/routes/brain.py +21 -5
- package/src/superlocalmemory/server/routes/chat.py +72 -16
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +436 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +9 -4
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +135 -17
- package/src/superlocalmemory/server/routes/helpers.py +16 -1
- package/src/superlocalmemory/server/routes/ingest.py +7 -4
- package/src/superlocalmemory/server/routes/insights.py +3 -3
- package/src/superlocalmemory/server/routes/learning.py +14 -14
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +221 -49
- package/src/superlocalmemory/server/routes/mesh.py +95 -15
- package/src/superlocalmemory/server/routes/optimize.py +33 -1
- package/src/superlocalmemory/server/routes/prewarm.py +2 -0
- package/src/superlocalmemory/server/routes/profiles.py +63 -17
- package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +13 -6
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/v3_api.py +194 -81
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +30 -5
- package/src/superlocalmemory/server/unified_daemon.py +431 -75
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +53 -0
- package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
- package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
- package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
- package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
- package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
- package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
- package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +87 -0
- package/src/superlocalmemory/storage/schema_v32.py +0 -9
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- package/src/superlocalmemory/trust/gate.py +49 -8
- package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
- package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
- package/src/superlocalmemory/ui/css/design-system.css +621 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
- package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
- package/src/superlocalmemory/ui/favicon.svg +35 -4
- package/src/superlocalmemory/ui/index.html +306 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +47 -31
- package/src/superlocalmemory/ui/js/dashboard.js +314 -63
- package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
- package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
- package/src/superlocalmemory/ui/js/math-health.js +1 -1
- package/src/superlocalmemory/ui/js/memories.js +15 -4
- package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
- package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
- package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
- package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
- package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
- package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
- package/src/superlocalmemory/ui/js/od-agents.js +544 -0
- package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
- package/src/superlocalmemory/ui/js/od-backup.js +780 -0
- package/src/superlocalmemory/ui/js/od-brain.js +779 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +593 -0
- package/src/superlocalmemory/ui/js/od-health.js +539 -0
- package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
- package/src/superlocalmemory/ui/js/od-memories.js +887 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
- package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
- package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
- package/src/superlocalmemory/ui/js/od-shell.js +593 -0
- package/src/superlocalmemory/ui/js/od-skills.js +573 -0
- package/src/superlocalmemory/ui/js/od-team.js +258 -0
- package/src/superlocalmemory/ui/js/profiles.js +159 -46
- package/src/superlocalmemory/ui/js/settings.js +2 -2
- package/src/superlocalmemory/ui/js/timeline.js +34 -5
- package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
- package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -236,6 +236,11 @@ class EncodingConfig:
|
|
|
236
236
|
# Entropy gate
|
|
237
237
|
entropy_threshold: float = 0.95
|
|
238
238
|
|
|
239
|
+
# Entity reflexion (Wave Q1) — Mode B/C self-review of extracted entities.
|
|
240
|
+
# Fail-open; one extra bounded LLM call per chunk. Mode A is unaffected.
|
|
241
|
+
enable_entity_reflexion: bool = True
|
|
242
|
+
reflexion_max_facts: int = 8
|
|
243
|
+
|
|
239
244
|
|
|
240
245
|
# ---------------------------------------------------------------------------
|
|
241
246
|
# Retrieval Config
|
|
@@ -297,6 +302,11 @@ class RetrievalConfig:
|
|
|
297
302
|
recall_per_fact_max_chars: int = 2400 # ~600 tokens; head 70% + tail 30%
|
|
298
303
|
recall_total_max_chars: int = 12000 # ~3K tokens; stubs beyond this
|
|
299
304
|
|
|
305
|
+
# Wave Q2b: attach a precomputed community summary as thematic context when
|
|
306
|
+
# the top results cluster in one community. Read-only lookup, gated, and
|
|
307
|
+
# fail-open — never a per-query LLM call. Kill-switch for tuning.
|
|
308
|
+
enable_community_context: bool = True
|
|
309
|
+
|
|
300
310
|
|
|
301
311
|
# ---------------------------------------------------------------------------
|
|
302
312
|
# Math Config
|
|
@@ -659,6 +669,14 @@ class TemporalValidatorConfig:
|
|
|
659
669
|
# Sheaf contradiction threshold
|
|
660
670
|
contradiction_threshold: float = 0.45 # Mode A threshold (768d)
|
|
661
671
|
|
|
672
|
+
# P5-INT-01: superseded facts are DEMOTED in recall, not hidden. A fact
|
|
673
|
+
# marked system_expired_at keeps its channel evidence but its per-channel
|
|
674
|
+
# score is multiplied by this factor, so current facts rank above it while
|
|
675
|
+
# nothing valid silently vanishes (Mem0-2026 non-destructive design;
|
|
676
|
+
# retrieval-time recency resolves conflicts). 0.0 restores the old hide
|
|
677
|
+
# behaviour; 1.0 disables demotion.
|
|
678
|
+
superseded_demotion_factor: float = 0.25
|
|
679
|
+
|
|
662
680
|
# LLM pre-filter threshold (lower to catch more candidates)
|
|
663
681
|
llm_prefilter_threshold: float = 0.30
|
|
664
682
|
|
|
@@ -677,18 +695,31 @@ class EvolutionConfig:
|
|
|
677
695
|
"""Configuration for Skill Evolution Engine (v3.4.10).
|
|
678
696
|
|
|
679
697
|
OFF by default — opt in via `slm setup` (interactive) or
|
|
680
|
-
`slm config set evolution.enabled true` (CLI).
|
|
698
|
+
`slm config set evolution.enabled true` (CLI). Enabling makes
|
|
699
|
+
background LLM calls; the enable flow surfaces a cost advisory.
|
|
681
700
|
|
|
682
701
|
Backend auto-detection priority:
|
|
683
702
|
1. `claude` CLI available → spawn `claude --model haiku` (ECC pattern, free)
|
|
684
703
|
2. Ollama running → use Ollama (free, local)
|
|
685
704
|
3. API key set → use Anthropic/OpenAI API (paid)
|
|
686
705
|
4. Nothing → dashboard-only (show candidates, manual evolution)
|
|
706
|
+
|
|
707
|
+
Model selection (v3.7.9): each pipeline step is configurable and
|
|
708
|
+
defaults to the lowest-cost capable model for the active backend
|
|
709
|
+
(Claude→haiku, Ollama→local). Leave a field empty ("") for "auto".
|
|
710
|
+
The blind verifier is kept on a *different* model from the generator
|
|
711
|
+
so it can't grade its own homework; see
|
|
712
|
+
``evolution.model_selection.resolve_evolution_models``.
|
|
687
713
|
"""
|
|
688
714
|
|
|
689
715
|
enabled: bool = False # OFF by default, opt-in
|
|
690
716
|
backend: str = "auto" # auto, claude, ollama, anthropic, openai
|
|
691
717
|
max_evolutions_per_cycle: int = 3 # Budget cap per consolidation
|
|
718
|
+
# Empty string == "auto" (resolve cheapest capable model at runtime).
|
|
719
|
+
# Accepts short aliases ("haiku"/"sonnet") or allow-listed model ids.
|
|
720
|
+
mutation_model: str = "" # generator (quality-sensitive)
|
|
721
|
+
verify_model: str = "" # blind verifier (must differ from generator)
|
|
722
|
+
confirm_model: str = "" # cheap yes/no gate
|
|
692
723
|
|
|
693
724
|
|
|
694
725
|
@dataclass(frozen=True)
|
|
@@ -743,6 +774,152 @@ class AutoInvokeConfig:
|
|
|
743
774
|
relevance_threshold: float = 0.3 # Legacy compat with AutoRecall
|
|
744
775
|
|
|
745
776
|
|
|
777
|
+
# ---------------------------------------------------------------------------
|
|
778
|
+
# Deployment Config (v3.8.0)
|
|
779
|
+
# ---------------------------------------------------------------------------
|
|
780
|
+
|
|
781
|
+
_VALID_DEPLOYMENT_MODES = ("personal", "enterprise")
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
@dataclass(frozen=True)
|
|
785
|
+
class DeploymentConfig:
|
|
786
|
+
"""Deployment mode configuration.
|
|
787
|
+
|
|
788
|
+
Personal (default, safe defaults):
|
|
789
|
+
Single-user install. No login required, no PII redaction, no
|
|
790
|
+
retention scheduler. Behaviour is identical to pre-3.8.0 installs.
|
|
791
|
+
|
|
792
|
+
Enterprise:
|
|
793
|
+
Multi-user / team / company install. Login required, PII redacted,
|
|
794
|
+
retention scheduler active, audit enabled.
|
|
795
|
+
|
|
796
|
+
SAFE DEFAULTS = Personal — an existing config.toml with no [deployment]
|
|
797
|
+
section continues to behave exactly as before. Adding a [deployment]
|
|
798
|
+
section is opt-in; removing it reverts to Personal automatically.
|
|
799
|
+
"""
|
|
800
|
+
|
|
801
|
+
mode: str = "personal" # "personal" | "enterprise"
|
|
802
|
+
require_login: bool = False
|
|
803
|
+
pii_redaction: bool = False
|
|
804
|
+
retention_enabled: bool = False
|
|
805
|
+
audit: bool = True
|
|
806
|
+
|
|
807
|
+
def __post_init__(self) -> None:
|
|
808
|
+
if self.mode not in _VALID_DEPLOYMENT_MODES:
|
|
809
|
+
raise ValueError(
|
|
810
|
+
f"DeploymentConfig.mode must be one of {_VALID_DEPLOYMENT_MODES!r}, "
|
|
811
|
+
f"got {self.mode!r}"
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
@property
|
|
815
|
+
def is_personal(self) -> bool:
|
|
816
|
+
"""True when operating in personal (single-user) mode."""
|
|
817
|
+
return self.mode == "personal"
|
|
818
|
+
|
|
819
|
+
@property
|
|
820
|
+
def is_enterprise(self) -> bool:
|
|
821
|
+
"""True when operating in enterprise (team/company) mode."""
|
|
822
|
+
return self.mode == "enterprise"
|
|
823
|
+
|
|
824
|
+
def as_dict(self) -> dict:
|
|
825
|
+
return {
|
|
826
|
+
"mode": self.mode,
|
|
827
|
+
"require_login": self.require_login,
|
|
828
|
+
"pii_redaction": self.pii_redaction,
|
|
829
|
+
"retention_enabled": self.retention_enabled,
|
|
830
|
+
"audit": self.audit,
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
#: Canonical Personal preset — all permissive defaults.
|
|
835
|
+
DEPLOYMENT_PERSONAL = DeploymentConfig(
|
|
836
|
+
mode="personal",
|
|
837
|
+
require_login=False,
|
|
838
|
+
pii_redaction=False,
|
|
839
|
+
retention_enabled=False,
|
|
840
|
+
audit=True,
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
#: Canonical Enterprise preset — all enforcement defaults.
|
|
844
|
+
DEPLOYMENT_ENTERPRISE = DeploymentConfig(
|
|
845
|
+
mode="enterprise",
|
|
846
|
+
require_login=True,
|
|
847
|
+
pii_redaction=True,
|
|
848
|
+
retention_enabled=True,
|
|
849
|
+
audit=True,
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def load_deployment_config(
|
|
854
|
+
config_toml_path: Path | None = None,
|
|
855
|
+
) -> DeploymentConfig:
|
|
856
|
+
"""Parse [deployment] from config.toml; return Personal defaults if absent.
|
|
857
|
+
|
|
858
|
+
config.toml is the installer-written performance config (separate from the
|
|
859
|
+
daemon's config.json managed by SLMConfig). This function reads ONLY the
|
|
860
|
+
[deployment] section. All other sections are ignored.
|
|
861
|
+
|
|
862
|
+
Args:
|
|
863
|
+
config_toml_path: Explicit path to config.toml. When None, resolved to
|
|
864
|
+
``~/.superlocalmemory/config.toml`` via the canonical data root.
|
|
865
|
+
|
|
866
|
+
Returns:
|
|
867
|
+
DeploymentConfig — Personal preset when the file is absent, the section
|
|
868
|
+
is missing, or any parse error occurs (fail-open, non-destructive).
|
|
869
|
+
"""
|
|
870
|
+
if config_toml_path is None:
|
|
871
|
+
try:
|
|
872
|
+
config_toml_path = _runtime_base_dir() / "config.toml"
|
|
873
|
+
except Exception:
|
|
874
|
+
return DEPLOYMENT_PERSONAL
|
|
875
|
+
|
|
876
|
+
if not config_toml_path.exists():
|
|
877
|
+
return DEPLOYMENT_PERSONAL
|
|
878
|
+
|
|
879
|
+
try:
|
|
880
|
+
import tomllib as _tomllib
|
|
881
|
+
raw = config_toml_path.read_text(encoding="utf-8")
|
|
882
|
+
data = _tomllib.loads(raw)
|
|
883
|
+
except Exception as exc:
|
|
884
|
+
logger.warning(
|
|
885
|
+
"load_deployment_config: failed to parse %s: %s", config_toml_path, exc
|
|
886
|
+
)
|
|
887
|
+
return DEPLOYMENT_PERSONAL
|
|
888
|
+
|
|
889
|
+
dep = data.get("deployment", {})
|
|
890
|
+
if not dep:
|
|
891
|
+
# No [deployment] section — personal defaults, no behaviour change.
|
|
892
|
+
return DEPLOYMENT_PERSONAL
|
|
893
|
+
|
|
894
|
+
raw_mode = str(dep.get("mode", "personal")).lower()
|
|
895
|
+
if raw_mode not in _VALID_DEPLOYMENT_MODES:
|
|
896
|
+
logger.warning(
|
|
897
|
+
"load_deployment_config: unknown mode %r in %s — defaulting to personal",
|
|
898
|
+
raw_mode, config_toml_path,
|
|
899
|
+
)
|
|
900
|
+
raw_mode = "personal"
|
|
901
|
+
|
|
902
|
+
# Use the preset for the mode as the base so omitted keys inherit
|
|
903
|
+
# sensible values (enterprise → require_login=True etc.).
|
|
904
|
+
base = DEPLOYMENT_ENTERPRISE if raw_mode == "enterprise" else DEPLOYMENT_PERSONAL
|
|
905
|
+
|
|
906
|
+
try:
|
|
907
|
+
return DeploymentConfig(
|
|
908
|
+
mode=raw_mode,
|
|
909
|
+
require_login=bool(dep.get("require_login", base.require_login)),
|
|
910
|
+
pii_redaction=bool(dep.get("pii_redaction", base.pii_redaction)),
|
|
911
|
+
retention_enabled=bool(dep.get("retention_enabled", base.retention_enabled)),
|
|
912
|
+
audit=bool(dep.get("audit", base.audit)),
|
|
913
|
+
)
|
|
914
|
+
except (ValueError, TypeError) as exc:
|
|
915
|
+
logger.warning(
|
|
916
|
+
"load_deployment_config: invalid values in [deployment] in %s: %s — "
|
|
917
|
+
"falling back to personal",
|
|
918
|
+
config_toml_path, exc,
|
|
919
|
+
)
|
|
920
|
+
return DEPLOYMENT_PERSONAL
|
|
921
|
+
|
|
922
|
+
|
|
746
923
|
# ---------------------------------------------------------------------------
|
|
747
924
|
# Health Config (v3.6.9 BUG-A)
|
|
748
925
|
# ---------------------------------------------------------------------------
|
|
@@ -849,6 +1026,35 @@ class SLMConfig:
|
|
|
849
1026
|
path = config_path or (_runtime_base / "config.json")
|
|
850
1027
|
if not path.exists():
|
|
851
1028
|
return cls.for_mode(Mode.A, base_dir=_runtime_base)
|
|
1029
|
+
|
|
1030
|
+
# DASH-V4 (3.7.9): the ``current_mode`` file is the single source of
|
|
1031
|
+
# truth for the ACTIVE mode. If config.json drifted from it (a writer
|
|
1032
|
+
# bypassed switch_mode and reset the active config), load the
|
|
1033
|
+
# authoritative per-mode file so daemon/CLI/dashboard all run the user's
|
|
1034
|
+
# chosen mode — with its settings — instead of a stale one. Scoped to
|
|
1035
|
+
# config.json so explicit per-mode loads (switch_mode) are untouched.
|
|
1036
|
+
# Fail-open: any error falls through to the normal load below.
|
|
1037
|
+
if path.name == "config.json":
|
|
1038
|
+
try:
|
|
1039
|
+
import json as _json
|
|
1040
|
+
_disk_mode = str(
|
|
1041
|
+
_json.loads(path.read_text()).get("mode", "")
|
|
1042
|
+
).lower()
|
|
1043
|
+
_active_mode = cls.read_current_mode(path.parent)
|
|
1044
|
+
if _disk_mode and _active_mode and _disk_mode != _active_mode:
|
|
1045
|
+
_mode_path = cls._mode_config_path(
|
|
1046
|
+
path.parent, Mode(_active_mode)
|
|
1047
|
+
)
|
|
1048
|
+
if _mode_path.exists():
|
|
1049
|
+
logger.warning(
|
|
1050
|
+
"config.json mode=%s disagrees with current_mode=%s;"
|
|
1051
|
+
" loading authoritative %s",
|
|
1052
|
+
_disk_mode, _active_mode, _mode_path.name,
|
|
1053
|
+
)
|
|
1054
|
+
return cls.load(_mode_path)
|
|
1055
|
+
except Exception:
|
|
1056
|
+
pass # fail-open — fall through to the normal load below
|
|
1057
|
+
|
|
852
1058
|
import json
|
|
853
1059
|
try:
|
|
854
1060
|
data = json.loads(path.read_text())
|
|
@@ -1070,6 +1276,9 @@ class SLMConfig:
|
|
|
1070
1276
|
"enabled": self.evolution.enabled,
|
|
1071
1277
|
"backend": self.evolution.backend,
|
|
1072
1278
|
"max_evolutions_per_cycle": self.evolution.max_evolutions_per_cycle,
|
|
1279
|
+
"mutation_model": self.evolution.mutation_model,
|
|
1280
|
+
"verify_model": self.evolution.verify_model,
|
|
1281
|
+
"confirm_model": self.evolution.confirm_model,
|
|
1073
1282
|
}
|
|
1074
1283
|
|
|
1075
1284
|
# V3.4.65: Persist injection config
|
|
@@ -1264,6 +1473,7 @@ class SLMConfig:
|
|
|
1264
1473
|
_c_emb = EmbeddingConfig(
|
|
1265
1474
|
model_name=embedding_model_name or "text-embedding-3-large",
|
|
1266
1475
|
dimension=embedding_dimension or 3072,
|
|
1476
|
+
provider=_c_emb_provider,
|
|
1267
1477
|
api_endpoint=embedding_endpoint,
|
|
1268
1478
|
api_key=embedding_key,
|
|
1269
1479
|
deployment_name=embedding_deployment,
|
|
@@ -1275,8 +1485,11 @@ class SLMConfig:
|
|
|
1275
1485
|
# 768-dim Ollama embedder, which made ingestion fail at vector
|
|
1276
1486
|
# materialization. Cloud embeddings remain an explicit opt-in.
|
|
1277
1487
|
_c_emb = EmbeddingConfig(
|
|
1278
|
-
|
|
1279
|
-
|
|
1488
|
+
# Honour an on-disk embedding model when one was configured (the
|
|
1489
|
+
# load() path passes it through); default to the local nomic
|
|
1490
|
+
# model so Mode C never silently requires a paid cloud embedder.
|
|
1491
|
+
model_name=embedding_model_name or "nomic-ai/nomic-embed-text-v1.5",
|
|
1492
|
+
dimension=embedding_dimension or 768,
|
|
1280
1493
|
)
|
|
1281
1494
|
return cls(
|
|
1282
1495
|
mode=mode,
|
|
@@ -49,6 +49,59 @@ if TYPE_CHECKING:
|
|
|
49
49
|
logger = logging.getLogger(__name__)
|
|
50
50
|
|
|
51
51
|
|
|
52
|
+
def _recompute_entity_communities(
|
|
53
|
+
db: Any, profile_id: str, summarizer: Any = None,
|
|
54
|
+
) -> dict[str, int]:
|
|
55
|
+
"""Wave Q: rebuild the entity-community backbone + summaries (fail-open).
|
|
56
|
+
|
|
57
|
+
Shared spine for Q2 community summaries and Q3 progressive abstraction.
|
|
58
|
+
Runs in the background consolidation lane; never blocks store/recall.
|
|
59
|
+
Community detection and summary generation are independently fail-open.
|
|
60
|
+
"""
|
|
61
|
+
result: dict[str, int] = {"entity_count": 0, "community_count": 0}
|
|
62
|
+
try:
|
|
63
|
+
from superlocalmemory.core.entity_community import EntityCommunityBuilder
|
|
64
|
+
|
|
65
|
+
result = EntityCommunityBuilder(db).compute_and_store(profile_id)
|
|
66
|
+
logger.info(
|
|
67
|
+
"Background entity-community: %d entities, %d communities",
|
|
68
|
+
result.get("entity_count", 0),
|
|
69
|
+
result.get("community_count", 0),
|
|
70
|
+
)
|
|
71
|
+
except Exception as exc:
|
|
72
|
+
logger.debug("Entity-community recompute failed (non-fatal): %s", exc)
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
# Wave Q2: one synthesized report per community (rides on the backbone).
|
|
76
|
+
try:
|
|
77
|
+
from superlocalmemory.core.community_summary import CommunitySummaryBuilder
|
|
78
|
+
|
|
79
|
+
summ = CommunitySummaryBuilder(db, summarizer=summarizer).compute_and_store(
|
|
80
|
+
profile_id,
|
|
81
|
+
)
|
|
82
|
+
result["summaries_written"] = summ.get("summaries_written", 0)
|
|
83
|
+
logger.info(
|
|
84
|
+
"Background community summaries: %d written",
|
|
85
|
+
summ.get("summaries_written", 0),
|
|
86
|
+
)
|
|
87
|
+
except Exception as exc:
|
|
88
|
+
logger.debug("Community summaries failed (non-fatal): %s", exc)
|
|
89
|
+
|
|
90
|
+
# Wave Q3: persona roll-up (top tier over the community summaries).
|
|
91
|
+
try:
|
|
92
|
+
from superlocalmemory.core.progressive_abstraction import (
|
|
93
|
+
ProgressiveAbstraction,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
pa = ProgressiveAbstraction(db, summarizer=summarizer).compute_and_store(
|
|
97
|
+
profile_id,
|
|
98
|
+
)
|
|
99
|
+
result["persona_built"] = bool(pa.get("built", False))
|
|
100
|
+
except Exception as exc:
|
|
101
|
+
logger.debug("Persona roll-up failed (non-fatal): %s", exc)
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
|
|
52
105
|
class ConsolidationEngine:
|
|
53
106
|
"""Sleep-time memory consolidation with 6-step cycle.
|
|
54
107
|
|
|
@@ -254,21 +307,27 @@ class ConsolidationEngine:
|
|
|
254
307
|
activation. Takes ~200-800ms, runs on daemon thread, zero impact
|
|
255
308
|
on store/recall latency.
|
|
256
309
|
"""
|
|
257
|
-
if self._graph_analyzer is None:
|
|
258
|
-
return
|
|
259
310
|
analyzer = self._graph_analyzer
|
|
260
311
|
pid = profile_id
|
|
312
|
+
db = self._db
|
|
313
|
+
summarizer = self._summarizer
|
|
261
314
|
|
|
262
315
|
def _run() -> None:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
316
|
+
if analyzer is not None:
|
|
317
|
+
try:
|
|
318
|
+
result = analyzer.compute_and_store(pid)
|
|
319
|
+
logger.info(
|
|
320
|
+
"Background graph analysis complete: %d nodes, "
|
|
321
|
+
"%d communities",
|
|
322
|
+
result.get("node_count", 0),
|
|
323
|
+
result.get("community_count", 0),
|
|
324
|
+
)
|
|
325
|
+
except Exception as exc:
|
|
326
|
+
logger.debug(
|
|
327
|
+
"Background graph analysis failed (non-fatal): %s", exc,
|
|
328
|
+
)
|
|
329
|
+
# Wave Q: entity-community backbone + summaries (Q2/Q3 spine).
|
|
330
|
+
_recompute_entity_communities(db, pid, summarizer)
|
|
272
331
|
|
|
273
332
|
t = threading.Thread(target=_run, daemon=True, name="graph-analysis-bg")
|
|
274
333
|
t.start()
|
|
@@ -510,15 +569,21 @@ class ConsolidationEngine:
|
|
|
510
569
|
fact_id, profile_id,
|
|
511
570
|
)
|
|
512
571
|
|
|
513
|
-
# Fallback: direct SQL check
|
|
572
|
+
# Fallback: direct SQL check. Must consider BOTH valid_until (valid-time
|
|
573
|
+
# expiry) AND system_expired_at (transaction-time expiry) — a fact that
|
|
574
|
+
# was invalidated/erased sets system_expired_at, and ignoring it would
|
|
575
|
+
# let a GDPR-erased/superseded fact be promoted back into warm lifecycle.
|
|
514
576
|
rows = self._db.execute(
|
|
515
|
-
"SELECT valid_until FROM fact_temporal_validity "
|
|
577
|
+
"SELECT valid_until, system_expired_at FROM fact_temporal_validity "
|
|
516
578
|
"WHERE fact_id = ? AND profile_id = ?",
|
|
517
579
|
(fact_id, profile_id),
|
|
518
580
|
)
|
|
519
581
|
if not rows:
|
|
520
582
|
return True # No temporal record = valid
|
|
521
|
-
|
|
583
|
+
row = dict(rows[0])
|
|
584
|
+
if row.get("system_expired_at") is not None:
|
|
585
|
+
return False # transaction-time expired (invalidated/erased)
|
|
586
|
+
valid_until = row.get("valid_until")
|
|
522
587
|
if valid_until is None:
|
|
523
588
|
return True # Open-ended validity
|
|
524
589
|
try:
|
|
@@ -555,14 +620,22 @@ class ConsolidationEngine:
|
|
|
555
620
|
def _step5_recompute_graph(
|
|
556
621
|
self, profile_id: str,
|
|
557
622
|
) -> dict[str, Any]:
|
|
558
|
-
"""Recompute PageRank + communities. Delegates to GraphAnalyzer.
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
623
|
+
"""Recompute PageRank + communities. Delegates to GraphAnalyzer.
|
|
624
|
+
|
|
625
|
+
Wave Q: also rebuilds the entity-community backbone (Q2/Q3 spine).
|
|
626
|
+
"""
|
|
627
|
+
result: dict[str, Any] = {"node_count": 0, "community_count": 0}
|
|
628
|
+
if self._graph_analyzer is not None:
|
|
629
|
+
try:
|
|
630
|
+
result = self._graph_analyzer.compute_and_store(profile_id)
|
|
631
|
+
except Exception as exc:
|
|
632
|
+
logger.warning("Graph recompute failed: %s", exc)
|
|
633
|
+
ec = _recompute_entity_communities(
|
|
634
|
+
self._db, profile_id, self._summarizer,
|
|
635
|
+
)
|
|
636
|
+
result["entity_community_count"] = ec.get("community_count", 0)
|
|
637
|
+
result["community_summaries"] = ec.get("summaries_written", 0)
|
|
638
|
+
return result
|
|
566
639
|
|
|
567
640
|
# ------------------------------------------------------------------
|
|
568
641
|
# Step 6: Derive Associations
|
|
@@ -65,6 +65,18 @@ class CacheEntry:
|
|
|
65
65
|
provenance: str = "tool_observation"
|
|
66
66
|
computed_at: int = 0
|
|
67
67
|
byte_size: int = 0
|
|
68
|
+
profile_id: str = "default"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _active_profile_fallback(home: Path) -> str:
|
|
72
|
+
"""Resolve the active profile for the cache reader hot path (stdlib only,
|
|
73
|
+
never raises). Two profiles can share a session_id, so cached context MUST
|
|
74
|
+
be keyed by profile or one tenant reads another's context."""
|
|
75
|
+
try:
|
|
76
|
+
raw = (home / "profiles.json").read_text(encoding="utf-8")
|
|
77
|
+
return json.loads(raw).get("active_profile", "default") or "default"
|
|
78
|
+
except Exception:
|
|
79
|
+
return "default"
|
|
68
80
|
|
|
69
81
|
|
|
70
82
|
# ---------------------------------------------------------------------------
|
|
@@ -199,9 +211,24 @@ class ContextCache:
|
|
|
199
211
|
return conn
|
|
200
212
|
|
|
201
213
|
def _bootstrap_schema_and_meta(self) -> None:
|
|
214
|
+
# Isolation: cached context is keyed by profile so two tenants sharing a
|
|
215
|
+
# session_id cannot read each other's context. Older cache files lack
|
|
216
|
+
# the profile_id column — the cache is ephemeral (120s TTL), so drop and
|
|
217
|
+
# recreate rather than run a rebuild migration.
|
|
218
|
+
try:
|
|
219
|
+
cols = {
|
|
220
|
+
r[1] for r in self._write_conn.execute(
|
|
221
|
+
"PRAGMA table_info(context_entries)"
|
|
222
|
+
).fetchall()
|
|
223
|
+
}
|
|
224
|
+
if cols and "profile_id" not in cols:
|
|
225
|
+
self._write_conn.execute("DROP TABLE context_entries")
|
|
226
|
+
except sqlite3.Error: # pragma: no cover — defensive
|
|
227
|
+
pass
|
|
202
228
|
self._write_conn.executescript(
|
|
203
229
|
"""
|
|
204
230
|
CREATE TABLE IF NOT EXISTS context_entries (
|
|
231
|
+
profile_id TEXT NOT NULL DEFAULT 'default',
|
|
205
232
|
session_id TEXT NOT NULL,
|
|
206
233
|
topic_sig TEXT NOT NULL,
|
|
207
234
|
content TEXT NOT NULL,
|
|
@@ -209,11 +236,11 @@ class ContextCache:
|
|
|
209
236
|
provenance TEXT NOT NULL DEFAULT 'tool_observation',
|
|
210
237
|
computed_at INTEGER NOT NULL,
|
|
211
238
|
byte_size INTEGER NOT NULL,
|
|
212
|
-
PRIMARY KEY (session_id, topic_sig)
|
|
239
|
+
PRIMARY KEY (profile_id, session_id, topic_sig)
|
|
213
240
|
) WITHOUT ROWID;
|
|
214
241
|
|
|
215
242
|
CREATE INDEX IF NOT EXISTS idx_ctx_session_time
|
|
216
|
-
ON context_entries(session_id, computed_at);
|
|
243
|
+
ON context_entries(profile_id, session_id, computed_at);
|
|
217
244
|
CREATE INDEX IF NOT EXISTS idx_ctx_time
|
|
218
245
|
ON context_entries(computed_at);
|
|
219
246
|
|
|
@@ -263,26 +290,38 @@ class ContextCache:
|
|
|
263
290
|
self._write_conn.execute(
|
|
264
291
|
"""
|
|
265
292
|
INSERT OR REPLACE INTO context_entries
|
|
266
|
-
(session_id, topic_sig, content, fact_ids,
|
|
293
|
+
(profile_id, session_id, topic_sig, content, fact_ids,
|
|
267
294
|
provenance, computed_at, byte_size)
|
|
268
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
295
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
269
296
|
""",
|
|
270
|
-
(entry.session_id, entry.topic_sig,
|
|
271
|
-
entry.provenance, computed_at, byte_size),
|
|
297
|
+
(entry.profile_id or "default", entry.session_id, entry.topic_sig,
|
|
298
|
+
content, fact_ids_json, entry.provenance, computed_at, byte_size),
|
|
272
299
|
)
|
|
273
300
|
|
|
274
301
|
# -- Cleanup ------------------------------------------------------------
|
|
275
302
|
|
|
276
303
|
def cleanup_session(
|
|
277
304
|
self, session_id: str, *, older_than: int = CLEANUP_HORIZON_SECONDS,
|
|
305
|
+
profile_id: str | None = None,
|
|
278
306
|
) -> int:
|
|
279
|
-
"""Delete rows for ``session_id`` older than ``older_than`` seconds.
|
|
307
|
+
"""Delete rows for ``session_id`` older than ``older_than`` seconds.
|
|
308
|
+
|
|
309
|
+
When ``profile_id`` is given the delete is tenant-scoped so a cleanup on
|
|
310
|
+
a shared session_id cannot wipe another profile's cached context.
|
|
311
|
+
"""
|
|
280
312
|
cutoff = int(time.time()) - older_than
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
313
|
+
if profile_id is not None:
|
|
314
|
+
cur = self._write_conn.execute(
|
|
315
|
+
"DELETE FROM context_entries "
|
|
316
|
+
"WHERE profile_id=? AND session_id=? AND computed_at < ?",
|
|
317
|
+
(profile_id, session_id, cutoff),
|
|
318
|
+
)
|
|
319
|
+
else:
|
|
320
|
+
cur = self._write_conn.execute(
|
|
321
|
+
"DELETE FROM context_entries "
|
|
322
|
+
"WHERE session_id=? AND computed_at < ?",
|
|
323
|
+
(session_id, cutoff),
|
|
324
|
+
)
|
|
286
325
|
return cur.rowcount
|
|
287
326
|
|
|
288
327
|
def cleanup_global_lru(self) -> int:
|
|
@@ -309,17 +348,17 @@ class ContextCache:
|
|
|
309
348
|
target = int(MAX_BYTES * 0.9)
|
|
310
349
|
while total > target:
|
|
311
350
|
rows = self._write_conn.execute(
|
|
312
|
-
"SELECT session_id, topic_sig, byte_size "
|
|
351
|
+
"SELECT profile_id, session_id, topic_sig, byte_size "
|
|
313
352
|
"FROM context_entries "
|
|
314
353
|
"ORDER BY computed_at ASC LIMIT 100",
|
|
315
354
|
).fetchall()
|
|
316
355
|
if not rows: # pragma: no cover — reached only if table empties mid-sweep
|
|
317
356
|
break
|
|
318
|
-
for sess, sig, size in rows:
|
|
357
|
+
for pid, sess, sig, size in rows:
|
|
319
358
|
self._write_conn.execute(
|
|
320
359
|
"DELETE FROM context_entries "
|
|
321
|
-
"WHERE session_id=? AND topic_sig=?",
|
|
322
|
-
(sess, sig),
|
|
360
|
+
"WHERE profile_id=? AND session_id=? AND topic_sig=?",
|
|
361
|
+
(pid, sess, sig),
|
|
323
362
|
)
|
|
324
363
|
deleted += 1
|
|
325
364
|
total -= size
|
|
@@ -345,6 +384,7 @@ def read_entry_fast(
|
|
|
345
384
|
*,
|
|
346
385
|
db_path: Path | None = None,
|
|
347
386
|
home_dir: Path | None = None,
|
|
387
|
+
profile_id: str | None = None,
|
|
348
388
|
) -> CacheEntry | None:
|
|
349
389
|
"""Hot-path reader used by the UserPromptSubmit hook.
|
|
350
390
|
|
|
@@ -397,14 +437,17 @@ def read_entry_fast(
|
|
|
397
437
|
return None
|
|
398
438
|
|
|
399
439
|
now = int(time.time())
|
|
440
|
+
# Scope to the active profile so a shared session_id cannot read
|
|
441
|
+
# another tenant's cached context.
|
|
442
|
+
pid = profile_id or _active_profile_fallback(home)
|
|
400
443
|
row = conn.execute(
|
|
401
444
|
"""
|
|
402
445
|
SELECT content, fact_ids, provenance, computed_at, byte_size
|
|
403
446
|
FROM context_entries
|
|
404
|
-
WHERE session_id=? AND topic_sig=?
|
|
447
|
+
WHERE profile_id=? AND session_id=? AND topic_sig=?
|
|
405
448
|
AND computed_at > ?
|
|
406
449
|
""",
|
|
407
|
-
(session_id, topic_sig, now - TTL_SECONDS),
|
|
450
|
+
(pid, session_id, topic_sig, now - TTL_SECONDS),
|
|
408
451
|
).fetchone()
|
|
409
452
|
finally:
|
|
410
453
|
try:
|
|
@@ -65,6 +65,21 @@ def _embedding_backend_order() -> tuple[str, str]:
|
|
|
65
65
|
return ("onnx", "pytorch")
|
|
66
66
|
|
|
67
67
|
|
|
68
|
+
# H-02 (3.7.9): trust_remote_code=_trusts_remote_code(name) runs arbitrary Python from the model
|
|
69
|
+
# repository at load time. Restrict it to the pinned models SLM ships that
|
|
70
|
+
# genuinely need custom modeling code (nomic-embed). Any other model — including
|
|
71
|
+
# one swapped into config by a write-path attacker — loads with
|
|
72
|
+
# trust_remote_code=False and therefore cannot execute repo code.
|
|
73
|
+
_TRUSTED_REMOTE_CODE_MODELS = frozenset({
|
|
74
|
+
"nomic-ai/nomic-embed-text-v1.5",
|
|
75
|
+
"nomic-ai/nomic-embed-text-v1",
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _trusts_remote_code(model_name: str) -> bool:
|
|
80
|
+
return model_name in _TRUSTED_REMOTE_CODE_MODELS
|
|
81
|
+
|
|
82
|
+
|
|
68
83
|
def _load_embedding_model(name: str) -> tuple:
|
|
69
84
|
"""Load embedding model. ONNX CPU-only first, PyTorch fallback.
|
|
70
85
|
|
|
@@ -84,14 +99,14 @@ def _load_embedding_model(name: str) -> tuple:
|
|
|
84
99
|
m = SentenceTransformer(
|
|
85
100
|
name,
|
|
86
101
|
backend="onnx",
|
|
87
|
-
trust_remote_code=
|
|
102
|
+
trust_remote_code=_trusts_remote_code(name),
|
|
88
103
|
model_kwargs={"provider": "CPUExecutionProvider"},
|
|
89
104
|
)
|
|
90
105
|
else:
|
|
91
106
|
import torch
|
|
92
107
|
with torch.inference_mode():
|
|
93
108
|
m = SentenceTransformer(
|
|
94
|
-
name, trust_remote_code=
|
|
109
|
+
name, trust_remote_code=_trusts_remote_code(name), device="cpu",
|
|
95
110
|
)
|
|
96
111
|
return m, backend
|
|
97
112
|
except Exception:
|
|
@@ -295,7 +295,13 @@ class EmbeddingService:
|
|
|
295
295
|
never hangs indefinitely on cold model loads or network issues.
|
|
296
296
|
"""
|
|
297
297
|
with self._lock:
|
|
298
|
-
|
|
298
|
+
# Only an explicit terminal disable (``False``) short-circuits. A
|
|
299
|
+
# ``None`` availability is the recall-health self-heal's "re-probe"
|
|
300
|
+
# signal (recall_health._heal_embedder) — it must fall through and
|
|
301
|
+
# respawn the worker, matching OllamaEmbedder's tri-state
|
|
302
|
+
# convention. Using ``not self._available`` here bricked the local
|
|
303
|
+
# worker on the first heal tick, because ``None`` is falsy.
|
|
304
|
+
if self._available is False:
|
|
299
305
|
return None
|
|
300
306
|
# Worker recycling: restart after N requests to prevent
|
|
301
307
|
# C++ allocator fragmentation over long-running sessions.
|
|
@@ -348,6 +354,11 @@ class EmbeddingService:
|
|
|
348
354
|
self._available = False
|
|
349
355
|
self._kill_worker()
|
|
350
356
|
return None
|
|
357
|
+
# A successful embed proves the worker is healthy, so clear any
|
|
358
|
+
# transient/``None`` availability left by a self-heal re-probe
|
|
359
|
+
# back to a definite ``True``. Without this the flag lingers at
|
|
360
|
+
# ``None`` and the next ``not``-style check elsewhere re-blocks.
|
|
361
|
+
self._available = True
|
|
351
362
|
self._reset_idle_timer()
|
|
352
363
|
self._request_count += 1
|
|
353
364
|
return resp["vectors"]
|