superlocalmemory 3.8.7 → 3.8.9

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 (48) hide show
  1. package/CHANGELOG.md +49 -1
  2. package/README.md +3 -3
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-governance-advisor.md +1 -1
  7. package/plugin/agents/slm-loop-runner.md +1 -1
  8. package/plugin/agents/slm-memory-advisor.md +1 -1
  9. package/plugin/agents/slm-optimize-advisor.md +1 -1
  10. package/plugin/requirements.txt +1 -1
  11. package/plugin/skills/slm-cache/SKILL.md +1 -1
  12. package/plugin/skills/slm-compress/SKILL.md +1 -1
  13. package/plugin/skills/slm-governance/SKILL.md +1 -1
  14. package/plugin/skills/slm-graph/SKILL.md +1 -1
  15. package/plugin/skills/slm-loop/SKILL.md +1 -1
  16. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  17. package/plugin/skills/slm-profile/SKILL.md +1 -1
  18. package/plugin/skills/slm-recall/SKILL.md +1 -1
  19. package/plugin/skills/slm-remember/SKILL.md +1 -1
  20. package/plugin/skills/slm-scope/SKILL.md +1 -1
  21. package/plugin/skills/slm-session/SKILL.md +1 -1
  22. package/plugin/skills/slm-status/SKILL.md +1 -1
  23. package/plugin-src/rules/AGENTS.md +1 -1
  24. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  25. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  31. package/pyproject.toml +1 -1
  32. package/src/superlocalmemory/__init__.py +1 -1
  33. package/src/superlocalmemory/core/embeddings.py +128 -13
  34. package/src/superlocalmemory/core/engine.py +6 -1
  35. package/src/superlocalmemory/core/engine_ingestion.py +5 -0
  36. package/src/superlocalmemory/core/ingestion_command.py +36 -0
  37. package/src/superlocalmemory/core/materialization_control.py +20 -0
  38. package/src/superlocalmemory/core/ollama_embedder.py +5 -0
  39. package/src/superlocalmemory/core/recall_gate.py +63 -4
  40. package/src/superlocalmemory/core/recall_pipeline.py +40 -0
  41. package/src/superlocalmemory/core/store_pipeline.py +10 -0
  42. package/src/superlocalmemory/encoding/scene_builder.py +105 -15
  43. package/src/superlocalmemory/mcp/tools_core.py +16 -7
  44. package/src/superlocalmemory/retrieval/entity_channel.py +201 -56
  45. package/src/superlocalmemory/retrieval/vector_store.py +238 -123
  46. package/src/superlocalmemory/server/recall_health.py +3 -1
  47. package/src/superlocalmemory/server/unified_daemon.py +106 -16
  48. package/src/superlocalmemory/storage/embedding_migrator.py +88 -60
@@ -10,6 +10,7 @@ with decay. Handles BOTH uppercase and lowercase entity mentions.
10
10
  Part of Qualixar | Author: Varun Pratap Bhardwaj
11
11
  License: AGPL-3.0-or-later
