superlocalmemory 3.7.8 → 3.8.1
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 +129 -0
- package/README.md +205 -123
- package/package.json +12 -3
- package/plugin/.claude-plugin/plugin.json +2 -3
- 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/rules/AGENTS.md +57 -18
- 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-graph/SKILL.md +17 -3
- 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-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +2 -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 +228 -17
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/daemon.py +7 -0
- package/src/superlocalmemory/cli/loop_cmd.py +187 -0
- package/src/superlocalmemory/cli/main.py +49 -8
- 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/cli/version_banner.py +17 -3
- 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 +23 -59
- 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 +21 -7
- package/src/superlocalmemory/core/embeddings.py +131 -46
- package/src/superlocalmemory/core/engine.py +41 -22
- package/src/superlocalmemory/core/engine_ingestion.py +359 -43
- 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/ingestion_command.py +134 -25
- package/src/superlocalmemory/core/injection.py +12 -7
- package/src/superlocalmemory/core/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
- 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 +7 -3
- 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 +186 -29
- 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/hook_handlers.py +6 -1
- package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +148 -3
- 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/model_rollback.py +3 -0
- package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
- package/src/superlocalmemory/learning/reward.py +50 -0
- package/src/superlocalmemory/learning/reward_proxy.py +42 -9
- package/src/superlocalmemory/learning/source_quality.py +523 -1
- 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 +263 -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 +32 -79
- 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 +12 -4
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- package/src/superlocalmemory/mcp/tools_learning.py +2 -2
- 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 +221 -47
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- package/src/superlocalmemory/retrieval/reranker.py +3 -4
- 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/config_file.py +90 -0
- package/src/superlocalmemory/server/origin.py +50 -0
- 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 +317 -70
- package/src/superlocalmemory/server/routes/behavioral.py +349 -71
- package/src/superlocalmemory/server/routes/brain.py +69 -12
- 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 +438 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +108 -26
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +189 -68
- 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 +289 -118
- package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
- 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 +200 -31
- 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 +132 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +103 -158
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/token.py +3 -13
- package/src/superlocalmemory/server/routes/v3_api.py +247 -89
- 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 +827 -72
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +132 -1
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -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/migrations/M028_fact_entity_associations.py +270 -0
- package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
- package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +136 -1
- package/src/superlocalmemory/storage/schema_v32.py +2 -0
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- package/src/superlocalmemory/storage/schema_v347.py +4 -0
- 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 +303 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +100 -41
- package/src/superlocalmemory/ui/js/dashboard.js +403 -65
- 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 +871 -0
- package/src/superlocalmemory/ui/js/od-brain.js +816 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +600 -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 +929 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +553 -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 +1107 -0
- package/src/superlocalmemory/ui/js/od-shell.js +809 -0
- package/src/superlocalmemory/ui/js/od-skills.js +600 -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 +17 -3
- 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/plugin-src/.mcp.json +0 -12
- package/plugin-src/agents/slm-memory-advisor.md +0 -44
- package/plugin-src/agents/slm-optimize-advisor.md +0 -38
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +0 -23
- package/plugin-src/manifest.json +0 -25
- package/plugin-src/requirements.txt +0 -1
- package/plugin-src/rules/CLAUDE.md.fragment +0 -44
- package/plugin-src/scripts/ensure-venv.bat +0 -122
- package/plugin-src/scripts/ensure-venv.sh +0 -105
- package/plugin-src/scripts/slm-launch +0 -23
- package/plugin-src/scripts/slm-launch.bat +0 -23
- package/plugin-src/settings.json +0 -16
- package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -17,6 +17,7 @@ License: AGPL-3.0-or-later
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
import concurrent.futures
|
|
20
|
+
import functools
|
|
20
21
|
import logging
|
|
21
22
|
import math
|
|
22
23
|
import re
|
|
@@ -26,6 +27,11 @@ from typing import TYPE_CHECKING, Any, Protocol
|
|
|
26
27
|
|
|
27
28
|
from superlocalmemory.core.config import ChannelWeights, RetrievalConfig
|
|
28
29
|
from superlocalmemory.retrieval.fusion import FusionResult, weighted_rrf
|
|
30
|
+
from superlocalmemory.retrieval.time_window import (
|
|
31
|
+
in_window,
|
|
32
|
+
infer_window_from_query,
|
|
33
|
+
parse_window,
|
|
34
|
+
)
|
|
29
35
|
from superlocalmemory.retrieval.strategy import QueryStrategy, QueryStrategyClassifier
|
|
30
36
|
from superlocalmemory.storage.models import (
|
|
31
37
|
AtomicFact,
|
|
@@ -97,12 +103,11 @@ class RetrievalEngine:
|
|
|
97
103
|
self._profile_channel = profile_channel
|
|
98
104
|
self._bridge = bridge_discovery
|
|
99
105
|
self._trust_scorer = trust_scorer
|
|
100
|
-
# v3.
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
self._scope_lock = threading.Lock()
|
|
106
|
+
# v3.7.9: scope flags (include_global / include_shared) are now threaded
|
|
107
|
+
# as explicit call parameters into every channel's search() method, so
|
|
108
|
+
# concurrent recalls each carry their own flags — no shared mutable state,
|
|
109
|
+
# no lock needed. The _scope_lock and per-recall attribute-set loop have
|
|
110
|
+
# been removed. See defect S01 in the fix/3.7.9 branch notes.
|
|
106
111
|
# One executor belongs to one retrieval engine. Creating/destroying six
|
|
107
112
|
# worker threads on every recall caused allocator/thread-stack RSS churn
|
|
108
113
|
# under sustained sessions. The scope lock already serializes channel
|
|
@@ -149,6 +154,7 @@ class RetrievalEngine:
|
|
|
149
154
|
extra_disabled_channels: set[str] | None = None,
|
|
150
155
|
include_global: bool = False,
|
|
151
156
|
include_shared: bool = False,
|
|
157
|
+
window: str | tuple[str, str] | None = None,
|
|
152
158
|
) -> RecallResponse:
|
|
153
159
|
"""Full retrieval pipeline: strategy -> channels -> RRF -> rerank.
|
|
154
160
|
|
|
@@ -161,7 +167,11 @@ class RetrievalEngine:
|
|
|
161
167
|
for the ``--fast`` CLI flag) without mutating shared config.
|
|
162
168
|
"""
|
|
163
169
|
t0 = time.monotonic()
|
|
164
|
-
|
|
170
|
+
# NOTE: extra_disabled_channels is passed as an explicit local argument
|
|
171
|
+
# to _run_channels() — it is NOT stored on self. Storing it as a shared
|
|
172
|
+
# mutable instance attribute (the old self._extra_disabled = ...) caused
|
|
173
|
+
# a race condition where two concurrent recalls could overwrite each
|
|
174
|
+
# other's channel-disable set (v3.4.64 fix).
|
|
165
175
|
|
|
166
176
|
# v3.5.0 diagnostic: stage timing inside retrieval (SLM_RECALL_TIMING=1).
|
|
167
177
|
import os as _os_e
|
|
@@ -195,18 +205,14 @@ class RetrievalEngine:
|
|
|
195
205
|
# Dynamic top-k for aggregation queries
|
|
196
206
|
effective_limit = 100 if strat.query_type == "aggregation" else limit
|
|
197
207
|
|
|
198
|
-
# 3. Run channels.
|
|
199
|
-
#
|
|
200
|
-
#
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if ch is not None:
|
|
207
|
-
ch.include_global = include_global
|
|
208
|
-
ch.include_shared = include_shared
|
|
209
|
-
ch_results = self._run_channels(query, profile_id, strat)
|
|
208
|
+
# 3. Run channels. Both scope flags AND extra_disabled_channels travel as
|
|
209
|
+
# explicit call parameters so concurrent recalls with different flags
|
|
210
|
+
# cannot corrupt each other. No lock needed — no shared mutable state.
|
|
211
|
+
ch_results = self._run_channels(
|
|
212
|
+
query, profile_id, strat,
|
|
213
|
+
extra_disabled_channels=extra_disabled_channels,
|
|
214
|
+
include_global=include_global, include_shared=include_shared,
|
|
215
|
+
)
|
|
210
216
|
_em("run_channels")
|
|
211
217
|
if profile_hits:
|
|
212
218
|
ch_results["profile"] = profile_hits
|
|
@@ -318,6 +324,32 @@ class RetrievalEngine:
|
|
|
318
324
|
logger.warning("Entity graph signal enhancement: %s", exc)
|
|
319
325
|
|
|
320
326
|
_em("expand+entity_enh")
|
|
327
|
+
|
|
328
|
+
# T-window: prune candidates to the requested event-time range.
|
|
329
|
+
# Event times are fetched for the bounded candidate set only (indexed),
|
|
330
|
+
# then in-range facts are kept — before fact load, so out-of-window facts
|
|
331
|
+
# are never materialized. T3: when the caller passes no explicit window,
|
|
332
|
+
# infer one from natural-language scope in the query ("last week").
|
|
333
|
+
# Safety: an EXPLICIT window is authoritative (honoured even if it empties
|
|
334
|
+
# the set — the user asked for that scope), but an INFERRED window is
|
|
335
|
+
# additive and never makes recall worse — if it would empty the results,
|
|
336
|
+
# fall back to the unwindowed set.
|
|
337
|
+
_explicit_window = window is not None
|
|
338
|
+
_window = window if _explicit_window else infer_window_from_query(query)
|
|
339
|
+
if _window is not None and fused:
|
|
340
|
+
bounds = parse_window(_window)
|
|
341
|
+
if bounds is not None:
|
|
342
|
+
etimes = self._db.get_fact_event_times(
|
|
343
|
+
[fr.fact_id for fr in fused], profile_id,
|
|
344
|
+
)
|
|
345
|
+
windowed = [
|
|
346
|
+
fr for fr in fused
|
|
347
|
+
if in_window(etimes.get(fr.fact_id), bounds)
|
|
348
|
+
]
|
|
349
|
+
if windowed or _explicit_window:
|
|
350
|
+
fused = windowed
|
|
351
|
+
_em("time_window")
|
|
352
|
+
|
|
321
353
|
# 4. Load facts for rerank pool
|
|
322
354
|
pool = min(len(fused), max(effective_limit * 3, 30))
|
|
323
355
|
top = fused[:pool]
|
|
@@ -411,8 +443,85 @@ class RetrievalEngine:
|
|
|
411
443
|
no_confident_match=no_match,
|
|
412
444
|
reranker_applied=reranker_applied,
|
|
413
445
|
reranker_status=reranker_status,
|
|
446
|
+
# Q2b: thematic context when the top results cluster in one
|
|
447
|
+
# community. Precomputed summary lookup only — no per-query LLM.
|
|
448
|
+
community_context=self._community_context(results, profile_id),
|
|
414
449
|
)
|
|
415
450
|
|
|
451
|
+
# -- Community context (Wave Q2b) --------------------------------------
|
|
452
|
+
|
|
453
|
+
def _community_context(
|
|
454
|
+
self, results: list[Any], profile_id: str, top_k: int = 8,
|
|
455
|
+
) -> dict | None:
|
|
456
|
+
"""Attach the precomputed community summary the top results fall into.
|
|
457
|
+
|
|
458
|
+
On-device-safe (market CRIT-1): a single read of the ≤N precomputed
|
|
459
|
+
community_summaries rows + a membership tally — never a per-query LLM
|
|
460
|
+
fan-out. Gated: fires only when >=2 of the top results AND >=40% of
|
|
461
|
+
them belong to one community, so precise factual queries are untouched.
|
|
462
|
+
Fail-open: any error returns None (recall is never affected).
|
|
463
|
+
"""
|
|
464
|
+
if not results or not getattr(
|
|
465
|
+
self._config, "enable_community_context", True,
|
|
466
|
+
):
|
|
467
|
+
return None
|
|
468
|
+
try:
|
|
469
|
+
import json
|
|
470
|
+
from collections import Counter
|
|
471
|
+
|
|
472
|
+
rows = [
|
|
473
|
+
dict(r) for r in self._db.execute(
|
|
474
|
+
"SELECT community_id, summary, keywords, fact_ids_json, "
|
|
475
|
+
"fact_count FROM community_summaries WHERE profile_id = ?",
|
|
476
|
+
(profile_id,),
|
|
477
|
+
)
|
|
478
|
+
]
|
|
479
|
+
if not rows:
|
|
480
|
+
return None
|
|
481
|
+
|
|
482
|
+
fact_to_cid: dict[str, int] = {}
|
|
483
|
+
summ_by_cid: dict[int, dict] = {}
|
|
484
|
+
for r in rows:
|
|
485
|
+
cid = int(r["community_id"])
|
|
486
|
+
summ_by_cid[cid] = r
|
|
487
|
+
try:
|
|
488
|
+
for fid in json.loads(r.get("fact_ids_json") or "[]"):
|
|
489
|
+
fact_to_cid[str(fid)] = cid
|
|
490
|
+
except (ValueError, TypeError):
|
|
491
|
+
continue
|
|
492
|
+
|
|
493
|
+
top_ids = [
|
|
494
|
+
res.fact.fact_id
|
|
495
|
+
for res in results[:top_k]
|
|
496
|
+
if getattr(res, "fact", None) is not None
|
|
497
|
+
]
|
|
498
|
+
tally = Counter(
|
|
499
|
+
fact_to_cid[fid] for fid in top_ids if fid in fact_to_cid
|
|
500
|
+
)
|
|
501
|
+
if not tally:
|
|
502
|
+
return None
|
|
503
|
+
best_cid, count = tally.most_common(1)[0]
|
|
504
|
+
coverage = count / len(top_ids) if top_ids else 0.0
|
|
505
|
+
if count < 2 or coverage < 0.4:
|
|
506
|
+
return None
|
|
507
|
+
|
|
508
|
+
row = summ_by_cid[best_cid]
|
|
509
|
+
try:
|
|
510
|
+
members = json.loads(row.get("fact_ids_json") or "[]")
|
|
511
|
+
except (ValueError, TypeError):
|
|
512
|
+
members = []
|
|
513
|
+
return {
|
|
514
|
+
"community_id": best_cid,
|
|
515
|
+
"summary": row.get("summary", ""),
|
|
516
|
+
"keywords": row.get("keywords", ""),
|
|
517
|
+
"member_fact_ids": members,
|
|
518
|
+
"coverage": round(coverage, 3),
|
|
519
|
+
"matched_results": count,
|
|
520
|
+
}
|
|
521
|
+
except Exception as exc:
|
|
522
|
+
logger.debug("community context skipped (fail-open): %s", exc)
|
|
523
|
+
return None
|
|
524
|
+
|
|
416
525
|
# -- Evidence floor (v3.6.6) -------------------------------------------
|
|
417
526
|
|
|
418
527
|
@staticmethod
|
|
@@ -570,32 +679,47 @@ class RetrievalEngine:
|
|
|
570
679
|
fused: list,
|
|
571
680
|
ch_results: dict[str, list[tuple[str, float]]],
|
|
572
681
|
effective_limit: int,
|
|
573
|
-
min_per_channel: int = 2,
|
|
574
682
|
) -> list:
|
|
575
|
-
"""
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
683
|
+
"""Keep strong lexical and structure evidence visible in the result cap.
|
|
684
|
+
|
|
685
|
+
A semantic channel with a larger weight can fill a small result limit
|
|
686
|
+
even when BM25 has an exact, high-signal hit. That broke the
|
|
687
|
+
``queryable now`` ingestion contract: a freshly inserted FTS row could
|
|
688
|
+
exist durably but remain invisible to immediate recall. Reserve one
|
|
689
|
+
capped slot for a strong BM25 hit and two for a structure channel when
|
|
690
|
+
such candidates exist, without returning more than ``effective_limit``.
|
|
581
691
|
"""
|
|
582
|
-
|
|
692
|
+
channel_minimums = (
|
|
693
|
+
("bm25", 1, 0.0),
|
|
694
|
+
("entity_graph", 2, 0.0),
|
|
695
|
+
)
|
|
583
696
|
top_ids = {fr.fact_id for fr in top}
|
|
584
697
|
|
|
585
698
|
promoted = []
|
|
586
|
-
for ch_name in
|
|
699
|
+
for ch_name, minimum, score_floor in channel_minimums:
|
|
587
700
|
ch_items = ch_results.get(ch_name, [])
|
|
588
701
|
if not ch_items:
|
|
589
702
|
continue
|
|
590
703
|
|
|
591
|
-
|
|
592
|
-
|
|
704
|
+
eligible_ids = {
|
|
705
|
+
fid
|
|
706
|
+
for fid, score in ch_items
|
|
707
|
+
if (
|
|
708
|
+
float(score) > score_floor
|
|
709
|
+
if ch_name == "bm25"
|
|
710
|
+
else float(score) >= score_floor
|
|
711
|
+
)
|
|
712
|
+
}
|
|
713
|
+
if not eligible_ids:
|
|
714
|
+
continue
|
|
715
|
+
|
|
716
|
+
present = sum(1 for fid in eligible_ids if fid in top_ids)
|
|
717
|
+
if present >= minimum:
|
|
593
718
|
continue
|
|
594
719
|
|
|
595
|
-
needed =
|
|
596
|
-
ch_fids = {fid for fid, _ in ch_items}
|
|
720
|
+
needed = minimum - present
|
|
597
721
|
for fr in fused:
|
|
598
|
-
if fr.fact_id in
|
|
722
|
+
if fr.fact_id in eligible_ids and fr.fact_id not in top_ids:
|
|
599
723
|
promoted.append(fr)
|
|
600
724
|
top_ids.add(fr.fact_id)
|
|
601
725
|
needed -= 1
|
|
@@ -605,10 +729,15 @@ class RetrievalEngine:
|
|
|
605
729
|
if not promoted:
|
|
606
730
|
return top
|
|
607
731
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
732
|
+
selected = promoted[:effective_limit]
|
|
733
|
+
result = list(top[:effective_limit])
|
|
734
|
+
free_slots = max(0, effective_limit - len(result))
|
|
735
|
+
result.extend(selected[:free_slots])
|
|
736
|
+
remaining = selected[free_slots:]
|
|
737
|
+
if remaining:
|
|
738
|
+
keep = max(0, effective_limit - len(remaining))
|
|
739
|
+
result = result[:keep] + remaining
|
|
740
|
+
return result[:effective_limit]
|
|
612
741
|
|
|
613
742
|
# -- Channel execution --------------------------------------------------
|
|
614
743
|
|
|
@@ -628,7 +757,14 @@ class RetrievalEngine:
|
|
|
628
757
|
return emb
|
|
629
758
|
|
|
630
759
|
def _run_channels(
|
|
631
|
-
self,
|
|
760
|
+
self,
|
|
761
|
+
query: str,
|
|
762
|
+
profile_id: str,
|
|
763
|
+
strat: QueryStrategy,
|
|
764
|
+
*,
|
|
765
|
+
extra_disabled_channels: set[str] | None = None,
|
|
766
|
+
include_global: bool = False,
|
|
767
|
+
include_shared: bool = False,
|
|
632
768
|
) -> dict[str, list[tuple[str, float]]]:
|
|
633
769
|
"""Run active retrieval channels.
|
|
634
770
|
|
|
@@ -646,7 +782,9 @@ class RetrievalEngine:
|
|
|
646
782
|
out: dict[str, list[tuple[str, float]]] = {}
|
|
647
783
|
# Skip channels listed in disabled_channels (ablation support)
|
|
648
784
|
# V3.4.40: union with per-recall extra_disabled set (e.g. --fast skip)
|
|
649
|
-
|
|
785
|
+
# V3.4.64: extra_disabled is now a local parameter, not a shared instance
|
|
786
|
+
# attribute — eliminates the concurrent-recall race condition.
|
|
787
|
+
disabled = set(self._config.disabled_channels) | set(extra_disabled_channels or ())
|
|
650
788
|
|
|
651
789
|
# V3.3.4: Embed query ONCE, reuse for semantic + hopfield channels
|
|
652
790
|
q_emb: list[float] | None = None
|
|
@@ -688,22 +826,38 @@ class RetrievalEngine:
|
|
|
688
826
|
if self._semantic is not None and q_emb is not None and "semantic" not in disabled:
|
|
689
827
|
futures["semantic"] = executor.submit(
|
|
690
828
|
_safe_channel, "semantic",
|
|
691
|
-
|
|
829
|
+
functools.partial(
|
|
830
|
+
self._semantic.search,
|
|
831
|
+
include_global=include_global, include_shared=include_shared,
|
|
832
|
+
),
|
|
833
|
+
q_emb, profile_id, self._config.semantic_top_k,
|
|
692
834
|
)
|
|
693
835
|
if self._bm25 is not None and "bm25" not in disabled:
|
|
694
836
|
futures["bm25"] = executor.submit(
|
|
695
837
|
_safe_channel, "bm25",
|
|
696
|
-
|
|
838
|
+
functools.partial(
|
|
839
|
+
self._bm25.search,
|
|
840
|
+
include_global=include_global, include_shared=include_shared,
|
|
841
|
+
),
|
|
842
|
+
query, profile_id, self._config.bm25_top_k,
|
|
697
843
|
)
|
|
698
844
|
if self._temporal is not None and "temporal" not in disabled:
|
|
699
845
|
futures["temporal"] = executor.submit(
|
|
700
846
|
_safe_channel, "temporal",
|
|
701
|
-
|
|
847
|
+
functools.partial(
|
|
848
|
+
self._temporal.search,
|
|
849
|
+
include_global=include_global, include_shared=include_shared,
|
|
850
|
+
),
|
|
851
|
+
query, profile_id, self._config.bm25_top_k,
|
|
702
852
|
)
|
|
703
853
|
if self._hopfield is not None and q_emb is not None and "hopfield" not in disabled:
|
|
704
854
|
futures["hopfield"] = executor.submit(
|
|
705
855
|
_safe_channel, "hopfield",
|
|
706
|
-
|
|
856
|
+
functools.partial(
|
|
857
|
+
self._hopfield.search,
|
|
858
|
+
include_global=include_global, include_shared=include_shared,
|
|
859
|
+
),
|
|
860
|
+
q_emb, profile_id, self._config.hopfield_top_k,
|
|
707
861
|
)
|
|
708
862
|
if (
|
|
709
863
|
self._spreading_activation is not None
|
|
@@ -712,17 +866,33 @@ class RetrievalEngine:
|
|
|
712
866
|
):
|
|
713
867
|
futures["spreading_activation"] = executor.submit(
|
|
714
868
|
_safe_channel, "spreading_activation",
|
|
715
|
-
|
|
869
|
+
functools.partial(
|
|
870
|
+
self._spreading_activation.search,
|
|
871
|
+
include_global=include_global, include_shared=include_shared,
|
|
872
|
+
),
|
|
873
|
+
q_emb, profile_id, self._config.bm25_top_k,
|
|
716
874
|
)
|
|
717
875
|
|
|
718
|
-
#
|
|
876
|
+
# Each local channel gets a strict latency budget. A slow graph walk
|
|
877
|
+
# must not make an interactive recall wait 30 seconds; completed
|
|
878
|
+
# channels still participate in fusion and the timeout is observable.
|
|
879
|
+
channel_timeout_seconds = 1.0
|
|
880
|
+
# One shared deadline keeps parallel dispatch genuinely bounded. A
|
|
881
|
+
# per-future timeout here would serialise the wait and turn five slow
|
|
882
|
+
# channels into five seconds of UI latency.
|
|
883
|
+
done, pending = concurrent.futures.wait(
|
|
884
|
+
futures.values(), timeout=channel_timeout_seconds,
|
|
885
|
+
)
|
|
719
886
|
for name, fut in futures.items():
|
|
887
|
+
if fut in pending:
|
|
888
|
+
logger.warning("Channel %s exceeded %.1fs latency budget", name, channel_timeout_seconds)
|
|
889
|
+
continue
|
|
720
890
|
try:
|
|
721
|
-
ch_name, result = fut.result(
|
|
891
|
+
ch_name, result = fut.result()
|
|
722
892
|
if result:
|
|
723
893
|
out[ch_name] = result
|
|
724
894
|
except Exception as exc:
|
|
725
|
-
logger.warning("Channel %s
|
|
895
|
+
logger.warning("Channel %s failed: %s", name, exc)
|
|
726
896
|
|
|
727
897
|
# Apply registered post-retrieval filters (forgetting filter, etc.)
|
|
728
898
|
if hasattr(self, '_registry') and self._registry._filters:
|
|
@@ -981,6 +1151,10 @@ class RetrievalEngine:
|
|
|
981
1151
|
|
|
982
1152
|
_CHANNEL_KEYS: tuple[str, ...] = (
|
|
983
1153
|
"semantic", "bm25", "entity_graph", "temporal",
|
|
1154
|
+
# hopfield + spreading_activation are real retrieval channels (score
|
|
1155
|
+
# contract v2) with bandit-chosen weights; omitting them here silently
|
|
1156
|
+
# discarded adaptive reranking for multi-hop relational recall.
|
|
1157
|
+
"spreading_activation", "hopfield",
|
|
984
1158
|
)
|
|
985
1159
|
|
|
986
1160
|
|
|
@@ -835,13 +835,15 @@ class EntityGraphChannel:
|
|
|
835
835
|
)[:top_k]
|
|
836
836
|
# Shadow SQLite before accepting a projected answer. The graph
|
|
837
837
|
# channel has optional PageRank/community enrichments, so exact
|
|
838
|
-
#
|
|
839
|
-
#
|
|
840
|
-
#
|
|
838
|
+
# Score equality is neither required nor useful; result *membership*
|
|
839
|
+
# is the correctness contract. Order within the same fact set is
|
|
840
|
+
# tolerated — requiring identical ordering would fail closed on
|
|
841
|
+
# every query with score ties, leaving Cozo permanently unused.
|
|
842
|
+
# Any membership divergence is recorded and fails closed to SQLite.
|
|
841
843
|
sqlite_results = self._search_without_cozo(query, profile_id, top_k)
|
|
842
|
-
matches =
|
|
844
|
+
matches = {fact_id for fact_id, _ in cozo_results} == {
|
|
843
845
|
fact_id for fact_id, _ in sqlite_results
|
|
844
|
-
|
|
846
|
+
}
|
|
845
847
|
record = getattr(self._cozo, "record_shadow_comparison", None)
|
|
846
848
|
if callable(record):
|
|
847
849
|
record(matches=matches, projected=cozo_results, canonical=sqlite_results)
|
|
@@ -88,6 +88,8 @@ class HopfieldChannel:
|
|
|
88
88
|
query: Any,
|
|
89
89
|
profile_id: str,
|
|
90
90
|
top_k: int = 50,
|
|
91
|
+
include_global: bool | None = None,
|
|
92
|
+
include_shared: bool | None = None,
|
|
91
93
|
) -> list[tuple[str, float]]:
|
|
92
94
|
"""Search for facts using Hopfield associative retrieval.
|
|
93
95
|
|
|
@@ -95,6 +97,9 @@ class HopfieldChannel:
|
|
|
95
97
|
query: Query embedding (list[float] or np.ndarray).
|
|
96
98
|
profile_id: Scope search to this profile.
|
|
97
99
|
top_k: Maximum results to return.
|
|
100
|
+
include_global: Include global-scope facts. Falls back to the
|
|
101
|
+
instance attribute when not supplied.
|
|
102
|
+
include_shared: Include shared-scope facts. Same fallback.
|
|
98
103
|
|
|
99
104
|
Returns:
|
|
100
105
|
List of (fact_id, score) sorted by score descending.
|
|
@@ -104,8 +109,10 @@ class HopfieldChannel:
|
|
|
104
109
|
if not self._config.enabled:
|
|
105
110
|
return []
|
|
106
111
|
|
|
107
|
-
include_global
|
|
108
|
-
|
|
112
|
+
if include_global is None:
|
|
113
|
+
include_global = bool(getattr(self, "include_global", False))
|
|
114
|
+
if include_shared is None:
|
|
115
|
+
include_shared = bool(getattr(self, "include_shared", False))
|
|
109
116
|
try:
|
|
110
117
|
with self._cache_lock:
|
|
111
118
|
return self._search_inner(
|
|
@@ -25,9 +25,8 @@ import sys
|
|
|
25
25
|
import threading
|
|
26
26
|
import time
|
|
27
27
|
import weakref
|
|
28
|
-
from typing import Any
|
|
29
|
-
|
|
30
28
|
from pathlib import Path
|
|
29
|
+
from typing import Any
|
|
31
30
|
|
|
32
31
|
from superlocalmemory.infra.data_root import state_path
|
|
33
32
|
from superlocalmemory.storage.models import AtomicFact
|
|
@@ -57,9 +56,9 @@ _live_rerankers: set[weakref.ref] = set()
|
|
|
57
56
|
|
|
58
57
|
logger = logging.getLogger(__name__)
|
|
59
58
|
|
|
60
|
-
_IDLE_TIMEOUT_SECONDS =
|
|
59
|
+
_IDLE_TIMEOUT_SECONDS = 1800 # V3.8.1: keep interactive sessions warm.
|
|
61
60
|
# V3.3.12: Configurable via SLM_RERANKER_IDLE_TIMEOUT env var.
|
|
62
|
-
#
|
|
61
|
+
# Low-RAM installations can retain aggressive recycling through the override.
|
|
63
62
|
# Set ``SLM_RERANKER_IDLE_TIMEOUT=120`` + ``slm restart`` to revert.
|
|
64
63
|
_IDLE_TIMEOUT_SECONDS = int(os.environ.get("SLM_RERANKER_IDLE_TIMEOUT", _IDLE_TIMEOUT_SECONDS))
|
|
65
64
|
_SUBPROCESS_RESPONSE_TIMEOUT = 15 # v3.4.52: 15s (was 180s). Long timeout blocked the
|