superlocalmemory 4.0.10 → 4.1.2
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 +12 -2
- package/CHANGELOG.md +244 -0
- package/README.md +40 -75
- package/package.json +6 -3
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +4 -4
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +2 -2
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +5 -5
- package/plugin/skills/slm-recall/SKILL.md +102 -15
- package/plugin/skills/slm-remember/SKILL.md +35 -3
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +29 -3
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +16 -8
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +2 -2
- package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
- package/plugin-src/skills/slm-profile/SKILL.md +5 -5
- package/plugin-src/skills/slm-recall/SKILL.md +102 -15
- package/plugin-src/skills/slm-remember/SKILL.md +35 -3
- package/plugin-src/skills/slm-scope/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +29 -3
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +357 -18
- package/src/superlocalmemory/cli/daemon.py +30 -0
- package/src/superlocalmemory/cli/db_migrate.py +71 -1
- package/src/superlocalmemory/cli/gdpr_cmd.py +15 -2
- package/src/superlocalmemory/cli/main.py +24 -2
- package/src/superlocalmemory/code_graph/database.py +44 -0
- package/src/superlocalmemory/compliance/gdpr.py +449 -39
- package/src/superlocalmemory/core/admission.py +231 -11
- package/src/superlocalmemory/core/backend_orchestrator.py +190 -84
- package/src/superlocalmemory/core/config.py +90 -11
- package/src/superlocalmemory/core/consolidation_engine.py +34 -0
- package/src/superlocalmemory/core/engine.py +140 -11
- package/src/superlocalmemory/core/graph_analyzer.py +76 -112
- package/src/superlocalmemory/core/graph_metrics.py +597 -0
- package/src/superlocalmemory/core/graph_pruner.py +121 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +205 -0
- package/src/superlocalmemory/core/mode_capability.py +111 -0
- package/src/superlocalmemory/core/ollama_validator.py +315 -0
- package/src/superlocalmemory/core/projection_drain.py +380 -0
- package/src/superlocalmemory/core/recall_pipeline.py +390 -3
- package/src/superlocalmemory/core/recall_worker.py +6 -3
- package/src/superlocalmemory/core/scale_autopromote.py +196 -0
- package/src/superlocalmemory/core/scale_engine.py +16 -2
- package/src/superlocalmemory/core/score_contract.py +21 -1
- package/src/superlocalmemory/core/session_identity.py +85 -0
- package/src/superlocalmemory/core/status_contract.py +108 -0
- package/src/superlocalmemory/core/worker_pool.py +4 -4
- package/src/superlocalmemory/core/working_memory.py +288 -0
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +36 -6
- package/src/superlocalmemory/encoding/context_generator.py +1 -1
- package/src/superlocalmemory/encoding/entity_resolver.py +38 -0
- package/src/superlocalmemory/encoding/fact_extractor.py +18 -14
- package/src/superlocalmemory/encoding/prospective_markers.py +262 -0
- package/src/superlocalmemory/encoding/type_router.py +12 -12
- package/src/superlocalmemory/evolution/mutation_generator.py +30 -4
- package/src/superlocalmemory/graph/cozo_adjacency.py +122 -0
- package/src/superlocalmemory/graph/cozo_backend.py +103 -138
- package/src/superlocalmemory/hooks/portable_kit.py +10 -2
- package/src/superlocalmemory/learning/bandit.py +43 -0
- package/src/superlocalmemory/learning/consolidation_worker.py +54 -0
- package/src/superlocalmemory/learning/database.py +60 -3
- package/src/superlocalmemory/learning/entity_compiler.py +21 -58
- package/src/superlocalmemory/learning/feedback.py +3 -1
- package/src/superlocalmemory/learning/outcomes.py +47 -16
- package/src/superlocalmemory/learning/pattern_miner.py +28 -3
- package/src/superlocalmemory/learning/pattern_miner_constants.py +43 -0
- package/src/superlocalmemory/learning/pcos.py +291 -0
- package/src/superlocalmemory/learning/reward_from_outcomes.py +365 -0
- package/src/superlocalmemory/learning/reward_proxy.py +100 -10
- package/src/superlocalmemory/learning/signal_kinds.py +79 -0
- package/src/superlocalmemory/mcp/profiles.py +14 -2
- package/src/superlocalmemory/mcp/tools_active.py +2 -1
- package/src/superlocalmemory/mcp/tools_core.py +31 -3
- package/src/superlocalmemory/mcp/tools_v28.py +20 -1
- package/src/superlocalmemory/parameterization/pattern_extractor.py +14 -1
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +98 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +64 -3
- package/src/superlocalmemory/retrieval/channel_status.py +117 -0
- package/src/superlocalmemory/retrieval/engine.py +106 -11
- package/src/superlocalmemory/retrieval/entity_channel.py +210 -256
- package/src/superlocalmemory/retrieval/graph_adjacency.py +219 -0
- package/src/superlocalmemory/retrieval/scope_policy.py +20 -0
- package/src/superlocalmemory/retrieval/semantic_channel.py +47 -5
- package/src/superlocalmemory/retrieval/spreading.py +288 -0
- package/src/superlocalmemory/server/api.py +24 -5
- package/src/superlocalmemory/server/bandit_loops.py +17 -1
- package/src/superlocalmemory/server/rbac_enforce.py +26 -6
- package/src/superlocalmemory/server/recall_health.py +87 -10
- package/src/superlocalmemory/server/recall_serializer.py +9 -0
- package/src/superlocalmemory/server/routes/behavioral.py +75 -10
- package/src/superlocalmemory/server/routes/compliance.py +98 -18
- package/src/superlocalmemory/server/routes/config_api.py +186 -4
- package/src/superlocalmemory/server/routes/evolution.py +178 -0
- package/src/superlocalmemory/server/routes/ingest.py +8 -0
- package/src/superlocalmemory/server/routes/learning_telemetry.py +2 -1
- package/src/superlocalmemory/server/routes/memories.py +49 -7
- package/src/superlocalmemory/server/routes/timeline.py +4 -0
- package/src/superlocalmemory/server/routes/v3_api.py +191 -15
- package/src/superlocalmemory/server/ui.py +20 -4
- package/src/superlocalmemory/server/unified_daemon.py +241 -7
- package/src/superlocalmemory/storage/_migration_internals.py +54 -2
- package/src/superlocalmemory/storage/_schema_version.py +24 -3
- package/src/superlocalmemory/storage/database.py +477 -59
- package/src/superlocalmemory/storage/embedding_codec.py +71 -0
- package/src/superlocalmemory/storage/lineage_retention.py +236 -0
- package/src/superlocalmemory/storage/logical_edges.py +43 -2
- package/src/superlocalmemory/storage/migration_runner.py +119 -0
- package/src/superlocalmemory/storage/migrations/M043_quarantine_display_summaries.py +60 -36
- package/src/superlocalmemory/storage/migrations/M044_play_carries_its_own_evidence.py +127 -0
- package/src/superlocalmemory/storage/migrations/M045_fact_outcome_score.py +158 -0
- package/src/superlocalmemory/storage/migrations/M046_prospective_memory_has_its_own_name.py +620 -0
- package/src/superlocalmemory/storage/migrations/M047_fisher_vectors_are_stored_like_every_other_vector.py +306 -0
- package/src/superlocalmemory/storage/migrations/M048_upcoming_holds_only_what_is_upcoming.py +207 -0
- package/src/superlocalmemory/storage/migrations/M049_a_schema_version_marker_is_one_row.py +201 -0
- package/src/superlocalmemory/storage/migrations.py +18 -2
- package/src/superlocalmemory/storage/models.py +40 -1
- package/src/superlocalmemory/storage/projection_outbox.py +346 -0
- package/src/superlocalmemory/storage/retention_policy.py +860 -0
- package/src/superlocalmemory/storage/schema.py +35 -1
- package/src/superlocalmemory/storage/write_coordinator.py +19 -2
- package/src/superlocalmemory/trust/scorer.py +43 -1
- package/src/superlocalmemory/ui/index.html +9 -18
- package/src/superlocalmemory/ui/js/event-delegation.js +12 -1
- package/src/superlocalmemory/ui/js/od-health.js +28 -6
- package/src/superlocalmemory/ui/js/od-memories.js +19 -0
- package/src/superlocalmemory/ui/js/od-settings.js +87 -1
- package/src/superlocalmemory/ui/js/recall-lab.js +78 -3
|
@@ -26,7 +26,12 @@ import threading
|
|
|
26
26
|
import time
|
|
27
27
|
from typing import TYPE_CHECKING, Any, Protocol
|
|
28
28
|
|
|
29
|
-
from superlocalmemory.core.config import
|
|
29
|
+
from superlocalmemory.core.config import (
|
|
30
|
+
CANONICAL_RECALL_LIMIT,
|
|
31
|
+
ChannelWeights,
|
|
32
|
+
RetrievalConfig,
|
|
33
|
+
)
|
|
34
|
+
from superlocalmemory.retrieval import channel_status as chstat
|
|
30
35
|
from superlocalmemory.retrieval.fusion import FusionResult, weighted_rrf
|
|
31
36
|
from superlocalmemory.retrieval.strategy import QueryStrategy, QueryStrategyClassifier
|
|
32
37
|
from superlocalmemory.retrieval.temporal_validity_filter import (
|
|
@@ -195,7 +200,7 @@ class RetrievalEngine:
|
|
|
195
200
|
|
|
196
201
|
def recall(
|
|
197
202
|
self, query: str, profile_id: str,
|
|
198
|
-
mode: Mode = Mode.A, limit: int =
|
|
203
|
+
mode: Mode = Mode.A, limit: int = CANONICAL_RECALL_LIMIT,
|
|
199
204
|
*,
|
|
200
205
|
extra_disabled_channels: set[str] | None = None,
|
|
201
206
|
include_global: bool = False,
|
|
@@ -246,19 +251,41 @@ class RetrievalEngine:
|
|
|
246
251
|
strat = self._strategy.classify(query, self._base_weights)
|
|
247
252
|
_em("classify")
|
|
248
253
|
|
|
249
|
-
#
|
|
250
|
-
|
|
254
|
+
# What each channel did, so the caller can tell an answer apart from
|
|
255
|
+
# an outage. Owned by this call for the same reason the dropped set is:
|
|
256
|
+
# a dict on the engine would have two concurrent recalls overwriting
|
|
257
|
+
# each other's report.
|
|
258
|
+
channel_status: dict[str, str] = {}
|
|
259
|
+
|
|
260
|
+
# Profile shortcut (runs before channel search).
|
|
261
|
+
#
|
|
262
|
+
# The ablation flag is honoured here too. It was not, so an operator who
|
|
263
|
+
# switched this channel off still had it searched, still had its weight
|
|
264
|
+
# doubled on a hit, and read a status of "ok" or "empty" — which reports
|
|
265
|
+
# their own configuration as a live channel's finding.
|
|
266
|
+
_profile_disabled = "profile" in set(self._config.disabled_channels) | set(
|
|
267
|
+
extra_disabled_channels or (),
|
|
268
|
+
)
|
|
269
|
+
if _profile_disabled:
|
|
270
|
+
profile_hits = []
|
|
271
|
+
channel_status["profile"] = chstat.DISABLED
|
|
272
|
+
elif self._profile_channel is not None:
|
|
251
273
|
try:
|
|
252
274
|
profile_hits = self._profile_channel.search(
|
|
253
275
|
query, profile_id, top_k=10,
|
|
254
276
|
)
|
|
277
|
+
channel_status["profile"] = (
|
|
278
|
+
chstat.OK if profile_hits else chstat.EMPTY
|
|
279
|
+
)
|
|
255
280
|
if profile_hits:
|
|
256
281
|
strat.weights["profile"] = 2.0
|
|
257
282
|
except Exception as exc:
|
|
258
283
|
logger.warning("Profile channel: %s", exc)
|
|
259
284
|
profile_hits = []
|
|
285
|
+
channel_status["profile"] = chstat.ERROR
|
|
260
286
|
else:
|
|
261
287
|
profile_hits = []
|
|
288
|
+
channel_status["profile"] = chstat.NOT_CONFIGURED
|
|
262
289
|
|
|
263
290
|
# Dynamic top-k for aggregation queries
|
|
264
291
|
effective_limit = 100 if strat.query_type == "aggregation" else limit
|
|
@@ -276,6 +303,7 @@ class RetrievalEngine:
|
|
|
276
303
|
as_of=as_of, known_as_of=known_as_of, valid_at=valid_at,
|
|
277
304
|
include_unknown=include_unknown,
|
|
278
305
|
dropped_channels=dropped_channels,
|
|
306
|
+
channel_status=channel_status,
|
|
279
307
|
)
|
|
280
308
|
_em("run_channels")
|
|
281
309
|
# One request may need admission before fusion and again after optional
|
|
@@ -393,9 +421,29 @@ class RetrievalEngine:
|
|
|
393
421
|
# this stage re-scores every fused candidate and then re-sorts them, so
|
|
394
422
|
# whether it ran decided the top answer outright rather than adding to
|
|
395
423
|
# it. Bound by data if it ever needs bounding, never by elapsed time.
|
|
396
|
-
if
|
|
397
|
-
|
|
398
|
-
|
|
424
|
+
if self._entity is None:
|
|
425
|
+
channel_status["entity_graph"] = chstat.NOT_CONFIGURED
|
|
426
|
+
elif "entity_graph" in set(self._config.disabled_channels) | set(
|
|
427
|
+
extra_disabled_channels or (),
|
|
428
|
+
):
|
|
429
|
+
# Both the configured set AND the per-call one, the same way the
|
|
430
|
+
# profile channel resolves it. Reading only the configured set meant
|
|
431
|
+
# a caller that switched this channel off for one recall still had
|
|
432
|
+
# it run, and still saw it reported as though it were the caller's
|
|
433
|
+
# own setting that was being honoured.
|
|
434
|
+
channel_status["entity_graph"] = chstat.DISABLED
|
|
435
|
+
elif not fused:
|
|
436
|
+
# It did not run, and saying "found nothing" would be a different
|
|
437
|
+
# claim. This channel re-scores other channels' candidates rather
|
|
438
|
+
# than producing its own, so with nothing fused there is nothing for
|
|
439
|
+
# it to do — and if the reason nothing fused is that the other five
|
|
440
|
+
# failed, reporting this one as having searched and come back empty
|
|
441
|
+
# hides that.
|
|
442
|
+
channel_status["entity_graph"] = chstat.NO_CANDIDATES
|
|
443
|
+
else:
|
|
444
|
+
# One chain, evaluated once. Repeating the three conditions to guard
|
|
445
|
+
# the work separately is how a status starts describing a decision
|
|
446
|
+
# the code no longer makes.
|
|
399
447
|
try:
|
|
400
448
|
candidate_ids = [fr.fact_id for fr in fused[:100]]
|
|
401
449
|
eg_scores = self._entity.score_candidates(
|
|
@@ -405,6 +453,9 @@ class RetrievalEngine:
|
|
|
405
453
|
include_global=include_global,
|
|
406
454
|
include_shared=include_shared,
|
|
407
455
|
)
|
|
456
|
+
channel_status["entity_graph"] = (
|
|
457
|
+
chstat.OK if eg_scores else chstat.EMPTY
|
|
458
|
+
)
|
|
408
459
|
if eg_scores:
|
|
409
460
|
boosted = []
|
|
410
461
|
for fr in fused:
|
|
@@ -423,6 +474,7 @@ class RetrievalEngine:
|
|
|
423
474
|
fused = sorted(boosted, key=lambda r: (-r.fused_score, r.fact_id))
|
|
424
475
|
except Exception as exc:
|
|
425
476
|
logger.warning("Entity graph signal enhancement: %s", exc)
|
|
477
|
+
channel_status["entity_graph"] = chstat.ERROR
|
|
426
478
|
|
|
427
479
|
# Brain Core S402: bridge and scene expansion append candidates after
|
|
428
480
|
# the channel boundary. Reapply the same hard correction-admission rule
|
|
@@ -565,6 +617,7 @@ class RetrievalEngine:
|
|
|
565
617
|
# community. Precomputed summary lookup only — no per-query LLM.
|
|
566
618
|
community_context=self._community_context(results, profile_id),
|
|
567
619
|
incomplete_channels=tuple(sorted(dropped_channels)),
|
|
620
|
+
channel_status=dict(channel_status),
|
|
568
621
|
)
|
|
569
622
|
|
|
570
623
|
# -- Community context (Wave Q2b) --------------------------------------
|
|
@@ -977,6 +1030,7 @@ class RetrievalEngine:
|
|
|
977
1030
|
valid_at: str | None = None,
|
|
978
1031
|
include_unknown: bool = False,
|
|
979
1032
|
dropped_channels: set[str] | None = None,
|
|
1033
|
+
channel_status: dict[str, str] | None = None,
|
|
980
1034
|
) -> dict[str, list[tuple[str, float]]]:
|
|
981
1035
|
"""Run active retrieval channels.
|
|
982
1036
|
|
|
@@ -994,6 +1048,12 @@ class RetrievalEngine:
|
|
|
994
1048
|
than merely late. It is a caller-owned set passed down per recall and
|
|
995
1049
|
deliberately not an attribute of self — two concurrent recalls sharing
|
|
996
1050
|
one would report each other's losses (the v3.4.64 race).
|
|
1051
|
+
|
|
1052
|
+
``channel_status``, likewise caller-owned, receives one entry per
|
|
1053
|
+
channel saying what became of it. Every channel gets exactly one:
|
|
1054
|
+
those that cannot run are recorded before dispatch with the reason, and
|
|
1055
|
+
every dispatched channel is recorded by the collection loop below —
|
|
1056
|
+
which iterates the futures, so it cannot skip one.
|
|
997
1057
|
"""
|
|
998
1058
|
import os as _os_e
|
|
999
1059
|
import time as _time_e
|
|
@@ -1023,23 +1083,50 @@ class RetrievalEngine:
|
|
|
1023
1083
|
except Exception as exc:
|
|
1024
1084
|
logger.warning("Query embedding failed: %s", exc)
|
|
1025
1085
|
|
|
1086
|
+
# Why a channel will not run, recorded BEFORE dispatch. An embedding
|
|
1087
|
+
# failure silently takes three of the five channels down together, and
|
|
1088
|
+
# the answer never said so: it looked exactly like a store with nothing
|
|
1089
|
+
# relevant in it. Configuration and ablation are recorded too, so an
|
|
1090
|
+
# operator reading a list of absent channels can tell their own choices
|
|
1091
|
+
# apart from a fault.
|
|
1092
|
+
if channel_status is not None:
|
|
1093
|
+
for _name, _obj, _needs_emb in (
|
|
1094
|
+
("semantic", self._semantic, True),
|
|
1095
|
+
("bm25", self._bm25, False),
|
|
1096
|
+
("temporal", self._temporal, False),
|
|
1097
|
+
("hopfield", self._hopfield, True),
|
|
1098
|
+
("spreading_activation", self._spreading_activation, True),
|
|
1099
|
+
):
|
|
1100
|
+
if _obj is None:
|
|
1101
|
+
channel_status[_name] = chstat.NOT_CONFIGURED
|
|
1102
|
+
elif _name in disabled:
|
|
1103
|
+
channel_status[_name] = chstat.DISABLED
|
|
1104
|
+
elif _needs_emb and q_emb is None:
|
|
1105
|
+
channel_status[_name] = chstat.NO_EMBEDDING
|
|
1106
|
+
|
|
1026
1107
|
# v3.4.53: collect channel callables and run in parallel.
|
|
1027
1108
|
# Each channel is a standalone search — no shared mutable state,
|
|
1028
1109
|
# no ordering dependencies. SQLite WAL mode permits concurrent reads.
|
|
1029
1110
|
futures: dict[str, concurrent.futures.Future] = {}
|
|
1030
1111
|
|
|
1031
1112
|
def _safe_channel(name: str, fn, *args):
|
|
1032
|
-
"""Run a single channel, returning (name, result_or_None).
|
|
1113
|
+
"""Run a single channel, returning (name, result_or_None, status).
|
|
1114
|
+
|
|
1115
|
+
Returning the status alongside the result is what separates "found
|
|
1116
|
+
nothing" from "raised": both used to come back as ``None``.
|
|
1117
|
+
"""
|
|
1033
1118
|
_cs = _time_e.monotonic() if _et else 0.0
|
|
1034
1119
|
try:
|
|
1035
1120
|
res = fn(*args)
|
|
1036
1121
|
if _et:
|
|
1037
1122
|
logger.warning("[RECALL-TIMING] channel.%-16s %.0f ms",
|
|
1038
1123
|
name, (_time_e.monotonic() - _cs) * 1000.0)
|
|
1039
|
-
|
|
1124
|
+
if res:
|
|
1125
|
+
return (name, res, chstat.OK)
|
|
1126
|
+
return (name, None, chstat.EMPTY)
|
|
1040
1127
|
except Exception as exc:
|
|
1041
1128
|
logger.warning("%s channel: %s", name, exc)
|
|
1042
|
-
return (name, None)
|
|
1129
|
+
return (name, None, chstat.ERROR)
|
|
1043
1130
|
|
|
1044
1131
|
executor = self._channel_executor
|
|
1045
1132
|
if self._semantic is not None and q_emb is not None and "semantic" not in disabled:
|
|
@@ -1111,14 +1198,22 @@ class RetrievalEngine:
|
|
|
1111
1198
|
)
|
|
1112
1199
|
if dropped_channels is not None:
|
|
1113
1200
|
dropped_channels.add(name)
|
|
1201
|
+
# Same branch as the dropped set on purpose: two writes in one
|
|
1202
|
+
# place cannot disagree about which channels timed out.
|
|
1203
|
+
if channel_status is not None:
|
|
1204
|
+
channel_status[name] = chstat.TIMEOUT
|
|
1114
1205
|
fut.cancel() # no-op if already running; prevents queued jobs from starting
|
|
1115
1206
|
continue
|
|
1116
1207
|
try:
|
|
1117
|
-
ch_name, result = fut.result()
|
|
1208
|
+
ch_name, result, status = fut.result()
|
|
1209
|
+
if channel_status is not None:
|
|
1210
|
+
channel_status[ch_name] = status
|
|
1118
1211
|
if result:
|
|
1119
1212
|
out[ch_name] = result
|
|
1120
1213
|
except Exception as exc:
|
|
1121
1214
|
logger.warning("Channel %s failed: %s", name, exc)
|
|
1215
|
+
if channel_status is not None:
|
|
1216
|
+
channel_status[name] = chstat.ERROR
|
|
1122
1217
|
|
|
1123
1218
|
# Apply registered post-retrieval filters (forgetting filter, etc.).
|
|
1124
1219
|
# Pass as_of in context dict when set so the bi-temporal validity filter
|