12
12
  """
13
+
13
14
  from __future__ import annotations
14
15
 
15
16
  import json
@@ -24,7 +25,10 @@ from superlocalmemory.retrieval.scope_policy import (
24
25
  authorized_fact_ids,
25
26
  filter_authorized_results,
26
27
  )
27
- from superlocalmemory.storage.database import _scope_where
28
+ from superlocalmemory.storage.database import (
29
+ _scope_where,
30
+ _unbounded_facts_ceiling,
31
+ )
28
32
 
29
33
  if TYPE_CHECKING:
30
34
  from superlocalmemory.encoding.entity_resolver import EntityResolver
@@ -50,26 +54,126 @@ def _adj_ttl_seconds() -> float:
50
54
  except (TypeError, ValueError):
51
55
  return 3600.0
52
56
 
57
+
53
58
  _PROPER_NOUN_RE = re.compile(r"\b[A-Z][a-z]{1,}\b")
54
59
 
55
- _ENTITY_STOP: frozenset[str] = frozenset({
56
- # Expanded stop list for query entity extraction
57
- "what", "when", "where", "who", "which", "how", "does", "did",
58
- "the", "that", "this", "there", "then", "than", "they", "them",
59
- "have", "has", "had", "been", "being", "about", "after", "before",
60
- "from", "into", "with", "some", "other", "would", "could", "should",
61
- "will", "because", "also", "just", "like", "know", "think",
62
- "feel", "want", "need", "make", "take", "give", "tell", "said",
63
- "wow", "gonna", "got", "by", "thanks", "thank", "hey", "hi",
64
- "hello", "bye", "good", "great", "nice", "cool", "right",
65
- "let", "can", "might", "much", "many", "more", "most",
66
- "something", "anything", "everything", "nothing", "someone",
67
- "it", "my", "your", "our", "their", "me", "you", "we", "us",
68
- "do", "if", "or", "no", "to", "at", "on", "in", "so",
69
- "go", "come", "see", "look", "say", "ask", "try", "keep",
70
- "yes", "yeah", "sure", "okay", "ok", "really", "actually",
71
- "maybe", "well", "still", "even", "very",
72
- })
60
+ _ENTITY_STOP: frozenset[str] = frozenset(
61
+ {
62
+ # Expanded stop list for query entity extraction
63
+ "what",
64
+ "when",
65
+ "where",
66
+ "who",
67
+ "which",
68
+ "how",
69
+ "does",
70
+ "did",
71
+ "the",
72
+ "that",
73
+ "this",
74
+ "there",
75
+ "then",
76
+ "than",
77
+ "they",
78
+ "them",
79
+ "have",
80
+ "has",
81
+ "had",
82
+ "been",
83
+ "being",
84
+ "about",
85
+ "after",
86
+ "before",
87
+ "from",
88
+ "into",
89
+ "with",
90
+ "some",
91
+ "other",
92
+ "would",
93
+ "could",
94
+ "should",
95
+ "will",
96
+ "because",
97
+ "also",
98
+ "just",
99
+ "like",
100
+ "know",
101
+ "think",
102
+ "feel",
103
+ "want",
104
+ "need",
105
+ "make",
106
+ "take",
107
+ "give",
108
+ "tell",
109
+ "said",
110
+ "wow",
111
+ "gonna",
112
+ "got",
113
+ "by",
114
+ "thanks",
115
+ "thank",
116
+ "hey",
117
+ "hi",
118
+ "hello",
119
+ "bye",
120
+ "good",
121
+ "great",
122
+ "nice",
123
+ "cool",
124
+ "right",
125
+ "let",
126
+ "can",
127
+ "might",
128
+ "much",
129
+ "many",
130
+ "more",
131
+ "most",
132
+ "something",
133
+ "anything",
134
+ "everything",
135
+ "nothing",
136
+ "someone",
137
+ "it",
138
+ "my",
139
+ "your",
140
+ "our",
141
+ "their",
142
+ "me",
143
+ "you",
144
+ "we",
145
+ "us",
146
+ "do",
147
+ "if",
148
+ "or",
149
+ "no",
150
+ "to",
151
+ "at",
152
+ "on",
153
+ "in",
154
+ "so",
155
+ "go",
156
+ "come",
157
+ "see",
158
+ "look",
159
+ "say",
160
+ "ask",
161
+ "try",
162
+ "keep",
163
+ "yes",
164
+ "yeah",
165
+ "sure",
166
+ "okay",
167
+ "ok",
168
+ "really",
169
+ "actually",
170
+ "maybe",
171
+ "well",
172
+ "still",
173
+ "even",
174
+ "very",
175
+ }
176
+ )
73
177
 
74
178
 
75
179
  def extract_query_entities(query: str) -> list[str]:
@@ -94,10 +198,10 @@ def extract_query_entities(query: str) -> list[str]:
94
198
  for m in re.finditer(r'"([^"]+)"', query):
95
199
  _add(m.group(1).strip())
96
200
  # Also extract multi-word capitalized sequences (e.g. "New York", "San Francisco")
97
- for m in re.finditer(r'\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)\b', query):
201
+ for m in re.finditer(r"\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)\b", query):
98
202
  _add(m.group(1))
99
203
  # Extract all-caps abbreviations (e.g. NYU, MIT, UCLA) — min 2 chars
100
- for m in re.finditer(r'\b([A-Z]{2,})\b', query):
204
+ for m in re.finditer(r"\b([A-Z]{2,})\b", query):
101
205
  _add(m.group(1))
102
206
 
103
207
  return candidates
@@ -113,9 +217,11 @@ class EntityGraphChannel:
113
217
  """
