superlocalmemory 3.4.19 → 3.4.22
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/CHANGELOG.md +24 -0
- package/README.md +42 -34
- package/bin/slm +11 -0
- package/bin/slm.bat +12 -0
- package/package.json +4 -3
- package/pyproject.toml +4 -3
- package/scripts/build-slm-hook.ps1 +40 -0
- package/scripts/build-slm-hook.sh +45 -0
- package/scripts/build_entry.py +452 -0
- package/scripts/ci/stage5b_gate.sh +50 -0
- package/scripts/postinstall/validation.js +187 -0
- package/scripts/postinstall-interactive.js +756 -0
- package/scripts/postinstall_binary.js +287 -0
- package/scripts/release_manifest.py +273 -0
- package/scripts/slm-hook.spec +56 -0
- package/skills/slm-build-graph/SKILL.md +423 -0
- package/skills/slm-list-recent/SKILL.md +348 -0
- package/skills/slm-recall/SKILL.md +343 -0
- package/skills/slm-remember/SKILL.md +194 -0
- package/skills/slm-show-patterns/SKILL.md +224 -0
- package/skills/slm-status/SKILL.md +363 -0
- package/skills/slm-switch-profile/SKILL.md +442 -0
- package/src/superlocalmemory/cli/commands.py +254 -79
- package/src/superlocalmemory/cli/context_commands.py +192 -0
- package/src/superlocalmemory/cli/daemon.py +15 -1
- package/src/superlocalmemory/cli/db_migrate.py +80 -0
- package/src/superlocalmemory/cli/escape_hatch.py +220 -0
- package/src/superlocalmemory/cli/main.py +72 -1
- package/src/superlocalmemory/core/context_cache.py +397 -0
- package/src/superlocalmemory/core/engine.py +38 -2
- package/src/superlocalmemory/core/engine_wiring.py +1 -1
- package/src/superlocalmemory/core/ram_lock.py +111 -0
- package/src/superlocalmemory/core/recall_pipeline.py +433 -3
- package/src/superlocalmemory/core/recall_worker.py +8 -3
- package/src/superlocalmemory/core/security_primitives.py +635 -0
- package/src/superlocalmemory/core/shadow_router.py +319 -0
- package/src/superlocalmemory/core/slm_disabled.py +87 -0
- package/src/superlocalmemory/core/slmignore.py +125 -0
- package/src/superlocalmemory/core/topic_signature.py +143 -0
- package/src/superlocalmemory/core/worker_pool.py +14 -3
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +2 -2
- package/src/superlocalmemory/evolution/budget.py +321 -0
- package/src/superlocalmemory/evolution/llm_dispatch.py +508 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +144 -94
- package/src/superlocalmemory/hooks/_outcome_common.py +506 -0
- package/src/superlocalmemory/hooks/adapter_base.py +317 -0
- package/src/superlocalmemory/hooks/antigravity_adapter.py +192 -0
- package/src/superlocalmemory/hooks/claude_code_hooks.py +33 -1
- package/src/superlocalmemory/hooks/context_payload.py +312 -0
- package/src/superlocalmemory/hooks/copilot_adapter.py +154 -0
- package/src/superlocalmemory/hooks/cross_platform_connector.py +90 -0
- package/src/superlocalmemory/hooks/cursor_adapter.py +195 -0
- package/src/superlocalmemory/hooks/hook_handlers.py +109 -8
- package/src/superlocalmemory/hooks/ide_connector.py +25 -2
- package/src/superlocalmemory/hooks/post_tool_async_hook.py +165 -0
- package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +223 -0
- package/src/superlocalmemory/hooks/prewarm_auth.py +170 -0
- package/src/superlocalmemory/hooks/session_registry.py +186 -0
- package/src/superlocalmemory/hooks/stop_outcome_hook.py +134 -0
- package/src/superlocalmemory/hooks/sync_loop.py +114 -0
- package/src/superlocalmemory/hooks/user_prompt_hook.py +128 -0
- package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +202 -0
- package/src/superlocalmemory/infra/backup.py +3 -3
- package/src/superlocalmemory/infra/cloud_backup.py +2 -2
- package/src/superlocalmemory/infra/event_bus.py +2 -2
- package/src/superlocalmemory/infra/webhook_dispatcher.py +3 -3
- package/src/superlocalmemory/learning/arm_catalog.py +99 -0
- package/src/superlocalmemory/learning/bandit.py +526 -0
- package/src/superlocalmemory/learning/bandit_cache.py +133 -0
- package/src/superlocalmemory/learning/behavioral.py +53 -1
- package/src/superlocalmemory/learning/consolidation_cycle.py +381 -0
- package/src/superlocalmemory/learning/consolidation_worker.py +188 -520
- package/src/superlocalmemory/learning/database.py +256 -0
- package/src/superlocalmemory/learning/dedup_hnsw.py +413 -0
- package/src/superlocalmemory/learning/ensemble.py +300 -0
- package/src/superlocalmemory/learning/fact_outcome_joins.py +207 -0
- package/src/superlocalmemory/learning/forgetting_scheduler.py +55 -0
- package/src/superlocalmemory/learning/hnsw_dedup.py +69 -0
- package/src/superlocalmemory/learning/labeler.py +87 -0
- package/src/superlocalmemory/learning/legacy_migration.py +277 -0
- package/src/superlocalmemory/learning/memory_merge.py +160 -0
- package/src/superlocalmemory/learning/model_cache.py +269 -0
- package/src/superlocalmemory/learning/model_rollback.py +278 -0
- package/src/superlocalmemory/learning/outcome_queue.py +284 -0
- package/src/superlocalmemory/learning/pattern_miner.py +415 -0
- package/src/superlocalmemory/learning/pattern_miner_constants.py +47 -0
- package/src/superlocalmemory/learning/ranker.py +225 -81
- package/src/superlocalmemory/learning/ranker_common.py +163 -0
- package/src/superlocalmemory/learning/ranker_retrain_legacy.py +202 -0
- package/src/superlocalmemory/learning/ranker_retrain_online.py +411 -0
- package/src/superlocalmemory/learning/reward.py +777 -0
- package/src/superlocalmemory/learning/reward_archive.py +210 -0
- package/src/superlocalmemory/learning/reward_boost.py +201 -0
- package/src/superlocalmemory/learning/reward_proxy.py +326 -0
- package/src/superlocalmemory/learning/shadow_test.py +524 -0
- package/src/superlocalmemory/learning/signal_worker.py +270 -0
- package/src/superlocalmemory/learning/signals.py +314 -0
- package/src/superlocalmemory/learning/trigram_index.py +547 -0
- package/src/superlocalmemory/mcp/server.py +5 -5
- package/src/superlocalmemory/mcp/tools_context.py +183 -0
- package/src/superlocalmemory/mcp/tools_core.py +92 -27
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +13 -0
- package/src/superlocalmemory/retrieval/engine.py +52 -0
- package/src/superlocalmemory/server/api.py +2 -2
- package/src/superlocalmemory/server/bandit_loops.py +140 -0
- package/src/superlocalmemory/server/middleware/__init__.py +11 -0
- package/src/superlocalmemory/server/middleware/security_headers.py +144 -0
- package/src/superlocalmemory/server/routes/backup.py +36 -13
- package/src/superlocalmemory/server/routes/behavioral.py +50 -19
- package/src/superlocalmemory/server/routes/brain.py +1234 -0
- package/src/superlocalmemory/server/routes/data_io.py +4 -4
- package/src/superlocalmemory/server/routes/events.py +2 -2
- package/src/superlocalmemory/server/routes/helpers.py +1 -1
- package/src/superlocalmemory/server/routes/learning.py +192 -7
- package/src/superlocalmemory/server/routes/memories.py +189 -1
- package/src/superlocalmemory/server/routes/prewarm.py +171 -0
- package/src/superlocalmemory/server/routes/profiles.py +3 -3
- package/src/superlocalmemory/server/routes/token.py +88 -0
- package/src/superlocalmemory/server/routes/ws.py +5 -5
- package/src/superlocalmemory/server/security_middleware.py +13 -7
- package/src/superlocalmemory/server/ui.py +2 -2
- package/src/superlocalmemory/server/unified_daemon.py +335 -3
- package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +423 -0
- package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +348 -0
- package/src/superlocalmemory/skills/slm-recall/SKILL.md +343 -0
- package/src/superlocalmemory/skills/slm-remember/SKILL.md +194 -0
- package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +224 -0
- package/src/superlocalmemory/skills/slm-status/SKILL.md +363 -0
- package/src/superlocalmemory/skills/slm-switch-profile/SKILL.md +442 -0
- package/src/superlocalmemory/storage/migration_runner.py +545 -0
- package/src/superlocalmemory/storage/migrations/M001_add_signal_features_columns.py +67 -0
- package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +132 -0
- package/src/superlocalmemory/storage/migrations/M003_migration_log.py +38 -0
- package/src/superlocalmemory/storage/migrations/M004_cross_platform_sync_log.py +46 -0
- package/src/superlocalmemory/storage/migrations/M005_bandit_tables.py +75 -0
- package/src/superlocalmemory/storage/migrations/M006_action_outcomes_reward.py +75 -0
- package/src/superlocalmemory/storage/migrations/M007_pending_outcomes.py +63 -0
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +54 -0
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +75 -0
- package/src/superlocalmemory/storage/migrations/M011_archive_and_merge.py +87 -0
- package/src/superlocalmemory/storage/migrations/M012_shadow_observations.py +72 -0
- package/src/superlocalmemory/storage/migrations/M013_bi_temporal_columns.py +55 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +81 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/ui/css/brain.css +409 -0
- package/src/superlocalmemory/ui/css/legacy-dashboard.css +645 -0
- package/src/superlocalmemory/ui/index.html +459 -1345
- package/src/superlocalmemory/ui/js/brain.js +1321 -0
- package/src/superlocalmemory/ui/js/clusters.js +123 -4
- package/src/superlocalmemory/ui/js/init.js +48 -39
- package/src/superlocalmemory/ui/js/memories.js +88 -2
- package/src/superlocalmemory/ui/js/modal.js +71 -1
- package/src/superlocalmemory/ui/js/ng-shell.js +101 -88
- package/src/superlocalmemory/ui/js/trust-dashboard.js +168 -25
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/bootstrap-icons.css +2018 -0
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap.bundle.min.js +7 -0
- package/src/superlocalmemory/ui/vendor/bootstrap.min.css +6 -0
- package/src/superlocalmemory/ui/vendor/d3.v7.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/graphology-library.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/graphology.umd.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/inter-variable.min.css +8 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable-Italic.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/sigma.min.js +1 -0
- package/src/superlocalmemory/ui/js/behavioral.js +0 -447
- package/src/superlocalmemory/ui/js/graph-core.js +0 -447
- package/src/superlocalmemory/ui/js/graph-interactions.js +0 -351
- package/src/superlocalmemory/ui/js/learning.js +0 -435
- package/src/superlocalmemory/ui/js/patterns.js +0 -93
- package/src/superlocalmemory.egg-info/PKG-INFO +0 -647
- package/src/superlocalmemory.egg-info/SOURCES.txt +0 -335
- package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
- package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
- package/src/superlocalmemory.egg-info/requires.txt +0 -58
- package/src/superlocalmemory.egg-info/top_level.txt +0 -1
|
@@ -11,6 +11,8 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
+
import hashlib
|
|
15
|
+
import hmac
|
|
14
16
|
import logging
|
|
15
17
|
from typing import TYPE_CHECKING, Any
|
|
16
18
|
|
|
@@ -19,11 +21,130 @@ if TYPE_CHECKING:
|
|
|
19
21
|
from superlocalmemory.core.hooks import HookRegistry
|
|
20
22
|
from superlocalmemory.storage.database import DatabaseManager
|
|
21
23
|
|
|
24
|
+
from superlocalmemory.core.security_primitives import ensure_install_token
|
|
22
25
|
from superlocalmemory.storage.models import Mode, RecallResponse
|
|
23
26
|
|
|
24
27
|
logger = logging.getLogger(__name__)
|
|
25
28
|
|
|
26
29
|
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# LLD-00 §3 — HMAC fact-id markers (P0.4, SEC-C-01 fix)
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
#
|
|
34
|
+
# Every fact surfaced in a recall response is tagged with
|
|
35
|
+
# slm:fact:<fact_id>:<hmac8>
|
|
36
|
+
# where hmac8 is the first 8 hex chars of HMAC-SHA256(install_token, fact_id).
|
|
37
|
+
#
|
|
38
|
+
# post_tool_outcome_hook (LLD-09) scans only for this prefix and validates
|
|
39
|
+
# the HMAC. Unverified markers are ignored — this closes the tool-output
|
|
40
|
+
# injection attack where attacker-controlled output could forge engagement
|
|
41
|
+
# signals by spelling a known fact_id.
|
|
42
|
+
|
|
43
|
+
_HMAC_MARKER_PREFIX = "slm:fact:"
|
|
44
|
+
_HMAC_LEN = 8
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _emit_marker(fact_id: str) -> str:
|
|
48
|
+
"""Tag ``fact_id`` with its HMAC so downstream hooks can validate.
|
|
49
|
+
|
|
50
|
+
Deterministic per install: a given (install_token, fact_id) pair always
|
|
51
|
+
produces the same marker. Token rotation invalidates old markers.
|
|
52
|
+
"""
|
|
53
|
+
token = ensure_install_token()
|
|
54
|
+
digest = hmac.new(
|
|
55
|
+
token.encode("utf-8"), fact_id.encode("utf-8"), hashlib.sha256
|
|
56
|
+
).hexdigest()[:_HMAC_LEN]
|
|
57
|
+
return f"{_HMAC_MARKER_PREFIX}{fact_id}:{digest}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _validate_marker(marker: str) -> str | None:
|
|
61
|
+
"""Return ``fact_id`` if ``marker`` is a valid HMAC marker, else None.
|
|
62
|
+
|
|
63
|
+
Uses constant-time compare. Never raises.
|
|
64
|
+
"""
|
|
65
|
+
if not isinstance(marker, str) or not marker.startswith(_HMAC_MARKER_PREFIX):
|
|
66
|
+
return None
|
|
67
|
+
rest = marker[len(_HMAC_MARKER_PREFIX):]
|
|
68
|
+
fact_id, sep, presented = rest.rpartition(":")
|
|
69
|
+
if not sep or not fact_id or len(presented) != _HMAC_LEN:
|
|
70
|
+
return None
|
|
71
|
+
try:
|
|
72
|
+
token = ensure_install_token()
|
|
73
|
+
except Exception: # pragma: no cover — install-token I/O failure
|
|
74
|
+
return None
|
|
75
|
+
expected = hmac.new(
|
|
76
|
+
token.encode("utf-8"), fact_id.encode("utf-8"), hashlib.sha256
|
|
77
|
+
).hexdigest()[:_HMAC_LEN]
|
|
78
|
+
if hmac.compare_digest(presented, expected):
|
|
79
|
+
return fact_id
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _apply_markers_to_response(response: RecallResponse) -> None:
|
|
84
|
+
"""Populate ``result.marker`` on every result in ``response``, in place.
|
|
85
|
+
|
|
86
|
+
Called as the last step of :func:`run_recall` before returning. Empty
|
|
87
|
+
responses pass through untouched.
|
|
88
|
+
|
|
89
|
+
# L-P-06: audit flagged ``dataclasses.replace`` as a cheaper path.
|
|
90
|
+
# Verified: ``RecallResult`` is NOT frozen, so the direct in-place
|
|
91
|
+
# attribute assignment below is the O(1) mutation path — no dataclass
|
|
92
|
+
# reconstruction happens. ``replace`` would ALLOCATE a fresh instance
|
|
93
|
+
# per result (strictly slower). Keep the in-place mutation.
|
|
94
|
+
"""
|
|
95
|
+
for r in response.results:
|
|
96
|
+
r.marker = _emit_marker(r.fact.fact_id)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# Stage 8 SB-1 — feed shadow_router from recall-settled signals.
|
|
101
|
+
#
|
|
102
|
+
# LLD-10 Track A.3 needs live-recall A/B observations to feed ShadowTest
|
|
103
|
+
# (pre-promotion) and ModelRollback (post-promotion). The ndcg_at_10
|
|
104
|
+
# signal materialises when ``EngagementRewardModel.finalize_outcome``
|
|
105
|
+
# settles a row — that is the natural call site for this helper.
|
|
106
|
+
#
|
|
107
|
+
# This is a THIN wrapper over ``core.shadow_router.get_shadow_router``
|
|
108
|
+
# so the finalize-outcome path does not need to import shadow_router
|
|
109
|
+
# directly. Fail-soft on every error — recall pipeline integrity comes
|
|
110
|
+
# first.
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def feed_recall_settled(
|
|
115
|
+
*,
|
|
116
|
+
memory_db: str,
|
|
117
|
+
learning_db: str,
|
|
118
|
+
profile_id: str,
|
|
119
|
+
query_id: str,
|
|
120
|
+
ndcg_at_10: float,
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Route a settled recall's NDCG@10 into the shadow router.
|
|
123
|
+
|
|
124
|
+
The arm is recomputed from ``query_id`` so callers don't need to
|
|
125
|
+
persist arm assignment anywhere — the router's determinism
|
|
126
|
+
guarantees the same arm decision at settle-time that was used at
|
|
127
|
+
recall-time.
|
|
128
|
+
|
|
129
|
+
Called from ``EngagementRewardModel.finalize_outcome`` (LLD-08 §4.2)
|
|
130
|
+
after the reward row is committed. Cheap on the hot path: one
|
|
131
|
+
singleton-cache read + one paired-list append.
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
from superlocalmemory.core import shadow_router as _sr
|
|
135
|
+
router = _sr.get_shadow_router(
|
|
136
|
+
memory_db=memory_db,
|
|
137
|
+
learning_db=learning_db,
|
|
138
|
+
profile_id=profile_id,
|
|
139
|
+
)
|
|
140
|
+
arm = router.route_query(query_id)
|
|
141
|
+
router.on_recall_settled(
|
|
142
|
+
query_id=query_id, arm=arm, ndcg_at_10=float(ndcg_at_10),
|
|
143
|
+
)
|
|
144
|
+
except Exception as exc: # pragma: no cover — defence in depth
|
|
145
|
+
logger.debug("feed_recall_settled error: %s", exc)
|
|
146
|
+
|
|
147
|
+
|
|
27
148
|
# ---------------------------------------------------------------------------
|
|
28
149
|
# V3.3.16: Module-level singletons for recall hot-path objects.
|
|
29
150
|
# Prevents creating new BehavioralTracker / ForgettingScheduler per recall
|
|
@@ -54,6 +175,80 @@ def _get_forgetting_scheduler(db: Any, config: Any) -> Any:
|
|
|
54
175
|
return _forgetting_scheduler_cache[key]
|
|
55
176
|
|
|
56
177
|
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
# S8-ARC-04 (v3.4.22): unified ranking entry point.
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
_RANKING_MODES: frozenset[str] = frozenset({"off", "v1", "v2", "v2-ensemble"})
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _resolve_ranking_mode(env: "dict[str, str] | os._Environ[str]") -> str:
|
|
186
|
+
"""Map the ``SLM_RANKING`` env var to a canonical mode.
|
|
187
|
+
|
|
188
|
+
Legacy ``SLM_V2_PIPELINE_DISABLED=1`` and ``SLM_BANDIT_DISABLED=1``
|
|
189
|
+
are honoured for one-release back-compat. Explicit ``SLM_RANKING``
|
|
190
|
+
wins if both are set.
|
|
191
|
+
"""
|
|
192
|
+
raw = (env.get("SLM_RANKING", "") or "").strip().lower()
|
|
193
|
+
if raw in _RANKING_MODES:
|
|
194
|
+
return raw
|
|
195
|
+
if (env.get("SLM_V2_PIPELINE_DISABLED", "0") or "0").strip() == "1":
|
|
196
|
+
# v2 disabled → fall back to v1 adaptive only.
|
|
197
|
+
return "v1"
|
|
198
|
+
if (env.get("SLM_BANDIT_DISABLED", "0") or "0").strip() == "1":
|
|
199
|
+
# Bandit disabled → v2 without ensemble.
|
|
200
|
+
return "v2"
|
|
201
|
+
return "v2-ensemble"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def apply_ranking(
|
|
205
|
+
response: "RecallResponse",
|
|
206
|
+
query: str,
|
|
207
|
+
profile_id: str,
|
|
208
|
+
query_id: str,
|
|
209
|
+
*,
|
|
210
|
+
config: Any = None,
|
|
211
|
+
pipeline_version: str = "v2-ensemble",
|
|
212
|
+
) -> "RecallResponse":
|
|
213
|
+
"""Run the ranking pipeline at the requested version.
|
|
214
|
+
|
|
215
|
+
Modes:
|
|
216
|
+
- ``off``: identity — no ranking passes run at all.
|
|
217
|
+
- ``v1``: v3.1 Active-Memory adaptive rerank only.
|
|
218
|
+
- ``v2``: v1 + v3.4.22 lambdarank rerank + signal enqueue.
|
|
219
|
+
- ``v2-ensemble`` (default): v2 + v3.4.22 contextual-bandit ensemble.
|
|
220
|
+
|
|
221
|
+
Each underlying pass is already defensive (catches its own exceptions),
|
|
222
|
+
so this wrapper adds an outer try/except to guarantee the caller
|
|
223
|
+
always gets a response back. Previously three separate call sites in
|
|
224
|
+
run_recall chained these; collapsing keeps precedence explicit.
|
|
225
|
+
"""
|
|
226
|
+
if pipeline_version == "off":
|
|
227
|
+
return response
|
|
228
|
+
try:
|
|
229
|
+
response = apply_adaptive_ranking(response, query, profile_id,
|
|
230
|
+
config=config)
|
|
231
|
+
except Exception as exc: # pragma: no cover — defensive
|
|
232
|
+
logger.debug("apply_ranking v1 step skipped: %s", exc)
|
|
233
|
+
if pipeline_version == "v1":
|
|
234
|
+
return response
|
|
235
|
+
try:
|
|
236
|
+
response = apply_v2_adaptive_ranking(
|
|
237
|
+
response, query, profile_id, query_id,
|
|
238
|
+
)
|
|
239
|
+
except Exception as exc: # pragma: no cover — defensive
|
|
240
|
+
logger.debug("apply_ranking v2 step skipped: %s", exc)
|
|
241
|
+
if pipeline_version == "v2":
|
|
242
|
+
return response
|
|
243
|
+
try:
|
|
244
|
+
response = apply_v2_bandit_ensemble(
|
|
245
|
+
response, query, profile_id, query_id,
|
|
246
|
+
)
|
|
247
|
+
except Exception as exc: # pragma: no cover — defensive
|
|
248
|
+
logger.debug("apply_ranking ensemble step skipped: %s", exc)
|
|
249
|
+
return response
|
|
250
|
+
|
|
251
|
+
|
|
57
252
|
# ---------------------------------------------------------------------------
|
|
58
253
|
# apply_adaptive_ranking (was MemoryEngine._apply_adaptive_ranking)
|
|
59
254
|
# ---------------------------------------------------------------------------
|
|
@@ -118,6 +313,227 @@ def apply_adaptive_ranking(
|
|
|
118
313
|
)
|
|
119
314
|
|
|
120
315
|
|
|
316
|
+
# ---------------------------------------------------------------------------
|
|
317
|
+
# apply_v2_adaptive_ranking (LLD-02 §4.3)
|
|
318
|
+
# ---------------------------------------------------------------------------
|
|
319
|
+
#
|
|
320
|
+
# Opt-in v3.4.22 path: load active model from learning.db with SHA-256
|
|
321
|
+
# verification, re-rank via native Booster, enqueue signals async. The
|
|
322
|
+
# existing ``apply_adaptive_ranking`` above stays for 3.4.20 callers.
|
|
323
|
+
# ---------------------------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def apply_v2_adaptive_ranking(
|
|
327
|
+
response: RecallResponse,
|
|
328
|
+
query: str,
|
|
329
|
+
profile_id: str,
|
|
330
|
+
query_id: str,
|
|
331
|
+
*,
|
|
332
|
+
learning_db_path: Any = None,
|
|
333
|
+
) -> RecallResponse:
|
|
334
|
+
"""LLD-02 §4.3 — load verified model, rerank, enqueue signals.
|
|
335
|
+
|
|
336
|
+
Never raises. On any error, returns ``response`` unchanged.
|
|
337
|
+
"""
|
|
338
|
+
try:
|
|
339
|
+
from pathlib import Path as _P
|
|
340
|
+
|
|
341
|
+
from superlocalmemory.learning.database import LearningDatabase
|
|
342
|
+
from superlocalmemory.learning.model_cache import load_active
|
|
343
|
+
from superlocalmemory.learning.ranker import AdaptiveRanker
|
|
344
|
+
from superlocalmemory.learning.signals import (
|
|
345
|
+
SignalBatch,
|
|
346
|
+
SignalCandidate,
|
|
347
|
+
enqueue,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
db_path = (_P(learning_db_path) if learning_db_path
|
|
351
|
+
else _P.home() / ".superlocalmemory" / "learning.db")
|
|
352
|
+
if not db_path.exists():
|
|
353
|
+
return response
|
|
354
|
+
|
|
355
|
+
db = LearningDatabase(db_path)
|
|
356
|
+
signal_count = db.count_signals(profile_id)
|
|
357
|
+
active = load_active(db, profile_id)
|
|
358
|
+
|
|
359
|
+
ranker = AdaptiveRanker(
|
|
360
|
+
signal_count=signal_count,
|
|
361
|
+
active_model=active,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Build result-dict shape expected by the ranker's rerank() path.
|
|
365
|
+
result_dicts: list[dict] = []
|
|
366
|
+
for r in response.results:
|
|
367
|
+
result_dicts.append({
|
|
368
|
+
"fact_id": r.fact.fact_id,
|
|
369
|
+
"score": r.score,
|
|
370
|
+
"cross_encoder_score": r.score,
|
|
371
|
+
"trust_score": r.trust_score,
|
|
372
|
+
"channel_scores": r.channel_scores or {},
|
|
373
|
+
"fact": {
|
|
374
|
+
"age_days": 0,
|
|
375
|
+
"access_count": r.fact.access_count,
|
|
376
|
+
},
|
|
377
|
+
"_original": r,
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
query_context = {
|
|
381
|
+
"query_type": response.query_type,
|
|
382
|
+
"profile_id": profile_id,
|
|
383
|
+
}
|
|
384
|
+
reranked_dicts = ranker.rerank(result_dicts, query_context)
|
|
385
|
+
new_results = [d["_original"] for d in reranked_dicts
|
|
386
|
+
if "_original" in d]
|
|
387
|
+
|
|
388
|
+
# S8-SK-04 fix: signal enqueue is OWNED by ``apply_v2_bandit_ensemble``
|
|
389
|
+
# (see below), not this function. Previously both emitted a batch
|
|
390
|
+
# under the same query_id which doubled ``learning_signals`` and
|
|
391
|
+
# tripped the phase-transition threshold at half the intended
|
|
392
|
+
# signal count. This function now just re-ranks; the ensemble path
|
|
393
|
+
# is the single source of signal events.
|
|
394
|
+
|
|
395
|
+
return RecallResponse(
|
|
396
|
+
query=response.query,
|
|
397
|
+
mode=response.mode,
|
|
398
|
+
results=new_results,
|
|
399
|
+
query_type=response.query_type,
|
|
400
|
+
channel_weights=response.channel_weights,
|
|
401
|
+
total_candidates=response.total_candidates,
|
|
402
|
+
retrieval_time_ms=response.retrieval_time_ms,
|
|
403
|
+
)
|
|
404
|
+
except Exception as exc: # pragma: no cover — defensive
|
|
405
|
+
logger.debug("apply_v2_adaptive_ranking skipped: %s", exc)
|
|
406
|
+
return response
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# ---------------------------------------------------------------------------
|
|
410
|
+
# apply_v2_bandit_ensemble (LLD-03 §5.5)
|
|
411
|
+
# ---------------------------------------------------------------------------
|
|
412
|
+
#
|
|
413
|
+
# Contextual Thompson bandit chooses channel weights. If an LGBM model is
|
|
414
|
+
# active, a D8-blended ensemble re-ranks the reweighted candidates. Never
|
|
415
|
+
# raises; honours ``SLM_BANDIT_DISABLED=1`` as a kill switch.
|
|
416
|
+
# ---------------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def apply_v2_bandit_ensemble(
|
|
420
|
+
response: RecallResponse,
|
|
421
|
+
query: str,
|
|
422
|
+
profile_id: str,
|
|
423
|
+
query_id: str,
|
|
424
|
+
*,
|
|
425
|
+
learning_db_path: Any = None,
|
|
426
|
+
) -> RecallResponse:
|
|
427
|
+
"""Apply contextual bandit + optional LGBM ensemble rerank. Safe on error."""
|
|
428
|
+
import os as _os
|
|
429
|
+
|
|
430
|
+
if _os.environ.get("SLM_BANDIT_DISABLED", "0") == "1":
|
|
431
|
+
return response
|
|
432
|
+
if not response.results:
|
|
433
|
+
return response
|
|
434
|
+
|
|
435
|
+
try:
|
|
436
|
+
from datetime import datetime as _dt
|
|
437
|
+
from pathlib import Path as _P
|
|
438
|
+
|
|
439
|
+
from superlocalmemory.learning.bandit import ContextualBandit
|
|
440
|
+
from superlocalmemory.learning.ensemble import (
|
|
441
|
+
choose_ensemble,
|
|
442
|
+
ensemble_rerank,
|
|
443
|
+
)
|
|
444
|
+
from superlocalmemory.learning.signals import (
|
|
445
|
+
SignalBatch,
|
|
446
|
+
SignalCandidate,
|
|
447
|
+
enqueue,
|
|
448
|
+
)
|
|
449
|
+
from superlocalmemory.retrieval.engine import apply_channel_weights
|
|
450
|
+
|
|
451
|
+
db_path = (_P(learning_db_path) if learning_db_path
|
|
452
|
+
else _P.home() / ".superlocalmemory" / "learning.db")
|
|
453
|
+
if not db_path.exists():
|
|
454
|
+
return response
|
|
455
|
+
|
|
456
|
+
# --- 1. bandit.choose ---------------------------------------------
|
|
457
|
+
entity_count = 0
|
|
458
|
+
# Use query_context hints if available on the engine — cheap fallback.
|
|
459
|
+
bandit = ContextualBandit(db_path, profile_id)
|
|
460
|
+
choice = bandit.choose(
|
|
461
|
+
{
|
|
462
|
+
"query_type": response.query_type,
|
|
463
|
+
"entity_count": entity_count,
|
|
464
|
+
},
|
|
465
|
+
query_id,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# --- 2. apply channel weights -------------------------------------
|
|
469
|
+
weighted = apply_channel_weights(list(response.results), choice.weights)
|
|
470
|
+
|
|
471
|
+
# --- 3. choose ensemble + load model (optional) -------------------
|
|
472
|
+
active_model = None
|
|
473
|
+
signal_count = 0
|
|
474
|
+
try:
|
|
475
|
+
from superlocalmemory.learning.database import LearningDatabase
|
|
476
|
+
from superlocalmemory.learning.model_cache import load_active
|
|
477
|
+
db = LearningDatabase(db_path)
|
|
478
|
+
signal_count = db.count_signals(profile_id)
|
|
479
|
+
active_model = load_active(db, profile_id)
|
|
480
|
+
except Exception as exc:
|
|
481
|
+
logger.debug("v2 bandit: model/signal load skipped: %s", exc)
|
|
482
|
+
|
|
483
|
+
weights = choose_ensemble(signal_count, active_model)
|
|
484
|
+
|
|
485
|
+
# --- 4. ensemble rerank -------------------------------------------
|
|
486
|
+
query_context = {
|
|
487
|
+
"query_type": response.query_type,
|
|
488
|
+
"profile_id": profile_id,
|
|
489
|
+
"query_id": query_id,
|
|
490
|
+
"bandit_play_id": choice.play_id,
|
|
491
|
+
}
|
|
492
|
+
try:
|
|
493
|
+
final_results = ensemble_rerank(
|
|
494
|
+
weighted, choice, active_model, weights, query_context,
|
|
495
|
+
)
|
|
496
|
+
except Exception as exc:
|
|
497
|
+
logger.debug("v2 bandit ensemble_rerank skipped: %s", exc)
|
|
498
|
+
final_results = weighted
|
|
499
|
+
|
|
500
|
+
# --- 5. enqueue signals (non-blocking) ----------------------------
|
|
501
|
+
try:
|
|
502
|
+
top20 = final_results[:20]
|
|
503
|
+
candidates = tuple(
|
|
504
|
+
SignalCandidate(
|
|
505
|
+
fact_id=r.fact.fact_id,
|
|
506
|
+
channel_scores=dict(r.channel_scores or {}),
|
|
507
|
+
cross_encoder_score=None,
|
|
508
|
+
result_dict={"fact_id": r.fact.fact_id,
|
|
509
|
+
"score": r.score},
|
|
510
|
+
)
|
|
511
|
+
for r in top20
|
|
512
|
+
)
|
|
513
|
+
enqueue(SignalBatch(
|
|
514
|
+
profile_id=profile_id,
|
|
515
|
+
query_id=query_id,
|
|
516
|
+
query_text=query,
|
|
517
|
+
candidates=candidates,
|
|
518
|
+
query_context=query_context,
|
|
519
|
+
))
|
|
520
|
+
except Exception as exc:
|
|
521
|
+
logger.debug("v2 bandit signal enqueue skipped: %s", exc)
|
|
522
|
+
|
|
523
|
+
return RecallResponse(
|
|
524
|
+
query=response.query,
|
|
525
|
+
mode=response.mode,
|
|
526
|
+
results=final_results,
|
|
527
|
+
query_type=response.query_type,
|
|
528
|
+
channel_weights=response.channel_weights,
|
|
529
|
+
total_candidates=response.total_candidates,
|
|
530
|
+
retrieval_time_ms=response.retrieval_time_ms,
|
|
531
|
+
)
|
|
532
|
+
except Exception as exc: # pragma: no cover — defensive top-level
|
|
533
|
+
logger.debug("apply_v2_bandit_ensemble skipped: %s", exc)
|
|
534
|
+
return response
|
|
535
|
+
|
|
536
|
+
|
|
121
537
|
# ---------------------------------------------------------------------------
|
|
122
538
|
# run_recall (was MemoryEngine.recall)
|
|
123
539
|
# ---------------------------------------------------------------------------
|
|
@@ -278,11 +694,21 @@ def run_recall(
|
|
|
278
694
|
except Exception as exc:
|
|
279
695
|
logger.debug("Hebbian strengthening: %s", exc)
|
|
280
696
|
|
|
281
|
-
#
|
|
697
|
+
# S8-ARC-04 (v3.4.22): unified ranking entry point. Single env-var
|
|
698
|
+
# (SLM_RANKING=off|v1|v2|v2-ensemble) controls the pipeline. Legacy
|
|
699
|
+
# SLM_V2_PIPELINE_DISABLED + SLM_BANDIT_DISABLED still honoured for
|
|
700
|
+
# one-release back-compat. Identity when no active model.
|
|
282
701
|
try:
|
|
283
|
-
|
|
702
|
+
import os as _os
|
|
703
|
+
import uuid as _uuid
|
|
704
|
+
query_id = _uuid.uuid4().hex
|
|
705
|
+
mode = _resolve_ranking_mode(_os.environ)
|
|
706
|
+
response = apply_ranking(
|
|
707
|
+
response, query, profile_id, query_id,
|
|
708
|
+
config=config, pipeline_version=mode,
|
|
709
|
+
)
|
|
284
710
|
except Exception as exc:
|
|
285
|
-
logger.debug("
|
|
711
|
+
logger.debug("Ranking pipeline skipped: %s", exc)
|
|
286
712
|
|
|
287
713
|
# Reconsolidation: access updates trust + count (neuroscience principle)
|
|
288
714
|
if trust_scorer:
|
|
@@ -321,4 +747,8 @@ def run_recall(
|
|
|
321
747
|
hook_ctx["query_type"] = response.query_type
|
|
322
748
|
hooks.run_post("recall", hook_ctx)
|
|
323
749
|
|
|
750
|
+
# LLD-00 §3 — stamp HMAC markers on every result so post_tool_outcome_hook
|
|
751
|
+
# can validate fact_ids observed in downstream tool output.
|
|
752
|
+
_apply_markers_to_response(response)
|
|
753
|
+
|
|
324
754
|
return response
|
|
@@ -72,9 +72,11 @@ def _get_engine():
|
|
|
72
72
|
return _engine
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
def _handle_recall(query: str, limit: int) -> dict:
|
|
75
|
+
def _handle_recall(query: str, limit: int, session_id: str = "") -> dict:
|
|
76
76
|
engine = _get_engine()
|
|
77
|
-
response = engine.recall(
|
|
77
|
+
response = engine.recall(
|
|
78
|
+
query, limit=limit, session_id=session_id or None,
|
|
79
|
+
)
|
|
78
80
|
|
|
79
81
|
# Batch-fetch original memory text for all results
|
|
80
82
|
memory_ids = list({r.fact.memory_id for r in response.results[:limit] if r.fact.memory_id})
|
|
@@ -288,7 +290,10 @@ def _worker_main() -> None:
|
|
|
288
290
|
|
|
289
291
|
try:
|
|
290
292
|
if cmd == "recall":
|
|
291
|
-
result = _handle_recall(
|
|
293
|
+
result = _handle_recall(
|
|
294
|
+
req.get("query", ""), req.get("limit", 10),
|
|
295
|
+
req.get("session_id", ""),
|
|
296
|
+
)
|
|
292
297
|
_respond(result)
|
|
293
298
|
elif cmd == "store":
|
|
294
299
|
result = _handle_store(req.get("content", ""), req.get("metadata", {}))
|