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
|
@@ -276,6 +276,18 @@ class MemoryEngine:
|
|
|
276
276
|
)
|
|
277
277
|
self._llm = None
|
|
278
278
|
|
|
279
|
+
# H-03 (3.7.9): surface mode-capability degradation explicitly at
|
|
280
|
+
# startup. validate_mode_config existed but was never called, so Mode B
|
|
281
|
+
# silently using rule-based extraction after Ollama vanished (update,
|
|
282
|
+
# restart, port conflict) went unwarned. Each mode only checks its own
|
|
283
|
+
# capabilities, so passing the single llm-availability signal is safe.
|
|
284
|
+
from superlocalmemory.core.modes import validate_mode_config
|
|
285
|
+
_llm_up = getattr(self, "_llm", None) is not None
|
|
286
|
+
for _warning in validate_mode_config(
|
|
287
|
+
self._config.mode, has_ollama=_llm_up, has_cloud_llm=_llm_up,
|
|
288
|
+
):
|
|
289
|
+
logger.warning("Mode config: %s", _warning)
|
|
290
|
+
|
|
279
291
|
from superlocalmemory.trust.scorer import TrustScorer
|
|
280
292
|
from superlocalmemory.trust.provenance import ProvenanceTracker
|
|
281
293
|
from superlocalmemory.compliance.eu_ai_act import EUAIActChecker
|
|
@@ -374,7 +386,9 @@ class MemoryEngine:
|
|
|
374
386
|
return
|
|
375
387
|
|
|
376
388
|
base_dir = self._config.base_dir
|
|
377
|
-
|
|
389
|
+
# Only drain items enqueued under THIS engine's profile — a queued
|
|
390
|
+
# memory must never materialize under a profile it was not written for.
|
|
391
|
+
pending = get_pending(base_dir, limit=20, profile_id=self.profile_id)
|
|
378
392
|
if not pending:
|
|
379
393
|
return
|
|
380
394
|
|
|
@@ -599,6 +613,7 @@ class MemoryEngine:
|
|
|
599
613
|
*,
|
|
600
614
|
include_global: bool | None = None,
|
|
601
615
|
include_shared: bool | None = None,
|
|
616
|
+
window: str | tuple[str, str] | None = None,
|
|
602
617
|
) -> RecallResponse:
|
|
603
618
|
"""Recall relevant facts for a query.
|
|
604
619
|
|
|
@@ -650,6 +665,7 @@ class MemoryEngine:
|
|
|
650
665
|
fast=fast,
|
|
651
666
|
include_global=include_global,
|
|
652
667
|
include_shared=include_shared,
|
|
668
|
+
window=window,
|
|
653
669
|
)
|
|
654
670
|
except Exception as exc:
|
|
655
671
|
from superlocalmemory.infra.local_diagnostics import record_operation
|
|
@@ -11,9 +11,13 @@ implementation of queryable projection and complete derivation.
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import hashlib
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
14
16
|
import uuid
|
|
15
17
|
from typing import TYPE_CHECKING
|
|
16
18
|
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
17
21
|
from superlocalmemory.core.ingestion_command import (
|
|
18
22
|
IngestionCommand,
|
|
19
23
|
IngestionOperation,
|
|
@@ -30,6 +34,21 @@ if TYPE_CHECKING:
|
|
|
30
34
|
_PREBUILT_FACT_KEY = "_slm_prebuilt_fact_v1"
|
|
31
35
|
|
|
32
36
|
|
|
37
|
+
def _pii_redaction_enabled(engine: "MemoryEngine") -> bool:
|
|
38
|
+
"""C4: opt-in PII redaction on ingest.
|
|
39
|
+
|
|
40
|
+
On when the engine config sets ``pii_redaction`` truthy OR the
|
|
41
|
+
``SLM_PII_REDACTION`` env var is set (1/on/true/yes). Default OFF — personal
|
|
42
|
+
use is unchanged; team/company operators opt in.
|
|
43
|
+
"""
|
|
44
|
+
cfg = getattr(engine, "_config", None)
|
|
45
|
+
if cfg is not None and getattr(cfg, "pii_redaction", False):
|
|
46
|
+
return True
|
|
47
|
+
return os.environ.get("SLM_PII_REDACTION", "").strip().lower() in (
|
|
48
|
+
"1", "on", "true", "yes",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
33
52
|
def content_passes_admission(content: str) -> bool:
|
|
34
53
|
"""Return whether raw content is eligible to reach durable ingestion.
|
|
35
54
|
|
|
@@ -140,6 +159,16 @@ def canonical_store(
|
|
|
140
159
|
error=ValueError("content rejected by local admission policy"),
|
|
141
160
|
)
|
|
142
161
|
return []
|
|
162
|
+
# C4: opt-in PII redaction. When enabled (config.pii_redaction or
|
|
163
|
+
# SLM_PII_REDACTION), scrub personal identifiers BEFORE the content is
|
|
164
|
+
# extracted, embedded, or persisted — nothing sensitive ever reaches disk.
|
|
165
|
+
if _pii_redaction_enabled(engine):
|
|
166
|
+
from superlocalmemory.core.pii import redact_pii
|
|
167
|
+
|
|
168
|
+
scrubbed, n_pii = redact_pii(content)
|
|
169
|
+
if n_pii:
|
|
170
|
+
content = scrubbed
|
|
171
|
+
logger.info("PII redaction: scrubbed %d identifier(s) on ingest", n_pii)
|
|
143
172
|
try:
|
|
144
173
|
command = build_engine_ingestion_command(engine)
|
|
145
174
|
receipt = command.submit(IngestionRequest(
|
|
@@ -588,6 +588,19 @@ def init_retrieval(
|
|
|
588
588
|
except Exception as exc:
|
|
589
589
|
logger.debug("Forgetting filter registration failed: %s", exc)
|
|
590
590
|
|
|
591
|
+
# Phase 4 (T1): Register bi-temporal validity filter. Drops superseded /
|
|
592
|
+
# system-invalidated facts from retrieval so contradicted memories never
|
|
593
|
+
# resurface. Pure SQL, safe in all modes; no-op until a fact is invalidated.
|
|
594
|
+
try:
|
|
595
|
+
from superlocalmemory.retrieval.temporal_validity_filter import (
|
|
596
|
+
register_temporal_validity_filter,
|
|
597
|
+
)
|
|
598
|
+
register_temporal_validity_filter(
|
|
599
|
+
engine._registry, db, config.temporal_validator,
|
|
600
|
+
)
|
|
601
|
+
except Exception as exc:
|
|
602
|
+
logger.debug("Temporal validity filter registration failed: %s", exc)
|
|
603
|
+
|
|
591
604
|
return engine
|
|
592
605
|
|
|
593
606
|
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Entity-community backbone (Wave Q) — the single principled clustering spine.
|
|
6
|
+
|
|
7
|
+
Best-in-market memory graphs (GraphRAG, Graphiti/Zep) cluster the ENTITY
|
|
8
|
+
graph, not the raw fact/chunk graph. SLM's fact graph is capped-sparse
|
|
9
|
+
(5 edges/entity), so a fact-level clustering fragments. This module builds an
|
|
10
|
+
entity co-occurrence graph (two entities are linked when they appear together
|
|
11
|
+
in a fact, weighted by co-occurrence count) and runs Louvain community
|
|
12
|
+
detection over it — the correct target and, since entities are far fewer than
|
|
13
|
+
facts, a cheaper computation.
|
|
14
|
+
|
|
15
|
+
The resulting entity communities are the shared backbone for:
|
|
16
|
+
- Q2 community summaries (one synthesized report per community), and
|
|
17
|
+
- Q3 progressive abstraction (scenario/persona tiers + drill-down).
|
|
18
|
+
|
|
19
|
+
Runs in the background (consolidation lane), never on the hot recall path.
|
|
20
|
+
Fail-open and idempotent: a recompute fully replaces a profile's rows.
|
|
21
|
+
|
|
22
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
23
|
+
License: AGPL-3.0-or-later
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import logging
|
|
30
|
+
from collections import Counter, defaultdict
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class EntityCommunityBuilder:
|
|
37
|
+
"""Build + persist entity communities via Louvain over co-occurrence."""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
db: Any,
|
|
42
|
+
min_community_size: int = 2,
|
|
43
|
+
resolution: float = 1.0,
|
|
44
|
+
seed: int = 42,
|
|
45
|
+
) -> None:
|
|
46
|
+
self._db = db
|
|
47
|
+
self._min_size = max(2, int(min_community_size))
|
|
48
|
+
self._resolution = float(resolution)
|
|
49
|
+
self._seed = int(seed)
|
|
50
|
+
|
|
51
|
+
# ------------------------------------------------------------------
|
|
52
|
+
# Detection
|
|
53
|
+
# ------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
def _cooccurrence(self, profile_id: str) -> Counter:
|
|
56
|
+
"""Count entity pairs that co-occur within a fact (per profile)."""
|
|
57
|
+
rows = self._db.execute(
|
|
58
|
+
"SELECT canonical_entities_json FROM atomic_facts "
|
|
59
|
+
"WHERE profile_id = ?",
|
|
60
|
+
(profile_id,),
|
|
61
|
+
)
|
|
62
|
+
weights: Counter = Counter()
|
|
63
|
+
for row in rows:
|
|
64
|
+
raw = dict(row).get("canonical_entities_json")
|
|
65
|
+
if not raw:
|
|
66
|
+
continue
|
|
67
|
+
try:
|
|
68
|
+
parsed = json.loads(raw)
|
|
69
|
+
except (ValueError, TypeError):
|
|
70
|
+
continue
|
|
71
|
+
if not isinstance(parsed, list):
|
|
72
|
+
continue
|
|
73
|
+
ents = sorted({str(e).strip() for e in parsed if str(e).strip()})
|
|
74
|
+
for i in range(len(ents)):
|
|
75
|
+
for j in range(i + 1, len(ents)):
|
|
76
|
+
weights[(ents[i], ents[j])] += 1
|
|
77
|
+
return weights
|
|
78
|
+
|
|
79
|
+
def detect(self, profile_id: str) -> dict[str, int]:
|
|
80
|
+
"""Return {entity_id -> community_id}; communities below min size drop."""
|
|
81
|
+
weights = self._cooccurrence(profile_id)
|
|
82
|
+
if not weights:
|
|
83
|
+
return {}
|
|
84
|
+
|
|
85
|
+
import networkx as nx
|
|
86
|
+
|
|
87
|
+
g = nx.Graph()
|
|
88
|
+
for (a, b), w in weights.items():
|
|
89
|
+
g.add_edge(a, b, weight=w)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
from networkx.algorithms.community import louvain_communities
|
|
93
|
+
|
|
94
|
+
communities = louvain_communities(
|
|
95
|
+
g, weight="weight", resolution=self._resolution, seed=self._seed,
|
|
96
|
+
)
|
|
97
|
+
except Exception as exc: # pragma: no cover - fallback path
|
|
98
|
+
logger.debug(
|
|
99
|
+
"Louvain unavailable/failed (%s); using connected components",
|
|
100
|
+
exc,
|
|
101
|
+
)
|
|
102
|
+
communities = nx.connected_components(g)
|
|
103
|
+
|
|
104
|
+
result: dict[str, int] = {}
|
|
105
|
+
cid = 0
|
|
106
|
+
for comm in communities:
|
|
107
|
+
members = list(comm)
|
|
108
|
+
if len(members) < self._min_size:
|
|
109
|
+
continue
|
|
110
|
+
for node in members:
|
|
111
|
+
result[node] = cid
|
|
112
|
+
cid += 1
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
# ------------------------------------------------------------------
|
|
116
|
+
# Persistence
|
|
117
|
+
# ------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
def compute_and_store(self, profile_id: str) -> dict[str, int]:
|
|
120
|
+
"""Detect communities and replace this profile's stored rows."""
|
|
121
|
+
mapping = self.detect(profile_id)
|
|
122
|
+
try:
|
|
123
|
+
self._db.execute(
|
|
124
|
+
"DELETE FROM entity_communities WHERE profile_id = ?",
|
|
125
|
+
(profile_id,),
|
|
126
|
+
)
|
|
127
|
+
for entity_id, community_id in mapping.items():
|
|
128
|
+
self._db.execute(
|
|
129
|
+
"INSERT OR REPLACE INTO entity_communities "
|
|
130
|
+
"(profile_id, entity_id, community_id, computed_at) "
|
|
131
|
+
"VALUES (?, ?, ?, datetime('now'))",
|
|
132
|
+
(profile_id, entity_id, community_id),
|
|
133
|
+
)
|
|
134
|
+
except Exception as exc:
|
|
135
|
+
logger.debug("entity_communities persist failed: %s", exc)
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
"entity_count": len(mapping),
|
|
139
|
+
"community_count": len(set(mapping.values())),
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# ------------------------------------------------------------------
|
|
143
|
+
# Read API
|
|
144
|
+
# ------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
def get_communities(self, profile_id: str) -> dict[int, list[str]]:
|
|
147
|
+
"""Return {community_id -> [entity_id, ...]} for a profile."""
|
|
148
|
+
try:
|
|
149
|
+
rows = self._db.execute(
|
|
150
|
+
"SELECT entity_id, community_id FROM entity_communities "
|
|
151
|
+
"WHERE profile_id = ? ORDER BY community_id",
|
|
152
|
+
(profile_id,),
|
|
153
|
+
)
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
logger.debug("get_communities failed: %s", exc)
|
|
156
|
+
return {}
|
|
157
|
+
out: dict[int, list[str]] = defaultdict(list)
|
|
158
|
+
for row in rows:
|
|
159
|
+
d = dict(row)
|
|
160
|
+
out[int(d["community_id"])].append(str(d["entity_id"]))
|
|
161
|
+
return dict(out)
|
|
162
|
+
|
|
163
|
+
def get_community_for_entity(
|
|
164
|
+
self, entity_id: str, profile_id: str,
|
|
165
|
+
) -> int | None:
|
|
166
|
+
"""Return the community id for one entity, or None."""
|
|
167
|
+
try:
|
|
168
|
+
rows = self._db.execute(
|
|
169
|
+
"SELECT community_id FROM entity_communities "
|
|
170
|
+
"WHERE profile_id = ? AND entity_id = ?",
|
|
171
|
+
(profile_id, entity_id),
|
|
172
|
+
)
|
|
173
|
+
except Exception as exc:
|
|
174
|
+
logger.debug("get_community_for_entity failed: %s", exc)
|
|
175
|
+
return None
|
|
176
|
+
for row in rows:
|
|
177
|
+
return int(dict(row)["community_id"])
|
|
178
|
+
return None
|
|
@@ -211,6 +211,43 @@ class GraphAnalyzer:
|
|
|
211
211
|
result[node] = comm_id
|
|
212
212
|
return result
|
|
213
213
|
|
|
214
|
+
def detect_communities_louvain(
|
|
215
|
+
self,
|
|
216
|
+
graph: Any = None,
|
|
217
|
+
profile_id: str = "",
|
|
218
|
+
) -> dict[str, int]:
|
|
219
|
+
"""Detect communities via Louvain (modularity-optimizing).
|
|
220
|
+
|
|
221
|
+
Higher quality than Label Propagation (deterministic with a seed,
|
|
222
|
+
no giant-community collapse), pure-Python via networkx — no extra
|
|
223
|
+
binary deps. Falls back to Label Propagation if unavailable.
|
|
224
|
+
"""
|
|
225
|
+
import networkx as nx
|
|
226
|
+
|
|
227
|
+
if graph is None:
|
|
228
|
+
graph = self._build_networkx_graph(profile_id)
|
|
229
|
+
if graph.number_of_nodes() == 0:
|
|
230
|
+
return {}
|
|
231
|
+
|
|
232
|
+
undirected = graph.to_undirected()
|
|
233
|
+
try:
|
|
234
|
+
from networkx.algorithms.community import louvain_communities
|
|
235
|
+
|
|
236
|
+
communities = louvain_communities(
|
|
237
|
+
undirected, weight="weight", seed=42,
|
|
238
|
+
)
|
|
239
|
+
except Exception as exc:
|
|
240
|
+
logger.debug(
|
|
241
|
+
"Louvain unavailable/failed (%s); using Label Propagation", exc,
|
|
242
|
+
)
|
|
243
|
+
return self.detect_communities(graph, profile_id)
|
|
244
|
+
|
|
245
|
+
result: dict[str, int] = {}
|
|
246
|
+
for comm_id, community in enumerate(communities):
|
|
247
|
+
for node in community:
|
|
248
|
+
result[node] = comm_id
|
|
249
|
+
return result
|
|
250
|
+
|
|
214
251
|
# ── v3.4.1: Leiden Community Detection ────────────────────────
|
|
215
252
|
|
|
216
253
|
def detect_communities_leiden(
|
|
@@ -234,9 +271,9 @@ class GraphAnalyzer:
|
|
|
234
271
|
import igraph
|
|
235
272
|
except ImportError:
|
|
236
273
|
logger.info(
|
|
237
|
-
"leidenalg not installed, using
|
|
274
|
+
"leidenalg not installed, using Louvain fallback",
|
|
238
275
|
)
|
|
239
|
-
return self.
|
|
276
|
+
return self.detect_communities_louvain(graph, profile_id)
|
|
240
277
|
|
|
241
278
|
# Convert DiGraph -> undirected -> igraph
|
|
242
279
|
undirected = graph.to_undirected()
|
|
@@ -350,8 +350,12 @@ def _cap_node_degree(
|
|
|
350
350
|
Algorithm (single-pass window function — no Python loops):
|
|
351
351
|
1. ROW_NUMBER() OVER (PARTITION BY source_id ORDER BY weight DESC) ranks
|
|
352
352
|
every edge per node in one full table scan.
|
|
353
|
-
2.
|
|
354
|
-
|
|
353
|
+
2. Excess edge IDs are collected in a reusable temp table.
|
|
354
|
+
3. A single DELETE statement removes them; rowcount is returned.
|
|
355
|
+
4. The temp table is created once and cleared via DELETE (not DROP) —
|
|
356
|
+
DROP TABLE acquires an EXCLUSIVE lock that conflicts with concurrent
|
|
357
|
+
writers, causing "database is locked". Using CREATE...IF NOT EXISTS
|
|
358
|
+
plus DELETE FROM avoids that conflict while preserving rowcount.
|
|
355
359
|
"""
|
|
356
360
|
# gi-03: cap BOTH out-degree (PARTITION BY source_id) AND in-degree
|
|
357
361
|
# (PARTITION BY target_id). Previously only out-degree was capped, so hub
|
|
@@ -379,9 +383,13 @@ def _cap_node_degree(
|
|
|
379
383
|
)
|
|
380
384
|
return excess
|
|
381
385
|
|
|
382
|
-
#
|
|
383
|
-
|
|
384
|
-
|
|
386
|
+
# CREATE IF NOT EXISTS + DELETE FROM instead of DROP + CREATE.
|
|
387
|
+
# DROP TABLE acquires EXCLUSIVE which conflicts with concurrent writers.
|
|
388
|
+
# CREATE IF NOT EXISTS is idempotent; DELETE FROM clears prior contents.
|
|
389
|
+
c.execute(
|
|
390
|
+
"CREATE TEMP TABLE IF NOT EXISTS _slm_cap_del (edge_id TEXT PRIMARY KEY)"
|
|
391
|
+
)
|
|
392
|
+
c.execute("DELETE FROM _slm_cap_del")
|
|
385
393
|
c.execute(
|
|
386
394
|
"""
|
|
387
395
|
INSERT OR IGNORE INTO _slm_cap_del (edge_id)
|
|
@@ -396,7 +404,6 @@ def _cap_node_degree(
|
|
|
396
404
|
(profile_id, max_degree, max_degree),
|
|
397
405
|
)
|
|
398
406
|
|
|
399
|
-
# Step 2: delete the over-cap edges (single DELETE).
|
|
400
407
|
c.execute(
|
|
401
408
|
"""
|
|
402
409
|
DELETE FROM graph_edges
|
|
@@ -407,8 +414,6 @@ def _cap_node_degree(
|
|
|
407
414
|
)
|
|
408
415
|
deleted = c.rowcount
|
|
409
416
|
|
|
410
|
-
c.execute("DROP TABLE IF EXISTS _slm_cap_del")
|
|
411
|
-
|
|
412
417
|
logger.info(
|
|
413
418
|
"_cap_node_degree: deleted %d low-weight edges (max_degree=%d, in+out capped)",
|
|
414
419
|
deleted, max_degree,
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Fact-augmented key expansion (Phase 4, T3b).
|
|
6
|
+
|
|
7
|
+
Generates *alternate keys* for a fact — synonyms, aliases, and paraphrases —
|
|
8
|
+
that get indexed in ``fact_expansion_fts`` and UNION'd into BM25 retrieval, so a
|
|
9
|
+
query for "automobile" or "the Big Apple" can match a fact that only says "car"
|
|
10
|
+
or "NYC".
|
|
11
|
+
|
|
12
|
+
Two tiers, matching the rest of SLM:
|
|
13
|
+
* Mode A (zero-LLM): pulls the fact's resolved entities' canonical names and
|
|
14
|
+
aliases from SLM's own entity graph — no model, no cost, and it reuses the
|
|
15
|
+
entity resolution already done at ingest.
|
|
16
|
+
* Mode B/C: additionally asks the LLM for a few paraphrase keywords (own
|
|
17
|
+
prompt, fail-open, bounded).
|
|
18
|
+
|
|
19
|
+
Keys already present in the fact's content are dropped — indexing them twice
|
|
20
|
+
adds nothing. Returns a single space-joined string ready for the FTS row.
|
|
21
|
+
|
|
22
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
23
|
+
License: AGPL-3.0-or-later
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import logging
|
|
29
|
+
from typing import TYPE_CHECKING, Any
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from superlocalmemory.storage.database import DatabaseManager
|
|
33
|
+
from superlocalmemory.storage.models import AtomicFact
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
# Bound the LLM enrichment so a pathological response can't bloat the index.
|
|
38
|
+
_MAX_LLM_KEYS = 8
|
|
39
|
+
_MAX_KEY_LEN = 60
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class KeyExpander:
|
|
43
|
+
"""Produces alternate search keys for a fact (T3b)."""
|
|
44
|
+
|
|
45
|
+
__slots__ = ("_db", "_llm")
|
|
46
|
+
|
|
47
|
+
def __init__(self, db: DatabaseManager, llm: Any = None) -> None:
|
|
48
|
+
self._db = db
|
|
49
|
+
self._llm = llm
|
|
50
|
+
|
|
51
|
+
def expand(self, fact: AtomicFact, profile_id: str, mode: str = "a") -> str:
|
|
52
|
+
"""Return space-joined alternate keys for ``fact`` (may be empty)."""
|
|
53
|
+
keys: set[str] = set()
|
|
54
|
+
keys |= self._alias_keys(fact, profile_id)
|
|
55
|
+
if mode in ("b", "c") and self._llm_available():
|
|
56
|
+
keys |= self._llm_keys(fact)
|
|
57
|
+
|
|
58
|
+
content_low = (getattr(fact, "content", "") or "").lower()
|
|
59
|
+
cleaned = {
|
|
60
|
+
k.strip() for k in keys
|
|
61
|
+
if k and k.strip() and len(k.strip()) <= _MAX_KEY_LEN
|
|
62
|
+
and k.strip().lower() not in content_low
|
|
63
|
+
}
|
|
64
|
+
return " ".join(sorted(cleaned))
|
|
65
|
+
|
|
66
|
+
# -- Mode A: entity aliases from SLM's own entity graph ------------------
|
|
67
|
+
|
|
68
|
+
def _alias_keys(self, fact: AtomicFact, profile_id: str) -> set[str]:
|
|
69
|
+
out: set[str] = set()
|
|
70
|
+
for name in (getattr(fact, "canonical_entities", None) or []):
|
|
71
|
+
if not name:
|
|
72
|
+
continue
|
|
73
|
+
try:
|
|
74
|
+
ent = self._db.get_entity_by_name(name, profile_id)
|
|
75
|
+
except Exception:
|
|
76
|
+
ent = None
|
|
77
|
+
if ent is None:
|
|
78
|
+
continue
|
|
79
|
+
canonical = getattr(ent, "canonical_name", None)
|
|
80
|
+
if canonical:
|
|
81
|
+
out.add(canonical)
|
|
82
|
+
entity_id = getattr(ent, "entity_id", None)
|
|
83
|
+
if not entity_id:
|
|
84
|
+
continue
|
|
85
|
+
try:
|
|
86
|
+
for alias in self._db.get_aliases_for_entity(entity_id, profile_id):
|
|
87
|
+
a = getattr(alias, "alias", None)
|
|
88
|
+
if a:
|
|
89
|
+
out.add(a)
|
|
90
|
+
except Exception:
|
|
91
|
+
continue
|
|
92
|
+
return out
|
|
93
|
+
|
|
94
|
+
# -- Mode B/C: LLM paraphrases (own prompt, fail-open) -------------------
|
|
95
|
+
|
|
96
|
+
def _llm_available(self) -> bool:
|
|
97
|
+
if self._llm is None:
|
|
98
|
+
return False
|
|
99
|
+
check = getattr(self._llm, "is_available", None)
|
|
100
|
+
try:
|
|
101
|
+
return bool(check()) if callable(check) else bool(check)
|
|
102
|
+
except Exception:
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
def _llm_keys(self, fact: AtomicFact) -> set[str]:
|
|
106
|
+
content = getattr(fact, "content", "") or ""
|
|
107
|
+
if not content:
|
|
108
|
+
return set()
|
|
109
|
+
prompt = (
|
|
110
|
+
"Give 3-6 short alternative search terms (synonyms, aliases, or "
|
|
111
|
+
"paraphrases) that a person might use to look up the memory below. "
|
|
112
|
+
"Output ONLY a comma-separated list, no numbering, no explanation.\n\n"
|
|
113
|
+
f"Memory: {content}"
|
|
114
|
+
)
|
|
115
|
+
raw = self._invoke_llm(prompt)
|
|
116
|
+
if not raw:
|
|
117
|
+
return set()
|
|
118
|
+
seen: list[str] = []
|
|
119
|
+
for part in raw.replace("\n", ",").split(","):
|
|
120
|
+
p = part.strip()
|
|
121
|
+
if p and p not in seen:
|
|
122
|
+
seen.append(p)
|
|
123
|
+
if len(seen) >= _MAX_LLM_KEYS:
|
|
124
|
+
break
|
|
125
|
+
return set(seen)
|
|
126
|
+
|
|
127
|
+
def _invoke_llm(self, prompt: str) -> str:
|
|
128
|
+
"""Call whatever generation method the injected LLM exposes; fail-open."""
|
|
129
|
+
for meth in ("generate", "complete", "chat"):
|
|
130
|
+
fn = getattr(self._llm, meth, None)
|
|
131
|
+
if callable(fn):
|
|
132
|
+
try:
|
|
133
|
+
out = fn(prompt)
|
|
134
|
+
return out if isinstance(out, str) else str(out or "")
|
|
135
|
+
except Exception as exc:
|
|
136
|
+
logger.debug("KeyExpander LLM (%s) failed: %s", meth, exc)
|
|
137
|
+
return ""
|
|
138
|
+
return ""
|
|
@@ -127,6 +127,7 @@ def run_maintenance(
|
|
|
127
127
|
"sheaf_checked": 0,
|
|
128
128
|
"entity_summaries_consolidated": 0, # V3.4.40
|
|
129
129
|
"orphan_metadata_gc": 0, # v3.6.4 (P1-3)
|
|
130
|
+
"expansion_backfilled": 0, # T3b
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
# P1-3 (embeddings-vector-02): sweep orphaned embedding_metadata left by
|
|
@@ -141,6 +142,28 @@ def run_maintenance(
|
|
|
141
142
|
if not facts:
|
|
142
143
|
return counts
|
|
143
144
|
|
|
145
|
+
# T3b: backfill fact-expansion alt-keys (Mode A, entity-alias based) for
|
|
146
|
+
# facts stored before expansion existed. Bounded per run + skips already-
|
|
147
|
+
# populated and entity-less facts, so it converges without re-work churn.
|
|
148
|
+
try:
|
|
149
|
+
from superlocalmemory.core.key_expander import KeyExpander
|
|
150
|
+
populated = {
|
|
151
|
+
dict(r)["fact_id"]
|
|
152
|
+
for r in db.execute("SELECT DISTINCT fact_id FROM fact_expansion_fts")
|
|
153
|
+
}
|
|
154
|
+
expander = KeyExpander(db)
|
|
155
|
+
for f in facts:
|
|
156
|
+
if counts["expansion_backfilled"] >= 500:
|
|
157
|
+
break
|
|
158
|
+
if f.fact_id in populated or not f.canonical_entities:
|
|
159
|
+
continue
|
|
160
|
+
alt = expander.expand(f, profile_id, mode="a")
|
|
161
|
+
if alt:
|
|
162
|
+
db.upsert_fact_expansion(f.fact_id, alt)
|
|
163
|
+
counts["expansion_backfilled"] += 1
|
|
164
|
+
except Exception as exc: # pragma: no cover — legacy DB / missing FTS
|
|
165
|
+
logger.debug("expansion backfill skipped: %s", exc)
|
|
166
|
+
|
|
144
167
|
# 1a. Backfill: seed uninitialized facts with metadata-aware positions (B+C)
|
|
145
168
|
if config.math.langevin_persist_positions:
|
|
146
169
|
try:
|
|
@@ -109,7 +109,7 @@ MODE_C = ModeCapabilities(
|
|
|
109
109
|
data_stays_local=False,
|
|
110
110
|
description=(
|
|
111
111
|
"FULL POWER — UNRESTRICTED. Best embeddings (text-embedding-3-large, 3072-dim). "
|
|
112
|
-
"Best LLMs (GPT-5
|
|
112
|
+
"Best configured cloud LLMs (e.g. GPT-5, Claude Opus 4). Agentic multi-round retrieval. "
|
|
113
113
|
"Cohere reranker option. No EU restriction. Target: 90%+"
|
|
114
114
|
),
|
|
115
115
|
)
|
|
@@ -59,7 +59,7 @@ def delete_fact_authorized(
|
|
|
59
59
|
if not rows:
|
|
60
60
|
return {"ok": False, "error": f"Memory {fact_id} not found"}
|
|
61
61
|
content_preview = dict(rows[0]).get("content", "")[:80]
|
|
62
|
-
engine._db.delete_fact(fact_id)
|
|
62
|
+
engine._db.delete_fact(fact_id, profile_id=profile_id)
|
|
63
63
|
try:
|
|
64
64
|
from superlocalmemory.core.backend_orchestrator import get_orchestrator
|
|
65
65
|
orchestrator = get_orchestrator()
|
|
@@ -120,7 +120,7 @@ def update_fact_authorized(
|
|
|
120
120
|
updates["fisher_variance"] = fisher_variance
|
|
121
121
|
except Exception as exc:
|
|
122
122
|
logger.warning("UPDATE embedding refresh failed: %s", exc)
|
|
123
|
-
engine._db.update_fact(fact_id, updates)
|
|
123
|
+
engine._db.update_fact(fact_id, updates, profile_id=profile_id)
|
|
124
124
|
try:
|
|
125
125
|
from superlocalmemory.core.backend_orchestrator import get_orchestrator
|
|
126
126
|
orchestrator = get_orchestrator()
|