114
218
 
115
219
  def __init__(
116
- self, db: DatabaseManager,
220
+ self,
221
+ db: DatabaseManager,
117
222
  entity_resolver: EntityResolver | None = None,
118
- decay: float = 0.7, activation_threshold: float = 0.05,
223
+ decay: float = 0.7,
224
+ activation_threshold: float = 0.05,
119
225
  max_hops: int = 4,
120
226
  graph_metrics: dict[str, dict] | None = None,
121
227
  cozo_backend: Any = None, # v3.4.5: optional CozoDB backend
@@ -174,18 +280,19 @@ class EntityGraphChannel:
174
280
  # the graph without changing the count (e.g. store_edge MAX-merge), and a
175
281
  # count-stable window would otherwise serve a stale adjacency map.
176
282
  import time as _t_ec
283
+
177
284
  _now_ec = _t_ec.monotonic()
178
285
  _ttl = _adj_ttl_seconds()
179
286
  # TTL=0 disables the time-based reload entirely (count-based correctness
180
287
  # reload still applies); otherwise the cache is fresh within the TTL.
181
- _fresh = _ttl <= 0.0 or (
182
- (_now_ec - getattr(self, "_adj_loaded_at", 0.0)) < _ttl
183
- )
184
- if (self._adj_scope_key == scope_key
185
- and (self._adj or self._visible_fact_ids)
186
- and self._adj_edge_count == current_count
187
- and self._adj_fact_count == current_fact_count
188
- and _fresh):
288
+ _fresh = _ttl <= 0.0 or ((_now_ec - getattr(self, "_adj_loaded_at", 0.0)) < _ttl)
289
+ if (
290
+ self._adj_scope_key == scope_key
291
+ and (self._adj or self._visible_fact_ids)
292
+ and self._adj_edge_count == current_count
293
+ and self._adj_fact_count == current_fact_count
294
+ and _fresh
295
+ ):
189
296
  return
190
297
  adj: dict[str, list[tuple[str, float]]] = defaultdict(list)
191
298
  try:
@@ -195,8 +302,7 @@ class EntityGraphChannel:
195
302
  include_shared=include_shared,
196
303
  )
197
304
  rows = self._db.execute(
198
- "SELECT source_id, target_id, weight FROM graph_edges "
199
- f"WHERE {where}",
305
+ f"SELECT source_id, target_id, weight FROM graph_edges WHERE {where}",
200
306
  (*params,),
201
307
  )
202
308
  except Exception:
@@ -234,8 +340,10 @@ class EntityGraphChannel:
234
340
 
235
341
  logger.info(
236
342
  "Loaded adjacency cache: %d nodes, %d edges, %d entity mappings for profile %s",
237
- len(self._adj), sum(len(v) for v in self._adj.values()) // 2,
238
- len(self._entity_to_facts), profile_id,
343
+ len(self._adj),
344
+ sum(len(v) for v in self._adj.values()) // 2,
345
+ len(self._entity_to_facts),
346
+ profile_id,
239
347
  )
240
348
 
