superlocalmemory 4.0.10 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +170 -0
  3. package/README.md +7 -7
  4. package/package.json +4 -2
  5. package/plugin/.claude-plugin/plugin.json +2 -2
  6. package/plugin/CLAUDE.md +3 -3
  7. package/plugin/agents/slm-governance-advisor.md +1 -1
  8. package/plugin/agents/slm-loop-runner.md +4 -4
  9. package/plugin/agents/slm-memory-advisor.md +1 -1
  10. package/plugin/agents/slm-optimize-advisor.md +1 -1
  11. package/plugin/requirements.txt +1 -1
  12. package/plugin/skills/slm-cache/SKILL.md +1 -1
  13. package/plugin/skills/slm-compress/SKILL.md +1 -1
  14. package/plugin/skills/slm-governance/SKILL.md +1 -1
  15. package/plugin/skills/slm-graph/SKILL.md +1 -1
  16. package/plugin/skills/slm-loop/SKILL.md +2 -2
  17. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  18. package/plugin/skills/slm-profile/SKILL.md +5 -5
  19. package/plugin/skills/slm-recall/SKILL.md +102 -15
  20. package/plugin/skills/slm-remember/SKILL.md +35 -3
  21. package/plugin/skills/slm-scope/SKILL.md +1 -1
  22. package/plugin/skills/slm-session/SKILL.md +29 -3
  23. package/plugin/skills/slm-status/SKILL.md +1 -1
  24. package/plugin-src/rules/AGENTS.md +16 -8
  25. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-governance/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-loop/SKILL.md +2 -2
  30. package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
  31. package/plugin-src/skills/slm-profile/SKILL.md +5 -5
  32. package/plugin-src/skills/slm-recall/SKILL.md +102 -15
  33. package/plugin-src/skills/slm-remember/SKILL.md +35 -3
  34. package/plugin-src/skills/slm-scope/SKILL.md +1 -1
  35. package/plugin-src/skills/slm-session/SKILL.md +29 -3
  36. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  37. package/pyproject.toml +1 -1
  38. package/src/superlocalmemory/__init__.py +1 -1
  39. package/src/superlocalmemory/cli/commands.py +263 -18
  40. package/src/superlocalmemory/cli/daemon.py +30 -0
  41. package/src/superlocalmemory/cli/db_migrate.py +71 -1
  42. package/src/superlocalmemory/cli/gdpr_cmd.py +15 -2
  43. package/src/superlocalmemory/cli/main.py +24 -2
  44. package/src/superlocalmemory/code_graph/database.py +44 -0
  45. package/src/superlocalmemory/compliance/gdpr.py +449 -39
  46. package/src/superlocalmemory/core/admission.py +231 -11
  47. package/src/superlocalmemory/core/backend_orchestrator.py +190 -84
  48. package/src/superlocalmemory/core/config.py +90 -11
  49. package/src/superlocalmemory/core/consolidation_engine.py +34 -0
  50. package/src/superlocalmemory/core/engine.py +140 -11
  51. package/src/superlocalmemory/core/graph_analyzer.py +76 -112
  52. package/src/superlocalmemory/core/graph_metrics.py +597 -0
  53. package/src/superlocalmemory/core/graph_pruner.py +121 -0
  54. package/src/superlocalmemory/core/maintenance_scheduler.py +205 -0
  55. package/src/superlocalmemory/core/mode_capability.py +111 -0
  56. package/src/superlocalmemory/core/ollama_validator.py +315 -0
  57. package/src/superlocalmemory/core/projection_drain.py +380 -0
  58. package/src/superlocalmemory/core/recall_pipeline.py +390 -3
  59. package/src/superlocalmemory/core/recall_worker.py +6 -3
  60. package/src/superlocalmemory/core/scale_autopromote.py +196 -0
  61. package/src/superlocalmemory/core/scale_engine.py +16 -2
  62. package/src/superlocalmemory/core/score_contract.py +21 -1
  63. package/src/superlocalmemory/core/session_identity.py +85 -0
  64. package/src/superlocalmemory/core/status_contract.py +108 -0
  65. package/src/superlocalmemory/core/worker_pool.py +4 -4
  66. package/src/superlocalmemory/core/working_memory.py +288 -0
  67. package/src/superlocalmemory/encoding/cognitive_consolidator.py +36 -6
  68. package/src/superlocalmemory/encoding/context_generator.py +1 -1
  69. package/src/superlocalmemory/encoding/entity_resolver.py +38 -0
  70. package/src/superlocalmemory/encoding/fact_extractor.py +18 -14
  71. package/src/superlocalmemory/encoding/prospective_markers.py +262 -0
  72. package/src/superlocalmemory/encoding/type_router.py +12 -12
  73. package/src/superlocalmemory/evolution/mutation_generator.py +30 -4
  74. package/src/superlocalmemory/graph/cozo_adjacency.py +122 -0
  75. package/src/superlocalmemory/graph/cozo_backend.py +103 -138
  76. package/src/superlocalmemory/hooks/portable_kit.py +10 -2
  77. package/src/superlocalmemory/learning/bandit.py +43 -0
  78. package/src/superlocalmemory/learning/consolidation_worker.py +54 -0
  79. package/src/superlocalmemory/learning/database.py +60 -3
  80. package/src/superlocalmemory/learning/entity_compiler.py +21 -58
  81. package/src/superlocalmemory/learning/feedback.py +3 -1
  82. package/src/superlocalmemory/learning/outcomes.py +47 -16
  83. package/src/superlocalmemory/learning/pattern_miner.py +28 -3
  84. package/src/superlocalmemory/learning/pattern_miner_constants.py +43 -0
  85. package/src/superlocalmemory/learning/pcos.py +291 -0
  86. package/src/superlocalmemory/learning/reward_from_outcomes.py +365 -0
  87. package/src/superlocalmemory/learning/reward_proxy.py +100 -10
  88. package/src/superlocalmemory/learning/signal_kinds.py +79 -0
  89. package/src/superlocalmemory/mcp/profiles.py +14 -2
  90. package/src/superlocalmemory/mcp/tools_active.py +2 -1
  91. package/src/superlocalmemory/mcp/tools_core.py +31 -3
  92. package/src/superlocalmemory/mcp/tools_v28.py +20 -1
  93. package/src/superlocalmemory/parameterization/pattern_extractor.py +14 -1
  94. package/src/superlocalmemory/parameterization/soft_prompt_generator.py +98 -0
  95. package/src/superlocalmemory/retrieval/bm25_channel.py +64 -3
  96. package/src/superlocalmemory/retrieval/channel_status.py +117 -0
  97. package/src/superlocalmemory/retrieval/engine.py +106 -11
  98. package/src/superlocalmemory/retrieval/entity_channel.py +210 -256
  99. package/src/superlocalmemory/retrieval/graph_adjacency.py +219 -0
  100. package/src/superlocalmemory/retrieval/scope_policy.py +20 -0
  101. package/src/superlocalmemory/retrieval/semantic_channel.py +47 -5
  102. package/src/superlocalmemory/retrieval/spreading.py +288 -0
  103. package/src/superlocalmemory/server/api.py +24 -5
  104. package/src/superlocalmemory/server/bandit_loops.py +17 -1
  105. package/src/superlocalmemory/server/rbac_enforce.py +26 -6
  106. package/src/superlocalmemory/server/recall_serializer.py +9 -0
  107. package/src/superlocalmemory/server/routes/behavioral.py +75 -10
  108. package/src/superlocalmemory/server/routes/compliance.py +98 -18
  109. package/src/superlocalmemory/server/routes/config_api.py +186 -4
  110. package/src/superlocalmemory/server/routes/evolution.py +178 -0
  111. package/src/superlocalmemory/server/routes/ingest.py +8 -0
  112. package/src/superlocalmemory/server/routes/learning_telemetry.py +2 -1
  113. package/src/superlocalmemory/server/routes/memories.py +49 -7
  114. package/src/superlocalmemory/server/routes/timeline.py +4 -0
  115. package/src/superlocalmemory/server/routes/v3_api.py +191 -15
  116. package/src/superlocalmemory/server/ui.py +20 -4
  117. package/src/superlocalmemory/server/unified_daemon.py +186 -5
  118. package/src/superlocalmemory/storage/_migration_internals.py +31 -0
  119. package/src/superlocalmemory/storage/_schema_version.py +24 -3
  120. package/src/superlocalmemory/storage/database.py +477 -59
  121. package/src/superlocalmemory/storage/embedding_codec.py +71 -0
  122. package/src/superlocalmemory/storage/lineage_retention.py +236 -0
  123. package/src/superlocalmemory/storage/logical_edges.py +43 -2
  124. package/src/superlocalmemory/storage/migration_runner.py +119 -0
  125. package/src/superlocalmemory/storage/migrations/M044_play_carries_its_own_evidence.py +127 -0
  126. package/src/superlocalmemory/storage/migrations/M045_fact_outcome_score.py +158 -0
  127. package/src/superlocalmemory/storage/migrations/M046_prospective_memory_has_its_own_name.py +620 -0
  128. package/src/superlocalmemory/storage/migrations/M047_fisher_vectors_are_stored_like_every_other_vector.py +306 -0
  129. package/src/superlocalmemory/storage/migrations/M048_upcoming_holds_only_what_is_upcoming.py +207 -0
  130. package/src/superlocalmemory/storage/migrations/M049_a_schema_version_marker_is_one_row.py +201 -0
  131. package/src/superlocalmemory/storage/migrations.py +18 -2
  132. package/src/superlocalmemory/storage/models.py +40 -1
  133. package/src/superlocalmemory/storage/projection_outbox.py +346 -0
  134. package/src/superlocalmemory/storage/retention_policy.py +860 -0
  135. package/src/superlocalmemory/storage/schema.py +12 -1
  136. package/src/superlocalmemory/storage/write_coordinator.py +19 -2
  137. package/src/superlocalmemory/trust/scorer.py +43 -1
  138. package/src/superlocalmemory/ui/index.html +9 -18
  139. package/src/superlocalmemory/ui/js/event-delegation.js +12 -1
  140. package/src/superlocalmemory/ui/js/od-health.js +28 -6
  141. package/src/superlocalmemory/ui/js/od-memories.js +19 -0
  142. package/src/superlocalmemory/ui/js/od-settings.js +87 -1
  143. 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 ChannelWeights, RetrievalConfig
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 = 20,
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
- # Profile shortcut (runs before channel search)
250
- if self._profile_channel is not None:
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 (self._entity is not None
397
- and "entity_graph" not in set(self._config.disabled_channels)
398
- and fused):
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
- return (name, res if res else None)
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