superlocalmemory 3.7.8 → 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 +69 -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 -1
- 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 +94 -10
- 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/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 +8 -1
- 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 +4 -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 +10 -5
- 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 +182 -57
- 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 +183 -69
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +20 -5
- package/src/superlocalmemory/server/unified_daemon.py +384 -56
- 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_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
|
@@ -13,6 +13,81 @@ from typing import Any
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Nested-group compatibility.
|
|
18
|
+
#
|
|
19
|
+
# The canonical serialized form (``OptimizeConfig.as_dict``) is FLAT
|
|
20
|
+
# (``compress_enabled``, ``compress_mode``, ``cache_enabled``, ...). But
|
|
21
|
+
# hand-authored ``optimize.json`` files — and older doc examples — group
|
|
22
|
+
# settings under nested blocks (``{"compress": {"enabled": true, ...}}``).
|
|
23
|
+
# ``from_dict`` accepts BOTH: be liberal in what we read, strict in what we
|
|
24
|
+
# write (Postel's law). Flat keys, when present, always win over a nested
|
|
25
|
+
# value for the same setting.
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
_NESTED_ALIASES: dict[str, dict[str, str]] = {
|
|
28
|
+
"compress": {
|
|
29
|
+
"enabled": "compress_enabled",
|
|
30
|
+
"mode": "compress_mode",
|
|
31
|
+
"prose": "compress_prose",
|
|
32
|
+
"protect_recent": "compress_protect_recent",
|
|
33
|
+
},
|
|
34
|
+
"cache": {
|
|
35
|
+
"enabled": "cache_enabled",
|
|
36
|
+
"ttl_seconds": "ttl_seconds",
|
|
37
|
+
"semantic": "semantic_enabled",
|
|
38
|
+
"semantic_enabled": "semantic_enabled",
|
|
39
|
+
},
|
|
40
|
+
"proxy": {
|
|
41
|
+
"enabled": "proxy_enabled",
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# compress_mode aliases → canonical value. Any unknown mode normalizes to
|
|
46
|
+
# "safe" (fail-open) rather than crashing ``validate()`` at daemon boot.
|
|
47
|
+
_COMPRESS_MODE_ALIASES: dict[str, str] = {
|
|
48
|
+
"safe": "safe",
|
|
49
|
+
"aggressive": "aggressive",
|
|
50
|
+
"fast": "safe", # legacy alias: "fast" == lightweight == safe
|
|
51
|
+
"lossless": "safe",
|
|
52
|
+
"off": "safe",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _flatten_optimize_dict(d: dict[str, Any]) -> dict[str, Any]:
|
|
57
|
+
"""Return a NEW flat dict, merging nested config groups into flat keys.
|
|
58
|
+
|
|
59
|
+
Explicit flat keys always take precedence over a nested value for the
|
|
60
|
+
same setting. The input dict is never mutated (immutable transform).
|
|
61
|
+
"""
|
|
62
|
+
flat: dict[str, Any] = dict(d)
|
|
63
|
+
for group, mapping in _NESTED_ALIASES.items():
|
|
64
|
+
block = d.get(group)
|
|
65
|
+
if not isinstance(block, dict):
|
|
66
|
+
continue
|
|
67
|
+
for nested_key, flat_key in mapping.items():
|
|
68
|
+
if nested_key in block and flat_key not in d:
|
|
69
|
+
flat[flat_key] = block[nested_key]
|
|
70
|
+
return flat
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _normalize_compress_mode(raw: Any) -> str:
|
|
74
|
+
"""Map a compress_mode value (including legacy aliases) to a valid mode.
|
|
75
|
+
|
|
76
|
+
Unknown values fall back to "safe" with a warning — a single bad enum in
|
|
77
|
+
a hand-authored config must never crash the optimize subsystem at boot.
|
|
78
|
+
"""
|
|
79
|
+
key = str(raw).strip().lower()
|
|
80
|
+
mode = _COMPRESS_MODE_ALIASES.get(key)
|
|
81
|
+
if mode is None:
|
|
82
|
+
logger.warning(
|
|
83
|
+
"optimize.json: unknown compress_mode %r — falling back to 'safe' "
|
|
84
|
+
"(valid: safe, aggressive)",
|
|
85
|
+
raw,
|
|
86
|
+
)
|
|
87
|
+
return "safe"
|
|
88
|
+
return mode
|
|
89
|
+
|
|
90
|
+
|
|
16
91
|
@dataclass
|
|
17
92
|
class TTLConfig:
|
|
18
93
|
"""TTL settings for each cache tier, in seconds."""
|
|
@@ -90,8 +165,12 @@ class OptimizeConfig:
|
|
|
90
165
|
semantic_max_index_entries: int = 10000
|
|
91
166
|
semantic_max_tenants: int = 10000
|
|
92
167
|
|
|
93
|
-
# Compress
|
|
94
|
-
|
|
168
|
+
# Compress. Safe lossless compression (whitespace normalize + JSON minify) is
|
|
169
|
+
# ON by default; it runs proxy-free via the slm_compress tool and never
|
|
170
|
+
# touches code. Lossy prose (LLMLingua) stays gated behind compress_mode=
|
|
171
|
+
# "aggressive" + compress_prose. Set compress_enabled=False (slm optimize off)
|
|
172
|
+
# to disable entirely.
|
|
173
|
+
compress_enabled: bool = True
|
|
95
174
|
compress_mode: str = "safe"
|
|
96
175
|
compress_prose: bool = False
|
|
97
176
|
compress_protect_recent: int = 4
|
|
@@ -144,6 +223,10 @@ class OptimizeConfig:
|
|
|
144
223
|
|
|
145
224
|
@classmethod
|
|
146
225
|
def from_dict(cls, d: dict[str, Any]) -> "OptimizeConfig":
|
|
226
|
+
# Accept nested config groups (compress/cache/proxy) in addition to
|
|
227
|
+
# the canonical flat form. Flat keys win on conflict.
|
|
228
|
+
d = _flatten_optimize_dict(d)
|
|
229
|
+
|
|
147
230
|
ttl_raw = d.get("ttl", {})
|
|
148
231
|
ttl = TTLConfig.from_dict(ttl_raw) if isinstance(ttl_raw, dict) else TTLConfig()
|
|
149
232
|
|
|
@@ -181,8 +264,8 @@ class OptimizeConfig:
|
|
|
181
264
|
),
|
|
182
265
|
semantic_max_index_entries=int(d.get("semantic_max_index_entries", 10000)),
|
|
183
266
|
semantic_max_tenants=int(d.get("semantic_max_tenants", 10000)),
|
|
184
|
-
compress_enabled=bool(d.get("compress_enabled",
|
|
185
|
-
compress_mode=
|
|
267
|
+
compress_enabled=bool(d.get("compress_enabled", True)),
|
|
268
|
+
compress_mode=_normalize_compress_mode(d.get("compress_mode", "safe")),
|
|
186
269
|
compress_prose=bool(d.get("compress_prose", False)),
|
|
187
270
|
compress_protect_recent=int(d.get("compress_protect_recent", 4)),
|
|
188
271
|
ttl_seconds=int(d.get("ttl_seconds", 86400)),
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
2
|
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
3
|
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
-
#
|
|
5
|
-
# ATTRIBUTION: CacheMetrics design pattern adapted from:
|
|
6
|
-
# omnicache_ai/core/metrics.py (MIT License)
|
|
7
4
|
|
|
8
5
|
"""Thread-safe atomic counters for all SLM Optimize operations."""
|
|
9
6
|
|
|
@@ -43,6 +40,7 @@ class MetricsCollector:
|
|
|
43
40
|
self._latency_overhead_ms_sum: float = 0.0
|
|
44
41
|
self._latency_samples: int = 0
|
|
45
42
|
self._compress_runs: int = 0
|
|
43
|
+
self._lossy_compress_runs: int = 0
|
|
46
44
|
self._compress_bytes_original: int = 0
|
|
47
45
|
self._compress_bytes_after: int = 0
|
|
48
46
|
self._cache_size_bytes: int = 0
|
|
@@ -69,18 +67,28 @@ class MetricsCollector:
|
|
|
69
67
|
with self._data_lock:
|
|
70
68
|
self._misses += 1
|
|
71
69
|
|
|
72
|
-
def on_compress(self, tokens_before: int, tokens_after: int) -> None:
|
|
70
|
+
def on_compress(self, tokens_before: int, tokens_after: int, lossy: bool = False) -> None:
|
|
73
71
|
"""Record a compression run. Arguments are word-count proxy estimates from _token_estimate().
|
|
74
72
|
|
|
75
73
|
M-03: consistent naming — these are token estimates, not byte counts.
|
|
76
74
|
Stored in compress_bytes_original/after fields for DB schema compat; unit is word-count.
|
|
75
|
+
|
|
76
|
+
``lossy`` distinguishes a Layer-2 lossy (LLMLingua prose) run from a
|
|
77
|
+
lossless Layer-1/JSON-minify run; tracked in memory (not yet persisted).
|
|
77
78
|
"""
|
|
78
79
|
with self._data_lock:
|
|
79
80
|
self._compress_runs += 1
|
|
81
|
+
if lossy:
|
|
82
|
+
self._lossy_compress_runs += 1
|
|
80
83
|
self._compress_bytes_original += max(0, tokens_before)
|
|
81
84
|
self._compress_bytes_after += max(0, tokens_after)
|
|
82
85
|
self._tokens_saved_compress += max(0, tokens_before - tokens_after)
|
|
83
86
|
|
|
87
|
+
def lossy_compress_runs(self) -> int:
|
|
88
|
+
"""Session-scoped count of lossy (Layer-2) compression runs."""
|
|
89
|
+
with self._data_lock:
|
|
90
|
+
return self._lossy_compress_runs
|
|
91
|
+
|
|
84
92
|
def on_eviction(self) -> None:
|
|
85
93
|
"""Record an eviction."""
|
|
86
94
|
with self._data_lock:
|
|
@@ -135,6 +143,7 @@ class MetricsCollector:
|
|
|
135
143
|
self._latency_overhead_ms_sum = 0.0
|
|
136
144
|
self._latency_samples = 0
|
|
137
145
|
self._compress_runs = 0
|
|
146
|
+
self._lossy_compress_runs = 0
|
|
138
147
|
self._compress_bytes_original = 0
|
|
139
148
|
self._compress_bytes_after = 0
|
|
140
149
|
self._cache_size_bytes = 0
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
2
|
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
3
|
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
-
#
|
|
5
|
-
# ATTRIBUTION: PricingRegistry + stale-detection pattern adapted from:
|
|
6
|
-
# headroom/pricing/registry.py (Apache-2.0)
|
|
7
4
|
|
|
8
5
|
"""Savings estimator — converts MetricsSnapshot to dollar/rupee savings."""
|
|
9
6
|
|
|
@@ -97,6 +97,9 @@ _HOP_BY_HOP = frozenset([
|
|
|
97
97
|
"te", "trailer", "transfer-encoding", "upgrade", "host",
|
|
98
98
|
"x-forwarded-for", "x-forwarded-host", "x-forwarded-proto",
|
|
99
99
|
"x-real-ip", "x-original-forwarded-for",
|
|
100
|
+
# ponytail: httpx decompresses gzip automatically; forwarding this header
|
|
101
|
+
# causes clients to double-decompress → ZlibError. Strip it here.
|
|
102
|
+
"content-encoding",
|
|
100
103
|
])
|
|
101
104
|
|
|
102
105
|
_ANTHROPIC_FORWARD_HEADERS = frozenset([
|
|
@@ -152,6 +155,21 @@ def _redact_headers(headers: dict) -> dict:
|
|
|
152
155
|
}
|
|
153
156
|
|
|
154
157
|
|
|
158
|
+
def _active_profile_for_cache() -> str:
|
|
159
|
+
"""Resolve the active memory profile for cache-tenant scoping.
|
|
160
|
+
|
|
161
|
+
Lazy import keeps the optimize layer free of a hard server dependency; any
|
|
162
|
+
failure falls back to 'default'. The profiles.json active_profile cache is
|
|
163
|
+
kept in sync on every switch, so this is correct for the daemon-mounted
|
|
164
|
+
proxy even outside an HTTP request context.
|
|
165
|
+
"""
|
|
166
|
+
try:
|
|
167
|
+
from superlocalmemory.server.routes.helpers import get_active_profile
|
|
168
|
+
return get_active_profile() or "default"
|
|
169
|
+
except Exception:
|
|
170
|
+
return "default"
|
|
171
|
+
|
|
172
|
+
|
|
155
173
|
def _derive_tenant_id(provider: str, raw_credential: "str | None") -> "str | None":
|
|
156
174
|
"""Derive a per-tenant isolation key from the raw (un-redacted) credential.
|
|
157
175
|
|
|
@@ -160,18 +178,27 @@ def _derive_tenant_id(provider: str, raw_credential: "str | None") -> "str | Non
|
|
|
160
178
|
into CacheManager.check() / .store() so that two users sharing the same
|
|
161
179
|
prompt but using different API keys receive independent cache namespaces.
|
|
162
180
|
|
|
181
|
+
ISOLATION (I-5): the active memory profile is folded into the key so two
|
|
182
|
+
profiles sharing the SAME API key never serve each other's cached LLM
|
|
183
|
+
responses. Cross-profile cache reuse is intentionally sacrificed for
|
|
184
|
+
tenant isolation (the accepted cost tradeoff for a multi-tenant memory DB).
|
|
185
|
+
|
|
163
186
|
Returns None when no credential is present — callers must SKIP caching
|
|
164
187
|
(never collapse to the default tenant) to prevent cross-tenant disclosure.
|
|
165
188
|
|
|
166
|
-
Output: 64-char lowercase hex SHA-256 of
|
|
167
|
-
Provider is folded in so
|
|
168
|
-
if the literal key
|
|
189
|
+
Output: 64-char lowercase hex SHA-256 of
|
|
190
|
+
``f"{profile}:{provider}:{raw_credential}"``. Provider is folded in so
|
|
191
|
+
anthropic:K and openai:K are distinct tenants even if the literal key
|
|
192
|
+
string coincidentally matches.
|
|
169
193
|
"""
|
|
170
194
|
import hashlib as _hashlib
|
|
171
195
|
|
|
172
196
|
if not raw_credential:
|
|
173
197
|
return None
|
|
174
|
-
|
|
198
|
+
profile = _active_profile_for_cache()
|
|
199
|
+
return _hashlib.sha256(
|
|
200
|
+
f"{profile}:{provider}:{raw_credential}".encode()
|
|
201
|
+
).hexdigest()
|
|
175
202
|
|
|
176
203
|
|
|
177
204
|
def _body_has_tools(body: dict) -> bool:
|
|
@@ -901,6 +901,8 @@ class CacheDB:
|
|
|
901
901
|
ccr_id: str,
|
|
902
902
|
original: bytes,
|
|
903
903
|
ttl_expires: float | None = None,
|
|
904
|
+
*,
|
|
905
|
+
tenant_id: str = "default",
|
|
904
906
|
) -> None:
|
|
905
907
|
import hashlib
|
|
906
908
|
try:
|
|
@@ -908,10 +910,11 @@ class CacheDB:
|
|
|
908
910
|
encrypted = self._encrypt(compressed)
|
|
909
911
|
self._db.execute(
|
|
910
912
|
"INSERT OR REPLACE INTO llmcache_ccr_originals "
|
|
911
|
-
"(ccr_id, original_blob, compressed_hash,
|
|
912
|
-
"
|
|
913
|
+
"(ccr_id, tenant_id, original_blob, compressed_hash, "
|
|
914
|
+
" byte_size_orig, byte_size_comp, ttl_expires) "
|
|
915
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
913
916
|
(
|
|
914
|
-
ccr_id, encrypted,
|
|
917
|
+
ccr_id, tenant_id, encrypted,
|
|
915
918
|
hashlib.sha256(original).hexdigest(),
|
|
916
919
|
len(original), len(compressed),
|
|
917
920
|
ttl_expires,
|
|
@@ -920,13 +923,14 @@ class CacheDB:
|
|
|
920
923
|
except sqlite3.Error as exc:
|
|
921
924
|
logger.warning("CacheDB.ccr_put failed: %s", exc)
|
|
922
925
|
|
|
923
|
-
def ccr_get(self, ccr_id: str) -> bytes | None:
|
|
926
|
+
def ccr_get(self, ccr_id: str, *, tenant_id: str = "default") -> bytes | None:
|
|
924
927
|
try:
|
|
925
928
|
rows = self._db.execute(
|
|
926
929
|
"SELECT original_blob FROM llmcache_ccr_originals "
|
|
927
930
|
"WHERE ccr_id = ? "
|
|
931
|
+
"AND tenant_id = ? "
|
|
928
932
|
"AND (ttl_expires IS NULL OR ttl_expires > ?)",
|
|
929
|
-
(ccr_id, time.time()),
|
|
933
|
+
(ccr_id, tenant_id, time.time()),
|
|
930
934
|
)
|
|
931
935
|
if not rows:
|
|
932
936
|
return None
|
|
@@ -948,15 +952,16 @@ class CacheDB:
|
|
|
948
952
|
except sqlite3.Error as exc:
|
|
949
953
|
logger.warning("CacheDB.ccr_update_compressed failed: %s", exc)
|
|
950
954
|
|
|
951
|
-
def ccr_delete(self, ccr_id: str) -> None:
|
|
952
|
-
"""Delete a CCR row by ccr_id. Idempotent — warns on sqlite error, never raises.
|
|
955
|
+
def ccr_delete(self, ccr_id: str, *, tenant_id: str = "default") -> None:
|
|
956
|
+
"""Delete a CCR row by ccr_id scoped to tenant. Idempotent — warns on sqlite error, never raises.
|
|
953
957
|
|
|
954
958
|
WP-10 D6: defensive infra + sweep parity. Deleting a non-existent row is a no-op.
|
|
959
|
+
H-02: tenant_id guard prevents a tenant from deleting another tenant's CCR.
|
|
955
960
|
"""
|
|
956
961
|
try:
|
|
957
962
|
self._db.execute(
|
|
958
|
-
"DELETE FROM llmcache_ccr_originals WHERE ccr_id = ?",
|
|
959
|
-
(ccr_id,),
|
|
963
|
+
"DELETE FROM llmcache_ccr_originals WHERE ccr_id = ? AND tenant_id = ?",
|
|
964
|
+
(ccr_id, tenant_id),
|
|
960
965
|
)
|
|
961
966
|
except sqlite3.Error as exc:
|
|
962
967
|
logger.warning("CacheDB.ccr_delete failed (non-fatal): %s", exc)
|
|
@@ -1049,6 +1054,30 @@ class CacheDB:
|
|
|
1049
1054
|
except sqlite3.Error as exc:
|
|
1050
1055
|
logger.warning("CacheDB.metrics_flush failed: %s", exc)
|
|
1051
1056
|
|
|
1057
|
+
# ---- KV counters (M2 — durable slm_cache_* hit/miss stats) ----
|
|
1058
|
+
|
|
1059
|
+
def kv_counter_incr(self, name: str, delta: int = 1) -> None:
|
|
1060
|
+
"""Atomically add ``delta`` to a named KV counter (fail-open)."""
|
|
1061
|
+
try:
|
|
1062
|
+
with self._db.transaction():
|
|
1063
|
+
self._db.execute(
|
|
1064
|
+
"INSERT INTO llmcache_kv_counters(name, count) "
|
|
1065
|
+
"VALUES (:name, :delta) "
|
|
1066
|
+
"ON CONFLICT(name) DO UPDATE SET count = count + :delta",
|
|
1067
|
+
{"name": name, "delta": delta},
|
|
1068
|
+
)
|
|
1069
|
+
except sqlite3.Error as exc:
|
|
1070
|
+
logger.debug("CacheDB.kv_counter_incr(%s) failed (non-fatal): %s", name, exc)
|
|
1071
|
+
|
|
1072
|
+
def kv_counters_load(self) -> dict[str, int]:
|
|
1073
|
+
"""Return all persisted KV counters as {name: count} (fail-open)."""
|
|
1074
|
+
try:
|
|
1075
|
+
rows = self._db.execute("SELECT name, count FROM llmcache_kv_counters")
|
|
1076
|
+
return {r["name"]: r["count"] for r in rows}
|
|
1077
|
+
except sqlite3.Error as exc:
|
|
1078
|
+
logger.debug("CacheDB.kv_counters_load failed: %s", exc)
|
|
1079
|
+
return {}
|
|
1080
|
+
|
|
1052
1081
|
# ---- convenience / non-contract helpers ----
|
|
1053
1082
|
|
|
1054
1083
|
def entry_exists(self, cache_key: str, tenant_id: str = _DEFAULT_TENANT) -> bool:
|
|
@@ -120,6 +120,15 @@ _DDL_STATEMENTS: tuple[str, ...] = (
|
|
|
120
120
|
updated_at REAL NOT NULL DEFAULT 0
|
|
121
121
|
)
|
|
122
122
|
""",
|
|
123
|
+
# M2: durable KV hit/miss counters for the slm_cache_* MCP tools. Kept in a
|
|
124
|
+
# separate table (not llmcache_metrics, whose columns mirror MetricsSnapshot
|
|
125
|
+
# exactly) so KV stats survive daemon restarts.
|
|
126
|
+
"""
|
|
127
|
+
CREATE TABLE IF NOT EXISTS llmcache_kv_counters (
|
|
128
|
+
name TEXT PRIMARY KEY,
|
|
129
|
+
count INTEGER NOT NULL DEFAULT 0
|
|
130
|
+
)
|
|
131
|
+
""",
|
|
123
132
|
)
|
|
124
133
|
|
|
125
134
|
|
|
@@ -170,6 +179,7 @@ def get_table_names() -> tuple[str, ...]:
|
|
|
170
179
|
"llmcache_boundaries",
|
|
171
180
|
"llmcache_centroids",
|
|
172
181
|
"llmcache_metrics",
|
|
182
|
+
"llmcache_kv_counters",
|
|
173
183
|
)
|
|
174
184
|
|
|
175
185
|
|
|
@@ -157,7 +157,7 @@ class PatternExtractor:
|
|
|
157
157
|
all_patterns: list[PatternAssertion] = []
|
|
158
158
|
all_patterns.extend(self._extract_from_core_memory(profile_id))
|
|
159
159
|
all_patterns.extend(self._extract_from_behavioral(profile_id))
|
|
160
|
-
all_patterns.extend(self._extract_from_cross_project())
|
|
160
|
+
all_patterns.extend(self._extract_from_cross_project(profile_id))
|
|
161
161
|
all_patterns.extend(self._extract_from_workflows(profile_id))
|
|
162
162
|
# v3.4.7: Extract from behavioral assertions (learned patterns)
|
|
163
163
|
all_patterns.extend(self._extract_from_assertions(profile_id))
|
|
@@ -270,9 +270,12 @@ class PatternExtractor:
|
|
|
270
270
|
))
|
|
271
271
|
return patterns
|
|
272
272
|
|
|
273
|
-
def _extract_from_cross_project(
|
|
274
|
-
|
|
273
|
+
def _extract_from_cross_project(
|
|
274
|
+
self, profile_id: str = "default"
|
|
275
|
+
) -> list[PatternAssertion]:
|
|
276
|
+
"""Extract patterns from cross-project aggregator, scoped to *profile_id*."""
|
|
275
277
|
preferences = self._cross_project.get_preferences(
|
|
278
|
+
profile_id=profile_id,
|
|
276
279
|
min_confidence=self._config.min_confidence,
|
|
277
280
|
)
|
|
278
281
|
patterns: list[PatternAssertion] = []
|
|
@@ -314,7 +314,7 @@ class AgenticRetriever:
|
|
|
314
314
|
entity = self._db.get_entity_by_name(name, profile_id)
|
|
315
315
|
if entity:
|
|
316
316
|
try:
|
|
317
|
-
aliases = self._db.get_aliases_for_entity(entity.entity_id)
|
|
317
|
+
aliases = self._db.get_aliases_for_entity(entity.entity_id, profile_id)
|
|
318
318
|
for a in aliases[:2]:
|
|
319
319
|
sub_queries.append(f"{a.alias} {' '.join(action_words)}")
|
|
320
320
|
except Exception:
|
|
@@ -80,13 +80,26 @@ class BM25Channel:
|
|
|
80
80
|
def document_count(self) -> int:
|
|
81
81
|
return len(self._corpus)
|
|
82
82
|
|
|
83
|
-
def ensure_loaded(
|
|
83
|
+
def ensure_loaded(
|
|
84
|
+
self,
|
|
85
|
+
profile_id: str,
|
|
86
|
+
include_global: bool | None = None,
|
|
87
|
+
include_shared: bool | None = None,
|
|
88
|
+
) -> None:
|
|
84
89
|
"""Cold-load BM25 tokens from DB for a profile (once).
|
|
85
90
|
|
|
86
|
-
Idempotent: subsequent calls for the same profile are no-ops.
|
|
91
|
+
Idempotent: subsequent calls for the same profile/scope are no-ops.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
profile_id: Profile to load.
|
|
95
|
+
include_global: Include global-scope facts. Falls back to the
|
|
96
|
+
instance attribute when not supplied.
|
|
97
|
+
include_shared: Include shared-scope facts. Same fallback.
|
|
87
98
|
"""
|
|
88
|
-
include_global
|
|
89
|
-
|
|
99
|
+
if include_global is None:
|
|
100
|
+
include_global = bool(getattr(self, "include_global", False))
|
|
101
|
+
if include_shared is None:
|
|
102
|
+
include_shared = bool(getattr(self, "include_shared", False))
|
|
90
103
|
scope_key = (profile_id, include_global, include_shared)
|
|
91
104
|
if scope_key == self._loaded_scope_key:
|
|
92
105
|
return
|
|
@@ -174,7 +187,12 @@ class BM25Channel:
|
|
|
174
187
|
self._db.store_bm25_tokens(fact_id, profile_id, tokens)
|
|
175
188
|
|
|
176
189
|
def _fts5_search(
|
|
177
|
-
self,
|
|
190
|
+
self,
|
|
191
|
+
query: str,
|
|
192
|
+
profile_id: str,
|
|
193
|
+
top_k: int = 30,
|
|
194
|
+
include_global: bool | None = None,
|
|
195
|
+
include_shared: bool | None = None,
|
|
178
196
|
) -> list[tuple[str, float]]:
|
|
179
197
|
"""v3.5.0: SQLite FTS5 keyword search (C-level indexed, scales to millions).
|
|
180
198
|
|
|
@@ -187,6 +205,10 @@ class BM25Channel:
|
|
|
187
205
|
Raises (OperationalError) if the FTS5 table is absent — the caller
|
|
188
206
|
then falls back to the legacy in-memory rank_bm25 path.
|
|
189
207
|
"""
|
|
208
|
+
if include_global is None:
|
|
209
|
+
include_global = bool(getattr(self, "include_global", False))
|
|
210
|
+
if include_shared is None:
|
|
211
|
+
include_shared = bool(getattr(self, "include_shared", False))
|
|
190
212
|
tokens = tokenize(query)
|
|
191
213
|
if not tokens:
|
|
192
214
|
return []
|
|
@@ -195,8 +217,8 @@ class BM25Channel:
|
|
|
195
217
|
match_expr = " OR ".join('"' + t.replace('"', "") + '"' for t in tokens)
|
|
196
218
|
where, params = _scope_where(
|
|
197
219
|
profile_id,
|
|
198
|
-
include_global=
|
|
199
|
-
include_shared=
|
|
220
|
+
include_global=include_global,
|
|
221
|
+
include_shared=include_shared,
|
|
200
222
|
prefix="af",
|
|
201
223
|
)
|
|
202
224
|
sql = (
|
|
@@ -214,13 +236,39 @@ class BM25Channel:
|
|
|
214
236
|
if not fid:
|
|
215
237
|
continue
|
|
216
238
|
out.append((fid, -float(d.get("rank", 0.0))))
|
|
217
|
-
|
|
239
|
+
|
|
240
|
+
# T3b: UNION fact-expansion (alias / paraphrase) matches so a query for
|
|
241
|
+
# a synonym matches a fact that only used the canonical term. Additive —
|
|
242
|
+
# direct content hits stay primary; an alias-only hit is added at a
|
|
243
|
+
# discount so it ranks below content matches. Fail-open if the expansion
|
|
244
|
+
# FTS is absent (legacy DB).
|
|
245
|
+
content_ids = {fid for fid, _ in out}
|
|
246
|
+
try:
|
|
247
|
+
exp_sql = (
|
|
248
|
+
"SELECT af.fact_id AS fact_id, bm25(fact_expansion_fts) AS rank "
|
|
249
|
+
"FROM fact_expansion_fts "
|
|
250
|
+
"JOIN atomic_facts af ON af.fact_id = fact_expansion_fts.fact_id "
|
|
251
|
+
f"WHERE fact_expansion_fts MATCH ? AND {where} "
|
|
252
|
+
"ORDER BY rank LIMIT ?"
|
|
253
|
+
)
|
|
254
|
+
for r in self._db.execute(exp_sql, (match_expr, *params, int(top_k))):
|
|
255
|
+
d = dict(r)
|
|
256
|
+
fid = d.get("fact_id")
|
|
257
|
+
if fid and fid not in content_ids:
|
|
258
|
+
out.append((fid, -float(d.get("rank", 0.0)) * 0.85))
|
|
259
|
+
except Exception as exc: # pragma: no cover — legacy/missing expansion FTS
|
|
260
|
+
logger.debug("Expansion FTS search skipped: %s", exc)
|
|
261
|
+
|
|
262
|
+
out.sort(key=lambda x: x[1], reverse=True)
|
|
263
|
+
return out[:top_k]
|
|
218
264
|
|
|
219
265
|
def search(
|
|
220
266
|
self,
|
|
221
267
|
query: str,
|
|
222
268
|
profile_id: str,
|
|
223
269
|
top_k: int = 30,
|
|
270
|
+
include_global: bool | None = None,
|
|
271
|
+
include_shared: bool | None = None,
|
|
224
272
|
) -> list[tuple[str, float]]:
|
|
225
273
|
"""Search BM25 index for matching facts.
|
|
226
274
|
|
|
@@ -230,10 +278,17 @@ class BM25Channel:
|
|
|
230
278
|
query: Search query text.
|
|
231
279
|
profile_id: Scope to this profile.
|
|
232
280
|
top_k: Maximum results.
|
|
281
|
+
include_global: Include global-scope facts. Falls back to the
|
|
282
|
+
instance attribute when not supplied.
|
|
283
|
+
include_shared: Include shared-scope facts. Same fallback.
|
|
233
284
|
|
|
234
285
|
Returns:
|
|
235
286
|
List of (fact_id, bm25_score) sorted by score descending.
|
|
236
287
|
"""
|
|
288
|
+
if include_global is None:
|
|
289
|
+
include_global = bool(getattr(self, "include_global", False))
|
|
290
|
+
if include_shared is None:
|
|
291
|
+
include_shared = bool(getattr(self, "include_shared", False))
|
|
237
292
|
# v3.5.0: FTS5 fast path — C-level indexed, ~ms, scales to millions.
|
|
238
293
|
# The legacy in-memory rank_bm25 path rebuilt the whole index over the
|
|
239
294
|
# entire corpus on every corpus change (11s+ at 17.5k facts, does not
|
|
@@ -241,13 +296,16 @@ class BM25Channel:
|
|
|
241
296
|
# Falls back to rank_bm25 ONLY if the FTS5 table is genuinely
|
|
242
297
|
# unavailable (raises) — e.g. a pre-FTS legacy DB.
|
|
243
298
|
try:
|
|
244
|
-
return self._fts5_search(
|
|
299
|
+
return self._fts5_search(
|
|
300
|
+
query, profile_id, top_k,
|
|
301
|
+
include_global=include_global, include_shared=include_shared,
|
|
302
|
+
)
|
|
245
303
|
except Exception as exc: # pragma: no cover — legacy/missing FTS table
|
|
246
304
|
logger.debug(
|
|
247
305
|
"BM25 FTS5 path unavailable, using rank_bm25 fallback: %s", exc,
|
|
248
306
|
)
|
|
249
307
|
|
|
250
|
-
self.ensure_loaded(profile_id)
|
|
308
|
+
self.ensure_loaded(profile_id, include_global=include_global, include_shared=include_shared)
|
|
251
309
|
|
|
252
310
|
if not self._corpus:
|
|
253
311
|
return []
|