241
349
  def _get_edge_count(
@@ -272,7 +380,11 @@ class EntityGraphChannel:
272
380
  """Pre-load entity→fact and fact→entity maps into memory.
273
381
 
274
382
  Eliminates per-entity and per-fact SQL in the spreading activation loop.
275
- Same data, same algorithm zero quality change.
383
+ Fetch only the two columns this index consumes. Loading full AtomicFact
384
+ objects also deserializes every 768-d embedding and Fisher vector; on a
385
+ mature database that turned one new fact into a 5-second recall stall.
386
+ The scope predicate and configurable 50k safety ceiling are identical
387
+ to ``get_all_facts``; only heavyweight unused columns are omitted.
276
388
  """
277
389
  # entity_id -> [fact_id, ...]
278
390
  self._entity_to_facts: dict[str, list[str]] = defaultdict(list)
@@ -281,22 +393,41 @@ class EntityGraphChannel:
281
393
  self._visible_fact_ids = set()
282
394
 
283
395
  try:
284
- facts = self._db.get_all_facts(
396
+ where, params = _scope_where(
285
397
  profile_id,
286
398
  include_global=include_global,
287
399
  include_shared=include_shared,
288
400
  )
401
+ rows = self._db.execute(
402
+ "SELECT fact_id, canonical_entities_json "
403
+ f"FROM atomic_facts WHERE {where} "
404
+ "ORDER BY created_at DESC LIMIT ?",
405
+ (*params, _unbounded_facts_ceiling()),
406
+ )
289
407
  except Exception:
290
- facts = []
291
- for fact in facts:
292
- self._visible_fact_ids.add(fact.fact_id)
293
- for eid in fact.canonical_entities:
294
- self._entity_to_facts[eid].append(fact.fact_id)
295
- self._fact_to_entities[fact.fact_id].append(eid)
408
+ rows = []
409
+ for row in rows:
410
+ data = dict(row)
411
+ fact_id = str(data.get("fact_id") or "")
412
+ if not fact_id:
413
+ continue
414
+ self._visible_fact_ids.add(fact_id)
415
+ try:
416
+ entity_ids = json.loads(
417
+ data.get("canonical_entities_json") or "[]",
418
+ )
419
+ except (TypeError, ValueError):
420
+ entity_ids = []
421
+ for entity_id in entity_ids:
422
+ if not isinstance(entity_id, str) or not entity_id:
423
+ continue
424
+ self._entity_to_facts[entity_id].append(fact_id)
425
+ self._fact_to_entities[fact_id].append(entity_id)
296
426
 
297
427
  logger.info(
298
428
  "Loaded entity maps: %d entities, %d facts with entities",
299
- len(self._entity_to_facts), len(self._fact_to_entities),
429
+ len(self._entity_to_facts),
430
+ len(self._fact_to_entities),
300
431
  )
301
432
 
302
433
  def _load_graph_metrics(self, profile_id: str) -> None:
@@ -324,7 +455,8 @@ class EntityGraphChannel:
324
455
  }
325
456
  logger.info(
326
457
  "Loaded graph metrics: %d facts for profile %s",
327
- len(self._graph_metrics), profile_id,
458
+ len(self._graph_metrics),
459
+ profile_id,
328
460
  )
329
461
  except Exception as exc:
330
462
  logger.debug("Graph metrics load failed (graceful degradation): %s", exc)
@@ -412,7 +544,7 @@ class EntityGraphChannel:
412
544
  # Spreading activation through graph edges (all in-memory O(1) lookups)
413
545
  frontier = set(activation.keys())
414
546
  for hop in range(1, self._max_hops):
415
- hop_decay = self._decay ** hop
547
+ hop_decay = self._decay**hop
416
548
  if hop_decay < self._threshold:
417
549
  break
418
550
  next_frontier: set[str] = set()
@@ -448,9 +580,8 @@ class EntityGraphChannel:
448
580
  ):
449
581
  neighbor = edge.target_id if edge.source_id == fid else edge.source_id
450
582
  propagated = activation[fid] * self._decay
451
- if (
452
- propagated >= self._threshold
453
- and propagated > activation.get(neighbor, 0.0)
583
+ if propagated >= self._threshold and propagated > activation.get(
584
+ neighbor, 0.0
454
585
  ):
455
586
  activation[neighbor] = propagated
456
587
  next_frontier.add(neighbor)
@@ -490,6 +621,7 @@ class EntityGraphChannel:
490
621
  # v3.4.1 P2: Community-aware boosting
491
622
  if self._graph_metrics and use_cache:
492
623
  from collections import Counter as _Counter
624
+
493
625
  seed_communities: _Counter = _Counter()
494
626
  for eid in canonical_ids:
495
627
  for fid in self._entity_to_facts.get(eid, ()):
@@ -629,7 +761,7 @@ class EntityGraphChannel:
629
761
 
630
762
  frontier = set(activation.keys())
631
763
  for hop in range(1, self._max_hops):
632
- hop_decay = self._decay ** hop
764
+ hop_decay = self._decay**hop
633
765
  if hop_decay < self._threshold:
634
766
  break
635
767
  next_frontier: set[str] = set()
@@ -664,6 +796,7 @@ class EntityGraphChannel:
664
796
  # Community-aware boosting (same as search)
665
797
  if self._graph_metrics and use_cache:
666
798
  from collections import Counter as _Counter
799
+
667
800
  seed_communities: _Counter = _Counter()
668
801
  for eid in canonical_ids:
669
802
  for fid in self._entity_to_facts.get(eid, ()):
@@ -691,7 +824,9 @@ class EntityGraphChannel:
691
824
  return scored
692
825
 
693
826
  def _suppress_contradictions(
694
- self, activation: dict[str, float], profile_id: str,
827
+ self,
828
+ activation: dict[str, float],
829
+ profile_id: str,
695
830
  ) -> None:
696
831
  """P3: Penalize older fact in contradiction pairs, heavy-penalize superseded.
697
832
 
@@ -783,7 +918,10 @@ class EntityGraphChannel:
783
918
  return ids
784
919
 
785
920
  def _discover_entities(
786
- self, fact_ids: set[str], profile_id: str, visited: set[str],
921
+ self,
922
+ fact_ids: set[str],
923
+ profile_id: str,
924
+ visited: set[str],
787
925
  ) -> list[str]:
788
926
  """Find new canonical entity IDs referenced by a set of facts."""
789
927
  new: list[str] = []
@@ -797,7 +935,8 @@ class EntityGraphChannel:
797
935
  )
798
936
  for fid in allowed_fact_ids:
799
937
  rows = self._db.execute(
800
- "SELECT canonical_entities_json FROM atomic_facts WHERE fact_id = ?", (fid,),
938
+ "SELECT canonical_entities_json FROM atomic_facts WHERE fact_id = ?",
939
+ (fid,),
801
940
  )
802
941
  if not rows:
803
942
  continue
@@ -815,8 +954,11 @@ class EntityGraphChannel:
815
954
 
816
955
  # v3.4.5: CozoDB-backed search (Sprint 2)
817
956
  def _search_via_cozo(
818
- self, query: str, raw_entities: list[str],
819
- profile_id: str, top_k: int,
957
+ self,
958
+ query: str,
959
+ raw_entities: list[str],
960
+ profile_id: str,
961
+ top_k: int,
820
962
  *,
821
963
  include_global: bool = False,
822
964
  include_shared: bool = False,
@@ -879,7 +1021,10 @@ class EntityGraphChannel:
879
1021
  return self._search_without_cozo(query, profile_id, top_k)
880
1022
 
881
1023
  def _search_without_cozo(
882
- self, query: str, profile_id: str, top_k: int,
1024
+ self,
1025
+ query: str,
1026
+ profile_id: str,
1027
+ top_k: int,
883
1028
  ) -> list[tuple[str, float]]:
884
1029
  """Run canonical SQLite entity recall without recursive projection use."""
885
1030
  cozo, self._cozo = self._cozo, None